Querying a language model
Half of TTMSMCPClient is a language-model client. Before a single MCP server
matters, the component has to know which service to call, with which key, under
which model name, and what to do with the answer that comes back. That surface
is not the client's own: LLM is a full
TTMSMCPCloudAI instance, and Service,
APIKeys, Settings and Tools are its properties re-exposed on the client so
they can be set at design time too. This guide covers picking a provider and its
model, keeping keys out of your source, shaping the request, controlling which
tools the model is told about, and reading the result.
Choosing a service and its model
Reach for this first, because every other setting is read per provider. Service
selects which endpoint the request goes to, and
TTMSMCPCloudAISettings carries one
model property per service — so assigning Service without also setting that
provider's model leaves the previous provider's model name in place, where it
will be rejected:
procedure TForm1.SelectService(AService: TTMSMCPCloudAIService);
begin
MCPClient.Service := AService;
{ Each provider reads its own model property from Settings, so setting
Service alone is never enough. }
case AService of
aiOpenAI:
MCPClient.Settings.OpenAIModel := 'gpt-4o-mini';
aiClaude:
MCPClient.Settings.ClaudeModel := 'claude-sonnet-4-5';
aiGemini:
MCPClient.Settings.GeminiModel := 'gemini-2.5-flash';
aiGrok:
MCPClient.Settings.GrokModel := 'grok-4';
aiMistral:
MCPClient.Settings.MistralModel := 'mistral-large-latest';
aiDeepSeek:
MCPClient.Settings.DeepSeekModel := 'deepseek-chat';
aiPerplexity:
MCPClient.Settings.PerplexityModel := 'sonar';
aiOpenRouter:
MCPClient.Settings.OpenRouterModel := 'openai/gpt-4o-mini';
aiOllama:
begin
{ A local runtime needs a host and a port instead of a key. }
MCPClient.Settings.OllamaHost := 'localhost';
MCPClient.Settings.OllamaPort := 11434;
MCPClient.Settings.OllamaModel := 'llama3.1';
end;
aiLlamaCpp:
begin
MCPClient.Settings.LlamaCppHost := 'localhost';
MCPClient.Settings.LlamaCppPort := 8080;
MCPClient.Settings.LlamaCppModel := 'local-model';
end;
end;
end;
| Service | Model property | Also needs |
|---|---|---|
aiOpenAI |
OpenAIModel |
APIKeys.OpenAI, optionally OpenAIServers for the EU endpoint |
aiClaude |
ClaudeModel |
APIKeys.Claude; CacheControl applies here only |
aiGemini |
GeminiModel |
APIKeys.Gemini |
aiGrok |
GrokModel |
APIKeys.Grok |
aiMistral |
MistralModel |
APIKeys.Mistral |
aiDeepSeek |
DeepSeekModel |
APIKeys.DeepSeek |
aiPerplexity |
PerplexityModel |
APIKeys.Perplexity |
aiOpenRouter |
OpenRouterModel |
APIKeys.OpenRouter |
aiOllama |
OllamaModel |
OllamaHost, OllamaPort, optionally OllamaPath |
aiLlamaCpp |
LlamaCppModel |
LlamaCppHost, LlamaCppPort, optionally LlamaCppPath |
The two local runtimes need no key at all — they are reached over plain HTTP on the host and port you give them, which makes them the easiest way to try an agent without an account anywhere.
Supplying API keys
Do this before the first query, and do it from somewhere other than the source
file. TTMSMCPCloudAIAPIKeys has one
string per hosted service plus SaveToFile and LoadFromFile, which encrypt the
whole set with a password you supply — that is the mechanism the
settings dialog writes to. The environment is a reasonable
fallback for build servers and unattended tools:
procedure TForm1.LoadAPIKeys;
var
KeyFile: string;
begin
{ Preferred: the keys were entered once through the settings dialog and
written back encrypted. SaveToFile and LoadFromFile take the password
as their second argument. }
KeyFile := ChangeFileExt(ParamStr(0), '.keys');
if TFile.Exists(KeyFile) then
MCPClient.APIKeys.LoadFromFile(KeyFile, FKeyFilePassword)
else
begin
{ Fallback for unattended builds - never hard-code a key. }
MCPClient.APIKeys.OpenAI := GetEnvironmentVariable('OPENAI_API_KEY');
MCPClient.APIKeys.Claude := GetEnvironmentVariable('ANTHROPIC_API_KEY');
MCPClient.APIKeys.Gemini := GetEnvironmentVariable('GEMINI_API_KEY');
MCPClient.APIKeys.Mistral := GetEnvironmentVariable('MISTRAL_API_KEY');
end;
{ Only the key belonging to the active service is used, and the local
runtimes use a host and a port instead of a key. }
if (MCPClient.Service = aiOpenAI) and (MCPClient.APIKeys.OpenAI = '') then
raise EMCPClientException.Create(
'No OpenAI API key configured. Set OPENAI_API_KEY and restart.');
end;
Only the key for the active service is read, so an application that offers a provider list should check that the selected one actually has a key and disable the ask button when it does not, rather than letting the request fail with an authentication error the user cannot interpret.
Shaping the request
Turn to this when the answers are the wrong length, too creative, or too slow.
Settings is shared by every request the client makes, and most of its
generation properties are honoured only where the provider supports them:
procedure TForm1.ConfigureRequest;
begin
{ A ceiling on the answer, not on the conversation. 0 leaves the
provider default in place. }
MCPClient.Settings.MaxTokens := 2048;
{ Some providers reject a temperature outright; UseTemperature decides
whether the value is sent at all. }
MCPClient.Settings.UseTemperature := True;
MCPClient.Settings.Temperature := 0.2;
{ Let the model ask for several tools in one turn. }
MCPClient.Settings.ParallelToolExecution := True;
{ Reasoning effort, where the provider supports it. aiIgnore - the
default - leaves the request untouched. }
MCPClient.Settings.Reasoning := aiLow;
MCPClient.Settings.CacheControl := ccNone;
MCPClient.Settings.WebSearch := False;
{ Standing instructions for every query. They travel as the system role,
so they are stated once per request rather than repeated inside every
tool description. }
MCPClient.LLM.SystemRole.Text :=
'You are an internal reporting assistant. Prefer the connected tools ' +
'over guessing, and always state which report a number came from.';
end;
| Property | Effect |
|---|---|
MaxTokens |
Ceiling on the generated answer. 0 leaves the provider default. |
UseTemperature / Temperature |
UseTemperature decides whether a temperature is sent at all; some providers reject one outright. |
Reasoning |
aiLow, aiMedium, aiHigh, aiOff, or aiIgnore. aiIgnore — the default — omits the field. |
ParallelToolExecution |
Lets the model request several tools in one turn. |
CacheControl |
ccNone or ccEphemeral. A Claude-only prompt-caching hint. |
WebSearch |
Offers the provider's own web search alongside your tools. |
CustomOptions |
Raw JSON merged into the request for anything the properties do not cover. |
LLM.SystemRole is the companion of these settings and the most useful of the
lot. It is free text sent as the system role on every request, which makes it
the right home for rules that apply across all tools — which tool to prefer,
what units your numbers are in, what the answer must always state. Rules put
there are stated once per request instead of being repeated in every tool
description.
The tool collection the model sees
Look here when the model calls the wrong tool, or when you want it to know about
something no MCP server provides. Tools — added in 1.0.2.1 as direct access to
LLM.Tools — is the single list the model is told about. Every running
connection contributes its discovered tools to it with AutoCreated set to
True, and anything you add yourself keeps AutoCreated at False and
survives the rebuild that follows a reconnect:
procedure TForm1.ShowAdvertisedTools;
var
I: Integer;
Names: TStringList;
begin
{ MCPClient.Tools is the same collection as MCPClient.LLM.Tools, surfaced
on the client for convenience. Tools discovered on a connected MCP
server are added here with AutoCreated set to True; tools you add
yourself keep it False and survive a refresh. }
Names := TStringList.Create;
try
for I := 0 to MCPClient.Tools.Count - 1 do
if MCPClient.Tools[I].Enabled then
Names.Add(MCPClient.Tools[I].Name + ' - ' +
MCPClient.Tools[I].Description);
ShowAnswer(Names.Text);
finally
Names.Free;
end;
end;
procedure TForm1.DisableTool(const AToolName: string);
var
I: Integer;
begin
{ A disabled tool stays in the collection but is not advertised to the
model, so the connection that provides it keeps running. }
for I := 0 to MCPClient.Tools.Count - 1 do
if SameText(MCPClient.Tools[I].Name, AToolName) then
MCPClient.Tools[I].Enabled := False;
end;
procedure TForm1.AddLocalTool;
var
Tool: TTMSMCPCloudAITool;
Param: TTMSMCPCloudAIParameter;
begin
{ A hand-written tool answered by this application, offered to the model
alongside the ones the MCP servers contribute. AutoCreated stays False,
so a server reconnect does not remove it. }
Tool := MCPClient.Tools.Add;
Tool.Name := 'current_user';
Tool.Description := 'Returns the name of the signed-in user.';
Tool.OnExecute := DoCurrentUserTool;
Param := Tool.Parameters.Add;
Param.Name := 'format';
Param.Description := 'short or full';
Param.&Type := ptEnum;
Param.Required := True;
Param.Enum.Add('short');
Param.Enum.Add('full');
end;
procedure TForm1.DoCurrentUserTool(Sender: TObject; Args: TJSONObject;
var Result: string);
var
Shape: string;
begin
if not Args.TryGetValue<string>('format', Shape) then
Shape := 'short';
Result := GetEnvironmentVariable('USERNAME');
if Shape = 'full' then
Result := GetEnvironmentVariable('USERDOMAIN') + '\' + Result;
end;
Enabled is the lever to reach for when the model should not see a tool for one
question. It leaves the connection running and the tool discovered, and only
withholds it from the request — unlike stopping the server, which also loses
every other tool that server provides.
Sending a query and reading the answer
This is the whole runtime loop. Execute replaces LLM.Context with your text
and posts the request; it returns immediately, and the answer arrives on
OnExecuted after the client has run every tool the model asked for:
procedure TForm1.AskButtonClick(Sender: TObject);
begin
{ Execute returns immediately; the answer arrives on OnExecuted. }
if MCPClient.LLM.Busy then
begin
ShowAnswer('A request is still running.');
Exit;
end;
MCPClient.OnExecuted := DoExecuted;
WaitIndicator.Visible := True;
MCPClient.Execute(GetQuestionFromUI);
end;
procedure TForm1.DoExecuted(Sender: TObject; AResponse: string;
AHttpStatusCode: Integer; AHttpResult: string);
begin
WaitIndicator.Visible := False;
{ AResponse is the assistant answer. It is empty when the call failed,
when the model answered with tool calls only, or when the provider
returned an error - AHttpStatusCode and AHttpResult say which. }
if AResponse <> '' then
ShowAnswer(AResponse)
else if (AHttpStatusCode < 200) or (AHttpStatusCode >= 300) then
ShowAnswer(Format('The service returned %d: %s',
[AHttpStatusCode, AHttpResult]))
else
ShowAnswer('The model returned no text for this turn.');
end;
The three arguments of
TTMSMCPClientAIResultEvent have to
be read together. AResponse is the assistant's text and is empty when the
request failed, when the turn produced only tool calls, or when the provider
answered with an error; AHttpStatusCode and AHttpResult carry the transport
result and say which of those happened. LLM.Busy is True for the whole
exchange, tool round trips included, so it is the right guard against starting a
second query on top of the first.
The component does not accumulate a transcript: each Execute sends the text
you pass plus LLM.SystemRole, and nothing from the previous turn. Multi-turn
conversations are the application's job — keep the exchange yourself and pass
the part you want remembered, or set LLM.AssistantRole with the answer the
model should treat as its own last words.
Putting it together
A realistic ask button touches almost all of the above in sequence: it switches provider, loads that provider's key, tunes the request for the kind of answer wanted, narrows the tool surface to what this particular question should be allowed to reach, and only then sends the text:
procedure TForm1.AskWithClaude(const AQuestion: string);
var
I: Integer;
begin
if MCPClient.LLM.Busy then
Exit;
{ 1. Service selection. }
MCPClient.Service := aiClaude;
{ 2. The key for that service. }
MCPClient.APIKeys.Claude := GetEnvironmentVariable('ANTHROPIC_API_KEY');
if MCPClient.APIKeys.Claude = '' then
raise EMCPClientException.Create('No Claude API key configured.');
{ 3. Request tuning. ccEphemeral is a Claude-only setting. }
MCPClient.Settings.ClaudeModel := 'claude-sonnet-4-5';
MCPClient.Settings.MaxTokens := 4096;
MCPClient.Settings.UseTemperature := True;
MCPClient.Settings.Temperature := 0.1;
MCPClient.Settings.CacheControl := ccEphemeral;
{ 4. Narrow the tool surface for this question: read-only tools only. }
for I := 0 to MCPClient.Tools.Count - 1 do
MCPClient.Tools[I].Enabled :=
MCPClient.Tools[I].Name.StartsWith('read_') or
MCPClient.Tools[I].Name.StartsWith('list_');
{ 5. Send it. }
MCPClient.OnExecuted := DoExecuted;
MCPClient.Execute(AQuestion);
end;
That ordering matters in one place only — the tool loop has to run after the servers have finished discovery, or it walks an empty collection and disables nothing. Everything else can be set at design time and left alone.
Common mistakes
- Changing
Servicewithout changing the model. Each provider reads its own model property. The request goes out with whatever was left in it and comes back as a model-not-found error. - Setting
TemperaturewhileUseTemperatureisFalse. The value is never sent, and the model keeps answering at the provider default. - Treating an empty
AResponseas a failure. It is also what a tool-only turn looks like. CheckAHttpStatusCodebefore reporting an error. - Starting a second query from inside
OnExecutedwithout checkingLLM.Busy. The first exchange may still be unwinding its tool calls. - Expecting the client to remember the conversation.
Executereplaces the context every time. - Hard-coding a key to "test quickly". Use
APIKeys.LoadFromFileor the environment from the first line of code; a key committed once is a key leaked.
See also
- Connecting to MCP servers — where the tools come from
- The settings and tools dialogs — letting the user edit keys and models
- Cloud AI — the component behind
LLM, and everything else it offers TTMSMCPClient,TTMSMCPCloudAISettings,TTMSMCPCloudAITool