Table of Contents

Defining prompts

A prompt is a reusable, named conversation opener that a server offers and a user picks — a slash command in a chat client, an entry in a prompt palette, a "summarize this ticket" button. Unlike a tool, a prompt is not chosen by the model: the user chooses it, fills in its arguments, and the resulting messages are inserted into the conversation. That makes the declaration a small contract with two audiences. The name and title are what a person sees in a list, and the arguments are what they are asked to fill in, so both have to read well outside your code. This guide covers the two ways to declare a prompt, how arguments are declared and ordered, the presentation metadata a client can show, and how to grow the list at runtime and tell connected clients it changed.

Registering a prompt

TTMSMCPPrompts.RegisterPrompt is the compact form. It creates the item, sets the name and description, and attaches the handler in one call:

procedure TForm1.RegisterSummarizePrompt;
var
  Arg: TTMSMCPPromptArgument;
begin
  { Server is a TTMSMCPServer dropped on the form. }
  Server.Prompts.RegisterPrompt('summarize_ticket',
    'Asks the model to summarize a support ticket in three sentences',
    function(const Args: array of TValue): TTMSMCPPromptMessages
    begin
      Result := TTMSMCPPromptMessages.Create(nil);
      Result.AddUserMessage(Format(
        'Summarize support ticket %s in three sentences.', [Args[0].AsString]));
    end);

  { RegisterPrompt declares no arguments, so declare them separately -
    the client needs them to build the prompts/get call. }
  Arg := Server.Prompts.FindByName('summarize_ticket').Arguments.Add;
  Arg.Name := 'ticket_id';
  Arg.Description := 'Identifier of the ticket to summarize';
  Arg.Required := True;
end;

Note what RegisterPrompt does not do: it declares no arguments. The handler receives Args positionally, and the client only knows what to send from the arguments you declare — so declare them through Arguments.Add, as above, or use the builder, which keeps them next to the handler that reads them.

RegisterPrompt rejects an empty name, a nil handler, and a name that is already taken. All three raise a JSON-RPC error rather than failing silently, so a duplicate registration during start-up surfaces immediately.

Using the fluent builder

TTMSMCPPrompt.CreateBuilder returns a builder whose every method returns the builder again, so a complete declaration reads as one expression:

procedure TForm1.RegisterReviewUnitPrompt;
var
  Prompt: TTMSMCPPrompt;
begin
  Prompt := TTMSMCPPrompt.CreateBuilder
    .Name('review_unit')
    .Title('Review a unit')
    .Description('Asks the model to review one source unit for a named concern')
    .Handler(
      function(const Args: array of TValue): TTMSMCPPromptMessages
      begin
        Result := TTMSMCPPromptMessages.Create(nil);
        Result.AddUserMessage(Format(
          'Review the unit %s. Focus on %s and list findings as a numbered list.',
          [Args[0].AsString, Args[1].AsString]));
      end)
    .AddArgument
      .Name('unit_name')
      .Description('Unit to review, for example TMS.MCP.Prompts.pas')
      .Required(True)
      .&End
    .AddArgument
      .Name('focus')
      .Description('What to concentrate on, for example memory management')
      .Required(False)
      .&End
    .Build;

  { AddPrompt copies the declaration into the server collection instead of
    reparenting the instance, so do not free the prompt you built. }
  Server.Prompts.AddPrompt(Prompt);
end;

Two syntax details are worth stating plainly. AddArgument switches from the prompt builder to the argument builder, and .&End switches back — the ampersand is required because End is a reserved word. And Build validates before it returns: a prompt with no name or no handler raises rather than producing a half-declared item.

Ownership differs from the tools collection here, and the difference matters. TTMSMCPPrompts.AddPrompt copies the declaration — name, title, description, icons, handler, and every argument — into a new item in the collection. It does not reparent the instance you pass, so the built prompt is not owned by the server collection and must not be freed after adding.

Declaring arguments

An argument is a named slot the user fills in before the prompt runs. TTMSMCPPromptArgument carries just three things: a Name, a Description, and a Required flag. There is no type — every value reaches the handler as a string.

procedure TForm1.RegisterTranslatePrompt;
var
  Prompt: TTMSMCPPrompt;
begin
  Prompt := Server.Prompts.Add;
  Prompt.Name := 'translate_snippet';
  Prompt.Description := 'Asks the model to translate a text fragment';

  { The three-parameter Add overload sets name, description, and the
    required flag in one call. Declaration order is argument order. }
  Prompt.Arguments.Add('text', 'Text to translate', True);
  Prompt.Arguments.Add('target_language', 'Language to translate into', True);
  Prompt.Arguments.Add('tone', 'Desired tone, for example formal or casual');

  Prompt.PromptMethod :=
    function(const Args: array of TValue): TTMSMCPPromptMessages
    var
      Tone: string;
    begin
      Tone := 'neutral';
      if (Length(Args) > 2) and not Args[2].IsEmpty then
        Tone := Args[2].AsString;

      Result := TTMSMCPPromptMessages.Create(nil);
      Result.AddUserMessage(Format(
        'Translate the following text into %s using a %s tone.'#13#10#13#10'%s',
        [Args[1].AsString, Tone, Args[0].AsString]));
    end;
end;

Three rules follow from how arguments are dispatched:

Rule Consequence
Arguments are passed positionally, in declaration order Args[0] is the first declared argument. Inserting a new argument in the middle shifts every later index.
A missing required argument is rejected before the handler runs You never have to check for it. The client gets Required argument "x" is missing.
A missing optional argument arrives as an empty TValue Test with Args[n].IsEmpty and supply a default.

Because the position is the contract, append new arguments at the end of an existing prompt rather than inserting them, and keep the descriptions specific — they are what the user reads in the client's argument form.

Names, titles, and icons

Name is the protocol identifier a client uses in prompts/get. Keep it in the stable lowercase underscore form other MCP servers use, and treat it as frozen once a server has shipped: saved conversations and client configurations refer to a prompt by name.

Title is the human-readable label. It is free to change, so put the readable wording there. AddIcon attaches one or more icon variants — a source URL, an optional MIME type, and an optional size — which a client may show next to the title:

procedure TForm1.RegisterReleaseNotesPrompt;
var
  Prompt: TTMSMCPPrompt;
begin
  Prompt := TTMSMCPPrompt.CreateBuilder
    .Name('draft_release_notes')
    .Title('Draft release notes')
    .Description('Drafts release notes from a list of committed changes')
    .AddIcon('https://cdn.example.com/icons/notes-24.png', 'image/png', '24x24')
    .AddIcon('https://cdn.example.com/icons/notes-48.png', 'image/png', '48x48')
    .Handler(
      function(const Args: array of TValue): TTMSMCPPromptMessages
      begin
        Result := TTMSMCPPromptMessages.Create(nil);
        Result.AddUserMessage(Format(
          'Turn these changes into release notes for version %s:'#13#10'%s',
          [Args[0].AsString, LoadChangeLog(Args[0].AsString)]));
      end)
    .AddArgument
      .Name('version')
      .Description('Version the notes are written for, for example 2.0.0.0')
      .Required(True)
      .&End
    .Build;

  Server.Prompts.AddPrompt(Prompt);
end;

Each AddIcon call appends to the IconsJSON string rather than replacing it, so several calls declare several sizes of the same icon. A client picks the variant that fits its display.

Listing prompts dynamically

Not every prompt is known at start-up. OnListPrompts on the server fires on every prompts/list call, with the live collection, so the list can reflect what the application currently has loaded:

procedure TForm1.FormCreate(Sender: TObject);
begin
  Server.EnablePromptNotifications := True;
  Server.OnListPrompts := HandleListPrompts;
end;

procedure TForm1.HandleListPrompts(Sender: TObject;
  const PromptsList: TTMSMCPPrompts);
var
  Prompt: TTMSMCPPrompt;
  ProjectName: string;
begin
  { Runs on every prompts/list call, before the list is serialized. }
  ProjectName := CurrentProjectName;
  if ProjectName = '' then
    Exit;

  { FindByName raises on an empty name, so guard before searching, and
    skip the work when the prompt is already registered. }
  if PromptsList.FindByName('summarize_current_project') <> nil then
    Exit;

  Prompt := PromptsList.Add;
  Prompt.Name := 'summarize_current_project';
  Prompt.Title := Format('Summarize %s', [ProjectName]);
  Prompt.Description := Format(
    'Summarizes the project currently open in the application (%s)',
    [ProjectName]);
  Prompt.Arguments.Add('detail', 'How detailed the summary should be');
  Prompt.PromptMethod :=
    function(const Args: array of TValue): TTMSMCPPromptMessages
    begin
      Result := TTMSMCPPromptMessages.Create(nil);
      Result.AddUserMessage(Format('Summarize the project %s.'#13#10'%s',
        [CurrentProjectName, CurrentProjectOutline]));
    end;
end;

Because the handler runs on every list request, it must be idempotent — check with FindByName before adding, or you will accumulate duplicates. Note that FindByName raises on an empty name, so guard any name you build from user data.

Announcing list changes

When the set of prompts changes after a client has already listed them, the client needs to be told. Set EnablePromptNotifications to True and the server broadcasts notifications/prompts/list_changed whenever the collection changes — adding or removing an item raises the collection's OnChanged, which the server forwards for you.

Two details decide whether the notification actually goes out:

  • It is opt-in. With EnablePromptNotifications left False, SendPromptsChangedNotification returns without sending anything, and so does the automatic path.
  • Editing an existing item does not trigger it. Changing Description or Title on a prompt already in the collection does not raise the collection's OnChanged, so call Server.SendPromptsChangedNotification yourself after an in-place edit.

Combining the builder, arguments, icons, and notifications

A prompt published at runtime usually uses all of these at once. This one is declared with the builder, takes one required and one optional argument, carries a title and an icon for the client's prompt list, and is added to a server that has notifications enabled so every connected client refreshes its list:

procedure TForm1.PublishIncidentPrompt;
var
  Prompt: TTMSMCPPrompt;
begin
  { Notifications are opt-in; without this the server never broadcasts
    notifications/prompts/list_changed. }
  Server.EnablePromptNotifications := True;

  Prompt := TTMSMCPPrompt.CreateBuilder
    .Name('analyze_incident')
    .Title('Analyze an incident')
    .Description('Walks the model through an incident report and asks for a root cause')
    .AddIcon('https://cdn.example.com/icons/incident-24.png', 'image/png', '24x24')
    .Handler(
      function(const Args: array of TValue): TTMSMCPPromptMessages
      var
        Audience: string;
      begin
        Audience := 'the engineering team';
        if (Length(Args) > 1) and not Args[1].IsEmpty then
          Audience := Args[1].AsString;

        Result := TTMSMCPPromptMessages.Create(nil);
        Result.AddUserMessage(Format(
          'Here is incident report %s:'#13#10#13#10'%s',
          [Args[0].AsString, LoadIncidentReport(Args[0].AsString)]));
        Result.AddAssistantMessage(
          'I will identify the root cause before proposing any remedy.');
        Result.AddUserMessage(Format(
          'Write the root-cause analysis for %s.', [Audience]));
      end)
    .AddArgument
      .Name('incident_id')
      .Description('Identifier of the incident report to analyze')
      .Required(True)
      .&End
    .AddArgument
      .Name('audience')
      .Description('Who the analysis is written for, for example management')
      .Required(False)
      .&End
    .Build;

  { Adding an item raises the collection OnChanged, which the server turns
    into notifications/prompts/list_changed for every connected client. }
  Server.Prompts.AddPrompt(Prompt);
end;

The handler defaults the optional argument rather than assuming it is present, which is what keeps the prompt usable when the user fills in only the required field.

Common mistakes

  • Freeing the prompt returned by Build. AddPrompt copies the declaration rather than reparenting the instance, so the built prompt is never owned by the server collection. This is the opposite of TTMSMCPResources.AddResource, where Build does hand you the instance to free — check which collection you are working with.
  • Registering without declaring arguments. RegisterPrompt alone gives the client no argument list, so there is nothing for the user to fill in and Args[0] is empty. Declare every argument the handler reads.
  • Inserting an argument in the middle. Arguments are positional. A new argument added before an existing one silently shifts every later Args[n].
  • Assuming a type. There is no argument type: everything arrives as a string. Parse a number or a date yourself, and say so in the description.
  • Expecting a duplicate name to be ignored. RegisterPrompt and AddPrompt both raise on a name that already exists. Guard with FindByName when registering from a code path that can run twice.
  • Changing Name to improve the wording. Rename through Title; Name is what clients and saved conversations refer to.

See also