Table of Contents

Declaring tools with attributes

An attributed tool is a declaration and an implementation in the same place. The method body does the work; the attributes above it carry everything a client and a language model need in order to decide whether to call it — the protocol name, the human-readable title, the description, whether the call changes data, whether it can safely be repeated, what the result looks like, and whether it may run as a background task. The reflection pass reads all of that once, when the class is registered, and produces exactly the same TTMSMCPTool you would have built by hand. This guide covers the marker itself, the identity and wording attributes, the four behaviour hints, presentation, and result declaration.

Marking a method as a tool

[TTMSMCPToolAttribute] is the switch that makes the reflection pass look at a method at all. Without it the method is invisible, whatever other attributes it carries:

unit CatalogService;

interface

uses
  TMS.MCP.Attributes;

type
  TCatalogService = class
  public
    [TTMSMCPToolAttribute]
    [TTMSMCPNameAttribute('lookup_price')]
    [TTMSMCPDescriptionAttribute('Returns the current unit price of one catalog article')]
    [TTMSMCPReadOnlyAttribute]
    function LookupPrice(const ASku: string): Double;

    [TTMSMCPToolAttribute]
    [TTMSMCPNameAttribute('restock')]
    [TTMSMCPDescriptionAttribute('Adds units to the stock level of one catalog article')]
    procedure Restock(const ASku: string; AUnits: Integer);
  end;

implementation

uses
  System.SysUtils;

function TCatalogService.LookupPrice(const ASku: string): Double;
begin
  Result := CatalogDatabase.PriceOf(ASku);
end;

procedure TCatalogService.Restock(const ASku: string; AUnits: Integer);
begin
  if AUnits <= 0 then
    raise Exception.Create('AUnits must be greater than zero');

  CatalogDatabase.AddStock(ASku, AUnits);
end;

end.

Three rules follow from how RTTI works, and each one is a silent no-op rather than a compile error when you get it wrong:

  • The method must be public or published. The default {$RTTI} directive emits method metadata for those visibilities only, so a private or protected method is never seen.
  • A procedure is a valid tool. It is advertised with a Boolean return type, because the protocol needs the call to resolve to something.
  • A class method works without an instance; an ordinary method needs one. A target registered by class alone contributes only its class methods — see Hosting attributed servers.

Naming and describing the tool

[TTMSMCPNameAttribute] and [TTMSMCPDescriptionAttribute] are the two attributes worth applying to every tool, because they are what a model actually reads. With no [TTMSMCPNameAttribute] the generated name is <ClassName>_<MethodName>TCatalogService_LookupPrice — which leaks your class naming into the protocol and changes the moment you rename either. Declare the name explicitly, in the stable lowercase underscore form other MCP servers use, and treat it as part of your public contract from the first release.

The description is not a code comment. A client hands it to the model verbatim, and the model chooses between your tools on that text alone. "Processes the order" tells it far less than "Marks an order as shipped and sends the customer a confirmation e-mail".

Behaviour hints

Four marker attributes tell a client how a call behaves, so it can decide whether to confirm with the user, whether a retry is safe, and how to present the tool. They do not change what your method does:

Attribute Meaning
[TTMSMCPReadOnlyAttribute] The tool does not modify anything. Safe to call freely.
[TTMSMCPDestructiveAttribute] The tool modifies or deletes data. A client may require confirmation.
[TTMSMCPIdempotentAttribute] Calling twice with the same arguments has the same effect as calling once. Safe to retry.
[TTMSMCPOpenworldAttribute] The tool reaches outside the application — the network, another system — so its result is not fully predictable.
{ In the interface section of your service unit, with TMS.MCP.Attributes in uses: }
type
  TMaintenanceService = class
  public
    [TTMSMCPToolAttribute]
    [TTMSMCPNameAttribute('list_backups')]
    [TTMSMCPDescriptionAttribute('Lists every backup archive currently on disk')]
    [TTMSMCPReadOnlyAttribute]
    function ListBackups: TArray<string>;

    [TTMSMCPToolAttribute]
    [TTMSMCPNameAttribute('ensure_backup_folder')]
    [TTMSMCPDescriptionAttribute('Creates the backup folder if it does not exist yet')]
    [TTMSMCPIdempotentAttribute]
    function EnsureBackupFolder(const APath: string): Boolean;

    [TTMSMCPToolAttribute]
    [TTMSMCPNameAttribute('purge_backups')]
    [TTMSMCPDescriptionAttribute('Permanently deletes every backup archive older than the given number of days')]
    [TTMSMCPDestructiveAttribute]
    function PurgeBackups(ADays: Integer): Integer;

    [TTMSMCPToolAttribute]
    [TTMSMCPNameAttribute('check_update')]
    [TTMSMCPDescriptionAttribute('Asks the release server whether a newer version is available')]
    [TTMSMCPReadOnlyAttribute]
    [TTMSMCPOpenworldAttribute]
    function CheckUpdate: string;
  end;

Each attribute sets the corresponding Hint property on the generated tool, and each is present-or-absent: there is no [TTMSMCPReadOnlyAttribute(False)]. Set them honestly. A destructive tool marked read-only is a tool a client will happily call in a loop.

Titles and icons

A protocol name and a label serve different readers, so the API separates them. [TTMSMCPNameAttribute] is the identifier the model calls; [TTMSMCPTitleAttribute] is the wording a client shows a person. Renaming through Title is free, renaming through Name breaks saved conversations and client configuration.

[TTMSMCPIconAttribute] gives the client something to draw next to that title. It takes a source URL, optionally a MIME type, and optionally a size string, and it is the one attribute you may repeat — each occurrence appends another entry to the tool's icon list, so you can offer an SVG and a raster fallback:

{ In the interface section of your service unit, with TMS.MCP.Attributes in uses: }
type
  TInvoiceService = class
  public
    [TTMSMCPToolAttribute]
    [TTMSMCPNameAttribute('send_invoice')]
    [TTMSMCPTitleAttribute('Send invoice')]
    [TTMSMCPDescriptionAttribute('E-mails an invoice PDF to the customer on record')]
    [TTMSMCPIconAttribute('https://cdn.example.com/icons/invoice.svg', 'image/svg+xml')]
    [TTMSMCPIconAttribute('https://cdn.example.com/icons/invoice-32.png', 'image/png', '32x32')]
    function SendInvoice(const AInvoiceNo: string): Boolean;
  end;

Declaring the result

The return type is inferred from the method signature: a function is mapped from its Delphi result type, and a procedure is advertised as Boolean. Apply one of the four type attributes — [TTMSMCPStringAttribute], [TTMSMCPIntegerAttribute], [TTMSMCPFloatAttribute], [TTMSMCPBooleanAttribute] — directly to the method when you want to state the advertised type instead of letting inference pick it. The full mapping table is in Parameters and schema.

When the result is structured rather than a single scalar, describe it. [TTMSMCPOutputSchemaAttribute] takes a JSON Schema string that lands on the tool's OutputSchema, which lets a client validate the result and lets the model reason about individual fields instead of parsing prose. [TTMSMCPTaskSupportAttribute] opts the tool into long-running task execution, taking a TTMSMCPAttrTaskSupport value — atsForbidden, atsOptional, or atsRequired, defaulting to atsOptional when you write the attribute with no argument:

{ In the interface section of your service unit, with TMS.MCP.Attributes in uses: }
type
  TReportService = class
  public
    [TTMSMCPToolAttribute]
    [TTMSMCPNameAttribute('build_sales_report')]
    [TTMSMCPTitleAttribute('Build sales report')]
    [TTMSMCPDescriptionAttribute('Aggregates sales for one region and returns the totals as JSON')]
    [TTMSMCPStringAttribute]
    [TTMSMCPOutputSchemaAttribute(
      '{"type":"object","properties":{' +
      '"region":{"type":"string"},' +
      '"orders":{"type":"integer"},' +
      '"revenue":{"type":"number"}},' +
      '"required":["region","orders","revenue"]}')]
    [TTMSMCPTaskSupportAttribute(atsOptional)]
    function BuildSalesReport(const ARegion: string): string;
  end;

atsOptional lets the client choose between waiting for the result and polling a task; atsRequired forces the task route, which is the right setting for work that will outlive any reasonable request timeout.

Combining the whole vocabulary

A tool that matters usually needs most of this at once. The method below names itself for the protocol, labels itself for a person, warns that it destroys data while promising that a repeat is harmless, admits that it reaches another database, ships an icon, describes its JSON result, and insists on running as a task because the work takes minutes:

{ In the interface section of your service unit, with TMS.MCP.Attributes in uses: }
type
  TArchiveService = class
  public
    [TTMSMCPToolAttribute]
    [TTMSMCPNameAttribute('archive_period')]
    [TTMSMCPTitleAttribute('Archive accounting period')]
    [TTMSMCPDescriptionAttribute(
      'Moves every posted document of one accounting period into the archive ' +
      'database and removes it from the live tables. Returns a JSON summary ' +
      'of what was archived.')]
    [TTMSMCPDestructiveAttribute]
    [TTMSMCPIdempotentAttribute]
    [TTMSMCPOpenworldAttribute]
    [TTMSMCPIconAttribute('https://cdn.example.com/icons/archive.svg', 'image/svg+xml')]
    [TTMSMCPStringAttribute]
    [TTMSMCPOutputSchemaAttribute(
      '{"type":"object","properties":{' +
      '"period":{"type":"string"},' +
      '"documents":{"type":"integer"},' +
      '"seconds":{"type":"number"}}}')]
    [TTMSMCPTaskSupportAttribute(atsRequired)]
    function ArchivePeriod(const APeriod: string): string;
  end;

implementation

uses
  System.DateUtils, System.JSON, System.SysUtils;

function TArchiveService.ArchivePeriod(const APeriod: string): string;
var
  Started: TDateTime;
  Documents: Integer;
  Summary: TJSONObject;
begin
  Started := Now;
  Documents := ArchiveEngine.MovePeriod(APeriod);

  Summary := TJSONObject.Create;
  try
    Summary.AddPair('period', APeriod);
    Summary.AddPair('documents', TJSONNumber.Create(Documents));
    Summary.AddPair('seconds', TJSONNumber.Create(SecondSpan(Started, Now)));
    Result := Summary.ToJSON;
  finally
    Summary.Free;
  end;
end;

Read the attribute block top to bottom and it is a complete specification of the tool — which is the point of declaring it here rather than in a registration routine three units away.

Common mistakes

  • Forgetting [TTMSMCPToolAttribute]. Every other attribute is metadata about a tool. A method with [TTMSMCPNameAttribute] and [TTMSMCPDescriptionAttribute] but no [TTMSMCPToolAttribute] generates nothing, silently.
  • Decorating a private or protected method. RTTI does not emit method metadata for those visibilities under the default settings, so the reflection pass never sees the method. Move it to public.
  • Letting the name default. TCatalogService_LookupPrice ties your protocol surface to your class and method names, so an ordinary refactoring becomes a breaking change for every connected client.
  • Writing the description for yourself. It is the model's only guidance on when to use the tool. Write it for a reader who cannot see the code.
  • Expecting a hint to take a parameter. The four hint attributes are markers. To say a tool is not destructive, leave [TTMSMCPDestructiveAttribute] off.
  • Relying on the return type attribute to convert anything. It changes only what the tool advertises. The method still returns what its Delphi signature says it returns.

See also