Table of Contents

Configuring the server

A server's configuration is a contract it publishes. During initialize the client reads the server's identity, its instructions, and the set of capabilities it declares, and every later request is shaped by what was agreed there. A capability you never enable is one the client will never use, however much code sits behind it — so configuration is not boilerplate, it is the part that decides which half of the component is reachable. This guide covers identity, the fluent builder, each capability flag and what turning it on commits you to, change notifications, pagination, and logging.

Identity and instructions

Four properties describe the server to the client and, through it, to the model:

Property Purpose
ServerName The protocol-level name. Use the product name.
ServerVersion Your version string, in MAJOR.MINOR.RELEASE.BUILD form.
ServerDescription One line explaining what the server is for.
Instructions Server-wide guidance passed to the model in the initialize result.

Instructions is the most under-used of the four. It is free text the model reads before deciding anything, so it is the right place for rules that apply across every tool: which tool to prefer, what units your numbers are in, what vocabulary your domain uses, what the model must always state in its answer. Rules you would otherwise repeat in every tool description belong here once.

Using the builder

TTMSMCPServer.CreateBuilder returns a builder class; each method returns the builder again, so the whole configuration reads as one expression and the capability set is visible at a glance:

procedure TForm1.BuildServer;
begin
  Server := TTMSMCPServer.CreateBuilder
    .Name('InventoryServer')
    .ServerVersion('2.1.0')
    .Description('Warehouse inventory and replenishment')
    .Instructions('Prefer stock_level before reorder. Quantities are in units, ' +
      'never cases. Always state the warehouse you used.')
    .EnableToolNotifications(True)
    .EnableResourceNotifications(True)
    .EnablePromptNotifications(True)
    .EnableResourceSubscriptions(True)
    .EnableLogging(True)
    .EnableCompletions(True)
    .EnableSampling(True)
    .EnableElicitation(True)
    .EnableTasks(True)
    .DefaultTaskPollInterval(3000)
    .DefaultPageSize(100)
    .Build;

  Server.Start;
end;

Build returns the configured TTMSMCPServer. The builder covers identity, instructions, every capability flag, the default page size, the default task poll interval, and the transport; anything it does not cover is a plain property assignment afterwards.

Capability flags

Each flag advertises a capability during initialize. A client will not use a capability the server did not declare, and declaring one commits you to handling the requests it invites:

Property Declares What it commits you to
EnableToolNotifications tools.listChanged Calling SendToolsChangedNotification when the tool set changes.
EnableResourceNotifications resources.listChanged Calling SendResourcesChangedNotification when the resource set changes.
EnablePromptNotifications prompts.listChanged Calling SendPromptsChangedNotification when the prompt set changes.
EnableResourceSubscriptions resources.subscribe Handling resources/subscribe and sending SendResourceUpdatedNotification.
EnableLogging logging Pushing entries with SendLogMessage, and filtering them against CurrentLogLevel yourself.
EnableCompletions completions Assigning OnCompletion to supply argument suggestions.
EnableSampling sampling Being able to call RequestSampling — the client runs the completion.
EnableElicitation elicitation Being able to call RequestElicitation to ask the user a question.
EnableTasks tasks Long-running tools/call execution; see Tasks and elicitation.

All of them default to False. Turn on only what the server actually implements — an advertised capability that misbehaves is worse than one that was never offered.

Change notifications

When the tool, resource, or prompt set changes at runtime, the client needs to be told or it will keep working from a stale list. Each collection has its own broadcast, plus a targeted update for a single subscribed resource:

procedure TForm1.AddSeasonalTool;
begin
  { EnableToolNotifications must be on for the client to accept this. }
  Server.Tools.RegisterTool('holiday_stock',
    'Returns seasonal stock reserved for the holiday period',
    function(const Args: array of TValue): TValue
    begin
      Result := HolidayStock(Args[0].AsString);
    end,
    ptInteger);

  Server.SendToolsChangedNotification;
end;

procedure TForm1.PublishPriceList(const AURI: string);
begin
  RefreshPriceList(AURI);

  { Goes to every client subscribed to this URI. }
  Server.SendResourceUpdatedNotification(AURI);
end;
Method Sends to
SendToolsChangedNotification Every connected client.
SendResourcesChangedNotification Every connected client.
SendPromptsChangedNotification Every connected client.
SendResourceUpdatedNotification(AURI) Every client subscribed to that URI.
SendResourceUpdatedForSession(ASessionId, AURI) One session, if it is subscribed.
SendProgressNotification(AToken, AProgress, ATotal, AMessage) The client that supplied the progress token.
SendNotificationToSession(ASessionId, AJsonRpc) One session, raw JSON-RPC.
BroadcastNotification(AJsonRpc) Every session, raw JSON-RPC.

The notification only travels if the matching capability flag is on. Sending one with the flag off is a silent no-op from the client's point of view.

Pagination

DefaultPageSize caps how many items a list request returns at once; the client pages through the rest with a cursor. It defaults to 0, meaning no paging — every tool, resource, and prompt comes back in one response. Set it when a collection grows past a few dozen entries, so a tools/list does not hand the model a wall of text it has to read in full before choosing.

procedure TForm1.ConfigurePagination;
var
  I: Integer;
begin
  { Without this, tools/list returns every tool in one response. }
  Server.DefaultPageSize := 25;

  for I := 0 to CatalogSectionCount - 1 do
    Server.Tools.RegisterTool(
      Format('search_%s', [CatalogSectionKey(I)]),
      Format('Searches the %s section of the catalog', [CatalogSectionName(I)]),
      function(const Args: array of TValue): TValue
      begin
        Result := SearchSection(Args[0].AsString);
      end,
      ptString);

  { The client now pages through the tool list with a cursor. }
  Server.SendToolsChangedNotification;
end;

Logging

With EnableLogging on, the client can raise or lower the server's minimum log level at runtime through logging/setLevel, and CurrentLogLevel reports what it asked for. The filtering is yours to applySendLogMessage does not test CurrentLogLevel; it broadcasts every entry to every connected client once the server is initialized. Check the level before building an entry that is expensive to produce:

procedure TForm1.ConfigureLogging;
begin
  Server.EnableLogging := True;
  Server.CurrentLogLevel := llInfo;
  Server.OnLogLevelChanged := ServerLogLevelChanged;
end;

procedure TForm1.ServerLogLevelChanged(Sender: TObject; ALevel: TTMSMCPLogLevel);
begin
  { The client asked for a different minimum level through logging/setLevel. }
  MemoLog.Lines.Add('Client set log level to ' + GetEnumName(TypeInfo(TTMSMCPLogLevel), Ord(ALevel)));
end;

procedure TForm1.ReportReorder(const AProduct: string; AUnits: Integer);
begin
  { SendLogMessage does not filter - it broadcasts every entry. Test the
    level yourself before doing work to build one. }
  if Server.CurrentLogLevel <= llInfo then
    Server.SendLogMessage(llInfo, 'inventory',
      Format('Reordered %d units of %s', [AUnits, AProduct]));
end;

TTMSMCPLogLevel runs llDebug, llInfo, llNotice, llWarning, llError, llCritical, llAlert, llEmergency. OnLogLevelChanged fires when the client changes the level, which is also a useful signal that a client is paying attention to the log stream at all.

OnLog is a separate, local event carrying the server's own diagnostic messages — it does not go to the client and is not gated by EnableLogging.

Completions

EnableCompletions advertises the completions capability, which lets a client offer as-you-type suggestions while a user fills in an argument of a prompt or a resource template. The server answers each completion/complete request through OnCompletion:

procedure TForm1.ConfigureCompletions;
begin
  Server.EnableCompletions := True;
  Server.OnCompletion := ServerCompletion;
end;

procedure TForm1.ServerCompletion(Sender: TObject; const ARef,
  AArgument: TJSONObject; var AValues: TArray<string>; var ATotal: Integer;
  var AHasMore: Boolean);
var
  ArgumentName, Partial: string;
begin
  { ARef says what is being completed - a prompt or a resource template.
    AArgument carries the argument name and what has been typed so far. }
  ArgumentName := AArgument.GetValue<string>('name', '');
  Partial := AArgument.GetValue<string>('value', '');

  if ArgumentName = 'region' then
    AValues := MatchingRegions(Partial)
  else
    AValues := [];

  ATotal := Length(AValues);
  AHasMore := False;
end;

The handler receives ARef — what is being completed, a prompt or a resource template — and AArgument, the argument name plus the partial value typed so far. It answers through three var parameters: AValues with the suggestions, ATotal with the number of matches that exist, and AHasMore when the list was truncated. With EnableCompletions on but no handler assigned, the server still answers the request — with an empty list, which is worse than not advertising the capability at all.

Error codes

Failures reach the client as JSON-RPC errors. TTMSMCPErrorCode (declared in TMS.MCP.Helpers) holds the standard codes plus the server's own extensions:

Value Code Raised when
ecParseError -32700 The request was not valid JSON.
ecInvalidRequest -32600 The JSON was valid but not a JSON-RPC request.
ecMethodNotFound -32601 The method is unknown — or the capability behind it is off, which is how a sampling/createMessage arriving with EnableSampling false is answered.
ecInvalidParams -32602 Parameters are missing or the wrong shape: no tool name, no resource URI, no prompt name.
ecInternalError -32603 The handler failed, including a sampling request arriving with no OnSamplingRequest assigned.
ecServerNotInitialized -32000 A request arrived before initialize completed.
ecUnknownTool -32001 Reserved for an unknown tool. See the note below — the current server does not use it.
ecResourceNotFound -32002 No resource or template matches the requested URI. The tasks methods reuse this number for an unknown task id — including one belonging to another session, so a client cannot probe for tasks it does not own.
ecPromptNotFound -32003 prompts/get named a prompt that is not in the collection.
ecOperationFailed -32004 Reading a resource or building a prompt raised.
ecURLElicitationRequired -32042 Reserved for a request that cannot proceed until the user completes a URL-mode elicitation.

A failing tools/call is the exception to the table: it does not come back as a JSON-RPC error. An unknown tool name, or an exception escaping a tool method, returns a normal result with isError set to true and the message as a text content item — which is what the MCP specification asks for, so the model sees the failure and can react to it instead of the client swallowing a transport-level error. That is why ecUnknownTool is declared but never raised. Inside a task the same failure is reported through SetTaskError instead, with code -32602.

Combining identity, capabilities, and notifications

A realistic server sets its identity and instructions, turns on the capabilities it implements, pages its collections, and then keeps the client's view current as things change. Each flag is there because a later call depends on it:

procedure TForm1.ConfigureCatalogServer;
begin
  Server := TTMSMCPServer.CreateBuilder
    .Name('CatalogServer')
    .ServerVersion('3.0.0')
    .Description('Product catalog search and maintenance')
    .Instructions('Search before editing. Prices are in euro, excluding VAT. ' +
      'Always name the catalog section you searched.')
    .EnableToolNotifications(True)
    .EnableResourceNotifications(True)
    .EnableResourceSubscriptions(True)
    .EnableLogging(True)
    .DefaultPageSize(25)
    .Build;

  Server.CurrentLogLevel := llInfo;
  Server.OnLogLevelChanged := ServerLogLevelChanged;
  Server.Start;
end;

procedure TForm1.PublishSeasonalCatalog(const AURI: string);
begin
  { Capabilities declared above are what make these two calls meaningful:
    EnableToolNotifications for the list change, EnableResourceSubscriptions
    for the targeted resource update, EnableLogging for the audit line. }
  Server.Tools.RegisterTool('search_seasonal',
    'Searches the seasonal catalog section',
    function(const Args: array of TValue): TValue
    begin
      Result := SearchSeasonal(Args[0].AsString);
    end,
    ptString);
  Server.SendToolsChangedNotification;

  RefreshSeasonalCatalog(AURI);
  Server.SendResourceUpdatedNotification(AURI);

  Server.SendLogMessage(llInfo, 'catalog',
    'Seasonal catalog published and search tool registered');
end;

That is the whole loop in one place — builder-set identity and instructions, three capability flags, a page size, and then the notification and log calls those flags make meaningful. Turn any one flag off and the corresponding call becomes a silent no-op from the client's point of view, which is what makes configuration and runtime behaviour a single subject rather than two.

Common mistakes

  • Enabling a capability with nothing behind it. EnableCompletions := True without an OnCompletion handler advertises argument completion and then answers nothing.
  • Changing a collection without notifying. Adding a tool at runtime while EnableToolNotifications is off leaves every connected client using the list it fetched at connect time.
  • Putting cross-cutting rules in every tool description. That is what Instructions is for. The tool description should describe the tool.
  • Leaving DefaultPageSize at 0 with a large collection. The whole list goes to the model in one response, consuming context before any work starts.
  • Expecting OnLog to reach the client. It is local. SendLogMessage is the one that travels.

See also