Table of Contents

Hosting attributed servers

Attributes describe a surface; something has to read them. That something is TTMSMCPAttributedServer, a TTMSMCPServer descendant that keeps a collection of targets — the classes and object instances it reflects over — and rebuilds its tools, resources, and prompts from them. Everything a hand-built server offers still applies: transports, capabilities, sessions, notifications, and tasks are unchanged, because the attributed server only replaces the registration step. This guide covers registering targets, when the reflection pass actually runs, who owns what, giving a target a way back to its server, and the resource and prompt attributes the pass also reads.

Creating the server

Reach for the server directly when you have more than one target, more than one transport, or anything else to configure; reach for the factory when you have exactly one class to expose. TTMSMCPAttributedServer is an ordinary component — drop it on a form or create it in code with an owner:

procedure TForm1.BuildAttributedServer;
var
  Target: TTMSMCPTarget;
begin
  Server := TTMSMCPAttributedServer.Create(Self);
  Server.ServerName := 'Workshop';
  Server.ServerVersion := '1.0.0';

  { A class target exposes class methods only - no instance is needed. }
  Server.AddClass(TUnitConverter);

  { An instance target also exposes ordinary methods. AddObject leaves the
    instance yours; set OwnsObject so the target frees it instead. }
  Target := Server.AddObject(TCatalogService.Create);
  Target.OwnsObject := True;

  { Targets are matched by class and by instance, so adding the same target
    again returns the existing entry instead of duplicating its tools. }
  Server.AddClass(TUnitConverter);

  Memo1.Lines.Add(Format('%d target(s), %d tool(s) generated',
    [Server.Targets.Count, Server.Tools.Count]));
end;

TTMSMCPServerFactory is the one-liner for the common case. CreateFromObject creates a server and registers an instance; CreateFromClass does the same for a class. Both accept an optional owner component, and both return a server whose tools are already generated:

program CatalogServer;

{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  TMS.MCP.Attributes.Server,
  TMS.MCP.Transport.STDIO,
  CatalogService in 'CatalogService.pas';

var
  Service: TCatalogService;
  Server: TTMSMCPAttributedServer;
begin
  Service := TCatalogService.Create;
  try
    { CreateFromObject reflects over the class straight away, so the server
      already carries one tool per attributed method when it returns. }
    Server := TTMSMCPServerFactory.CreateFromObject(Service);
    try
      Server.ServerName := 'Catalog';
      Server.ServerVersion := '1.0.0';

      Server.Start;
      Server.Run;
    finally
      Server.Free;
    end;
  finally
    { The factory does not take ownership of the service instance. }
    Service.Free;
  end;
end.

Class targets and instance targets

Choose between the two according to what your methods need, because the reflection pass treats them differently. A class target — added with AddClass — can only invoke class function and class procedure members, since there is no instance to call an ordinary method on; instance methods on a class-only target are skipped. An instance target — added with AddObject — exposes both, so any method that touches fields, a database connection, or anything else the object holds needs one.

Both are stored as a TTMSMCPTarget in the server's Targets collection, and both are deduplicated: AddClass and AddObject first look for an existing entry with FindByClass / FindByObject and return that instead of adding a second one. Registering the same target twice is therefore harmless rather than a source of duplicate tools.

When the reflection pass runs

This is the detail that decides whether your server has any tools at all. TTMSMCPAttributedServer.AddClass and TTMSMCPAttributedServer.AddObject add the target and run the reflection pass immediately. The collection's own Targets.AddClass and Targets.AddObject only add the entry. So build your targets through the server's methods, and treat Targets as the place to inspect what is registered rather than the place to register it.

The pass is a full rebuild, not an append: it clears Tools, Resources, and Prompts before walking every target. Two consequences are worth planning around. Adding a second target regenerates the first target's tools as well, which is harmless. But any tool you registered by hand on the same server is discarded by the next AddObject or AddClass call — so add every target first, then register hand-built tools on top.

AutoRegister and Targets are both published, so a server dropped on a form keeps its target list in the form file and is configurable in the Object Inspector.

Ownership

Ownership is per target and is not implied by registration. AddObject leaves the instance yours: the demo pattern of creating the service, passing it to the factory, and freeing it after the server is correct. To hand an instance over instead, set OwnsObject on the returned target — it is then freed when the target is destroyed, which happens when the server is.

Pick one of the two for each instance. A target with OwnsObject set that you also free yourself is a double free, and an instance that neither owns is a leak in a long-running process that rebuilds its targets.

Giving a target its server

A target is a plain object, so by default it knows nothing about the server that calls it — which is limiting the moment a tool wants to send a log notification, raise a change notification, or look at the current session. Implement ITMSMCPServerAware and the server hands itself to the target while registering it:

{ In the interface section of your service unit: }
type
  TJobService = class(TInterfacedPersistent, ITMSMCPServerAware)
  private
    FServer: TTMSMCPAttributedServer;
  public
    { Called by the server while the target is being registered. }
    procedure SetMCPServer(AServer: TObject);

    [TTMSMCPToolAttribute]
    [TTMSMCPNameAttribute('queue_job')]
    [TTMSMCPDescriptionAttribute('Queues a background job and returns its identifier')]
    function QueueJob(const AKind: string): string;
  end;

implementation

uses
  System.SysUtils;

procedure TJobService.SetMCPServer(AServer: TObject);
begin
  FServer := AServer as TTMSMCPAttributedServer;
end;

function TJobService.QueueJob(const AKind: string): string;
begin
  Result := JobQueue.Enqueue(AKind);

  { With the server in hand the target reaches the rest of the protocol -
    here it sends a log notification the client can display. }
  if Assigned(FServer) then
    FServer.SendLogMessage(llInfo, 'jobs',
      Format('Queued %s job %s', [AKind, Result]));
end;

Derive such a target from a non-reference-counted base such as TInterfacedPersistent. A target descending from TInterfacedObject starts at a reference count of zero, and the interface query the server performs takes and releases a reference — which destroys the object before it has served a single call.

Resource and prompt metadata

The reflection pass reads three families of markers, not one. Alongside [TTMSMCPToolAttribute] it recognises [TTMSMCPResourceAttribute] and [TTMSMCPPromptAttribute], and fills in their metadata from these attributes:

Attribute Applies to Effect
[TTMSMCPResourceAttribute] Method Registers the method as a resource. Requires a URI or a URI template.
[TTMSMCPURIAttribute] Resource method Sets the resource's fixed URI.
[TTMSMCPURITemplateAttribute] Resource method Sets the resource's URITemplate for a parameterised family of URIs.
[TTMSMCPMimeTypeAttribute] Resource method Sets the advertised MimeType.
[TTMSMCPPromptAttribute] Method Registers the method as a prompt; each parameter becomes a prompt argument.
[TTMSMCPCompletionAttribute] Prompt parameter A comma-separated candidate list the server offers as completions for that argument.
[TTMSMCPNameAttribute], [TTMSMCPTitleAttribute], [TTMSMCPDescriptionAttribute], [TTMSMCPIconAttribute] All three Identity, wording, and icons, exactly as for a tool.

A resource needs [TTMSMCPURIAttribute] or [TTMSMCPURITemplateAttribute]; one with neither is skipped. [TTMSMCPOptionalAttribute] works on a prompt parameter too, making that argument optional. Completion candidates are matched case-insensitively on the prefix the user has typed so far, and the server wires its own completion handler only when OnCompletion is unassigned — so a handler of your own always wins. Remember to set EnableCompletions on the server, or the capability is never advertised.

Note that the generated reader and handler resolve their target method through the same lookup the tool dispatcher uses. A method marked only [TTMSMCPResourceAttribute] or only [TTMSMCPPromptAttribute] therefore contributes its metadata but is not invoked. Register resource readers and prompt handlers explicitly through Resources and Prompts on the same server, and use attributes for the tool surface.

Putting a whole server together

The form below combines everything in this guide: a class target for stateless conversions, an owned server-aware instance target for stateful work, a Streamable HTTP transport so clients connect to the process rather than launching it, and a pass over the generated tool list to confirm what was produced:

procedure TForm1.FormCreate(Sender: TObject);
var
  Jobs: TJobService;
  Target: TTMSMCPTarget;
  I: Integer;
begin
  Server := TTMSMCPAttributedServer.Create(Self);
  Server.ServerName := 'Workshop';
  Server.ServerVersion := '2.1.0';

  { Class target: TUnitConverter exposes class methods, so no instance. }
  Server.AddClass(TUnitConverter);

  { Instance target: TJobService implements ITMSMCPServerAware, so the server
    calls SetMCPServer on it during this AddObject call. }
  Jobs := TJobService.Create;
  Target := Server.AddObject(Jobs);
  Target.OwnsObject := True;

  { A transport the client connects to, rather than launches. }
  Transport := TTMSMCPStreamableHTTPTransport.Create(Self, 8934, '/mcp');
  Server.Transport := Transport;

  { Every attributed method across both targets is now a tool. }
  for I := 0 to Server.Tools.Count - 1 do
    Memo1.Lines.Add(Server.Tools[I].Name + ' - ' + Server.Tools[I].Description);

  Server.Start;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  Server.Stop;
  { Server owns Targets, and the owning target frees the TJobService
    instance, so nothing else has to be released here. }
end;

Listing Tools after registration is the quickest sanity check there is. An empty list almost always means a method is not public, or that the targets went into the collection without going through the server.

Common mistakes

  • Registering through Targets instead of the server. Server.Targets.AddObject(…) adds the entry but does not run the reflection pass, so the server ends up with a target and no tools. Use Server.AddObject / Server.AddClass.
  • Registering hand-built tools before adding a target. Each AddObject or AddClass clears Tools, Resources, and Prompts first. Add all targets, then add anything you build by hand.
  • Expecting instance methods from a class target. AddClass without an instance exposes class methods only. The rest are skipped silently.
  • Freeing an instance the target owns. Setting OwnsObject hands the instance to the target. Freeing it yourself as well is a double free; the factory and AddObject do not set it, so by default the instance stays yours.
  • Deriving a server-aware target from TInterfacedObject. Reference counting frees it during the server's interface query. Use TInterfacedPersistent or another non-counted base.
  • Publishing a resource or prompt by attribute alone. The metadata registers, but the handler is not reached. Wire those handlers explicitly.

See also