Table of Contents

Parameters and schema

Every parameter of an attributed method becomes one input property on the generated tool, and together those properties are the input schema the client sends to the model. That schema is the only thing standing between a model's guess and a correct call, so it is worth getting right: the right names, a description per argument, the right JSON type, and a clear statement of which arguments are genuinely required. This guide covers how parameters are mapped by default, how to rename and describe them, the Delphi-to-schema type table and when to override it, how optional parameters and their defaults work, and what a client sees when it sends something the method cannot accept.

What a parameter becomes by default

Start from the default because it is what you get with no attributes at all, and it is usually almost right. For each parameter the reflection pass creates a TTMSMCPRttiToolProperty whose:

  • name is the Delphi parameter name, verbatim — ASku, AMaxResults;
  • type is inferred from the declared Delphi type;
  • required flag is True;
  • description is empty.

The property also keeps the original parameter name internally, so renaming a property for the protocol never breaks the binding back to the method. The two things the default cannot give you are a readable name and a description — and those are exactly the two a model needs.

Renaming and describing parameters

Apply [TTMSMCPNameAttribute] and [TTMSMCPDescriptionAttribute] to the parameter itself, inside the parameter list, when the Delphi name is an implementation detail. Delphi's A-prefixed convention is good source style and poor protocol style: a model reading ASku and AUser has to guess, while ticket_id and assignee with a sentence each leave nothing to guess:

{ In the interface section of your service unit, with TMS.MCP.Attributes in uses: }
type
  TTicketService = class
  public
    [TTMSMCPToolAttribute]
    [TTMSMCPNameAttribute('assign_ticket')]
    [TTMSMCPDescriptionAttribute('Assigns a support ticket to a team member')]
    function AssignTicket(
      [TTMSMCPNameAttribute('ticket_id')]
      [TTMSMCPDescriptionAttribute('Identifier of the ticket, for example SUP-4821')]
      const AId: string;

      [TTMSMCPNameAttribute('assignee')]
      [TTMSMCPDescriptionAttribute('E-mail address of the team member to assign it to')]
      const AUser: string): Boolean;
  end;

Describe every argument whose meaning is not obvious from its name, and mention the format when there is one — an identifier pattern, a unit of measure, an accepted range. That sentence costs one line and removes a whole class of failed calls.

The type mapping

Type inference covers the common cases without any attribute, so reach for a type attribute only when you disagree with the result. This is the complete mapping from a Delphi parameter or result type to the advertised property type:

Delphi type Generated property type
Integer, Int64 and other integer types ptInteger
Double, Single, Currency and other floats ptFloat
TDateTime ptDateTime
string and other string types ptString
Boolean ptBoolean
Any other enumeration ptString
A TJSONValue descendant ptJSON
Any other class, or a record ptObject
TArray<Integer> ptArrayOfIntegers
TArray<Double> ptArrayOfFloats
TArray<Boolean> ptArrayOfBooleans
TArray<string> ptArrayOfStrings
An array of classes or records ptArrayOfObjects

The floating-point values are ptFloat and ptArrayOfFloats. There is no ptDouble. A parameter whose type kind is not in this table — a pointer, a method reference, a variant — raises an exception during registration rather than producing a half-formed schema, so an unsupported signature fails loudly at startup instead of at call time.

Overriding the advertised type

Override the inferred type when the Delphi declaration is shaped by the implementation rather than by the contract. [TTMSMCPStringAttribute], [TTMSMCPIntegerAttribute], [TTMSMCPFloatAttribute], and [TTMSMCPBooleanAttribute] each set the property type directly, on a parameter or on the method's return value:

{ In the interface section of your service unit, with TMS.MCP.Attributes in uses: }
type
  TMeterService = class
  public
    { Stating the types explicitly keeps the generated schema readable even
      where inference would already reach the same answer. }
    [TTMSMCPToolAttribute]
    [TTMSMCPNameAttribute('quality_score')]
    [TTMSMCPDescriptionAttribute('Returns the quality score of one device, between 0 and 1')]
    [TTMSMCPReadOnlyAttribute]
    [TTMSMCPFloatAttribute]
    function QualityScore([TTMSMCPIntegerAttribute] ADeviceId: Integer): Double;

    { The window is always a whole number of days, but the method takes a
      Double so it can do fractional arithmetic internally. Narrowing the
      advertised type to integer is safe: a JSON integer converts to Double
      without loss, and the model stops offering 3.5 as a window length. }
    [TTMSMCPToolAttribute]
    [TTMSMCPNameAttribute('average_consumption')]
    [TTMSMCPDescriptionAttribute('Returns the average consumption in kWh over a window of whole days')]
    [TTMSMCPReadOnlyAttribute]
    [TTMSMCPFloatAttribute]
    function AverageConsumption(
      [TTMSMCPDescriptionAttribute('Device serial number')]
      const ADevice: string;

      [TTMSMCPIntegerAttribute]
      [TTMSMCPDescriptionAttribute('Length of the window in days')]
      AWindowDays: Double): Double;
  end;

One rule governs every override: the attribute changes only what the schema advertises, never how the incoming value is converted into the Delphi parameter. So choose a JSON type that still converts cleanly — narrowing a Double parameter to ptInteger is safe, while advertising a string parameter as ptObject is not. Declaring the type you already have is also legitimate, and common in practice: it makes the schema obvious to the next reader of the source.

Optional parameters

Mark a parameter [TTMSMCPOptionalAttribute] when the tool is usable without it. The property drops out of the schema's required set, so the model may leave it out, and the attribute value becomes the argument the method receives in that case:

{ In the interface section of your service unit, with TMS.MCP.Attributes in uses: }
type
  TSearchService = class
  public
    [TTMSMCPToolAttribute]
    [TTMSMCPNameAttribute('search_documents')]
    [TTMSMCPDescriptionAttribute('Searches the document archive and returns matching titles')]
    [TTMSMCPReadOnlyAttribute]
    function SearchDocuments(
      [TTMSMCPDescriptionAttribute('Free-text query')]
      const AQuery: string;

      [TTMSMCPOptionalAttribute(25)]
      [TTMSMCPDescriptionAttribute('Maximum number of results to return')]
      AMaxResults: Integer;

      [TTMSMCPOptionalAttribute(False)]
      [TTMSMCPDescriptionAttribute('Set to true to search archived documents as well')]
      AIncludeArchived: Boolean): TArray<string>;
  end;

implementation

function TSearchService.SearchDocuments(const AQuery: string;
  AMaxResults: Integer; AIncludeArchived: Boolean): TArray<string>;
begin
  { The attribute value arrives as the argument when the client omits it,
    so no extra defaulting is needed here. }
  Result := DocumentIndex.Search(AQuery, AMaxResults, AIncludeArchived);
end;

TTMSMCPOptionalAttribute has constructors for a string, an Integer, a Double, and a Boolean, plus a no-argument form. Prefer the form that carries a value. The no-argument form marks the parameter optional but has nothing to pass when the client omits it, so the method receives an empty value — which is almost never what the signature expects.

Note also that a Delphi default parameter value (AMaxResults: Integer = 25) is invisible to the reflection pass. RTTI does not expose it, so the default that reaches the tool is the one in the attribute, and the two can silently disagree. Put the value in the attribute and keep any Delphi default identical to it, or leave the Delphi default off entirely.

Values arriving from the client

Clients are not consistent about JSON types — a model asked for a number will sometimes produce "42". Rather than failing such a call, the attributed tool coerces a JSON string into the type the Delphi parameter actually needs: to an integer for Integer and Int64 parameters, to a number for floats, and to a Boolean for Boolean parameters, where "true", "false", "1", and "0" are all accepted.

When coercion cannot succeed, the call fails with an invalid-params JSON-RPC error naming the parameter and showing what arrived — Parameter "max_age_days" expects an integer, got "soon". A missing required argument produces a comparable error that lists both the keys received and the parameter names expected, which is usually enough for a model to correct itself and retry without a round trip through your logs.

Combining renamed, retyped, and optional parameters

A realistic tool uses all of this at once. The export below renames every parameter for the protocol, describes each one, narrows a Double to an integer because a fractional number of days is meaningless, makes three of the four optional with sensible defaults, and validates what it is given:

{ In the interface section of your service unit, with TMS.MCP.Attributes in uses: }
type
  TExportService = class
  public
    [TTMSMCPToolAttribute]
    [TTMSMCPNameAttribute('export_orders')]
    [TTMSMCPTitleAttribute('Export orders')]
    [TTMSMCPDescriptionAttribute(
      'Exports the orders of one customer to a delimited text file and ' +
      'returns the full path of the file that was written.')]
    [TTMSMCPStringAttribute]
    function ExportOrders(
      [TTMSMCPNameAttribute('customer_id')]
      [TTMSMCPDescriptionAttribute('Identifier of the customer whose orders to export')]
      const ACustomer: string;

      [TTMSMCPNameAttribute('max_age_days')]
      [TTMSMCPIntegerAttribute]
      [TTMSMCPOptionalAttribute(90)]
      [TTMSMCPDescriptionAttribute('Only include orders placed within this many days')]
      AMaxAge: Double;

      [TTMSMCPNameAttribute('format')]
      [TTMSMCPOptionalAttribute('csv')]
      [TTMSMCPDescriptionAttribute('Output format, either csv or tsv')]
      const AFormat: string;

      [TTMSMCPNameAttribute('include_header')]
      [TTMSMCPOptionalAttribute(True)]
      [TTMSMCPDescriptionAttribute('Write a column header as the first line')]
      AIncludeHeader: Boolean): string;
  end;

implementation

uses
  System.SysUtils,
  TMS.MCP.Helpers;

function TExportService.ExportOrders(const ACustomer: string; AMaxAge: Double;
  const AFormat: string; AIncludeHeader: Boolean): string;
begin
  { Optional parameters always arrive, carrying the attribute value when the
    client left them out - so validate them like any other argument. }
  if not SameText(AFormat, 'csv') and not SameText(AFormat, 'tsv') then
    RaiseJsonRpcError(TTMSMCPErrorCode.ecInvalidParams,
      'format must be either "csv" or "tsv"');

  if AMaxAge <= 0 then
    RaiseJsonRpcError(TTMSMCPErrorCode.ecInvalidParams,
      'max_age_days must be greater than zero');

  Result := OrderExporter.Run(ACustomer, AMaxAge, AFormat, AIncludeHeader);
end;

Validating inside the method is still your job. The schema tells a model what shape to send; it does not stop a client from sending "format": "pdf". Report those with RaiseJsonRpcError and an ecInvalidParams code so the client sees a protocol-level error rather than an unhandled exception.

Common mistakes

  • A bare [TTMSMCPOptionalAttribute]. With no value the attribute makes the parameter optional but supplies nothing, so an omitted argument reaches the method as an empty value. Always pass the default you want.
  • Trusting a Delphi default parameter value. = 25 in the signature is not visible to RTTI. The attribute value is the only default the tool knows about.
  • Renaming a parameter in the schema but reading it positionally. The generated tool binds each property back to its original parameter, so [TTMSMCPNameAttribute] is safe — but the method must keep its parameters in the order it declares them. Reorder them and every existing client call shifts.
  • Expecting ptDouble. The floating-point property types are ptFloat and ptArrayOfFloats.
  • Overriding a type across an unconvertible gap. A type attribute changes the advertised schema, not the conversion. Advertising a class parameter as a string produces calls the tool cannot bind.
  • Leaving descriptions off. An argument named AValue with no description is a coin flip for the model. It costs one attribute to remove the ambiguity.

See also