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.
= 25in 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 areptFloatandptArrayOfFloats. - 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
AValuewith no description is a coin flip for the model. It costs one attribute to remove the ambiguity.
See also
- Declaring tools with attributes — the method-level attributes
- Hosting attributed servers — registering the class
- Defining tools — declaring the same properties by hand
TTMSMCPOptionalAttribute,TTMSMCPRttiToolProperty,TTMSMCPToolPropertyType