Table of Contents

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 Service without 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 Temperature while UseTemperature is False. The value is never sent, and the model keeps answering at the provider default.
  • Treating an empty AResponse as a failure. It is also what a tool-only turn looks like. Check AHttpStatusCode before reporting an error.
  • Starting a second query from inside OnExecuted without checking LLM.Busy. The first exchange may still be unwinding its tool calls.
  • Expecting the client to remember the conversation. Execute replaces the context every time.
  • Hard-coding a key to "test quickly". Use APIKeys.LoadFromFile or the environment from the first line of code; a key committed once is a key leaked.

See also