Defining tools
A tool is the unit of action an MCP server offers. A client lists the available tools, shows them to a language model together with their descriptions and input schemas, and the model decides which one to call and with what arguments. That makes a tool declaration a piece of documentation as much as a piece of code: the name, the description, and every property description are read by the model, not just by you. This guide covers the two ways to declare a tool, the input property types available, how to constrain an argument to a fixed set of values, how to accept a list of records, and what the four behaviour hints tell a client about calling your tool safely.
Registering a tool
TTMSMCPTools.RegisterTool is the compact form. It creates the tool, sets the
name, description, and return type, and attaches the method in one call:
procedure TForm1.RegisterGreetTool;
var
Prop: TTMSMCPToolProperty;
begin
{ Server is a TTMSMCPServer dropped on the form. }
Server.Tools.RegisterTool('greet_user', 'Generates a greeting for a person',
function(const Args: array of TValue): TValue
begin
Result := Format('Hello, %s!', [Args[0].AsString]);
end,
ptString);
{ RegisterTool creates the tool with no declared input properties, so
declare them separately - the client needs them to build the call. }
Prop := Server.Tools.FindByName('greet_user').Properties.Add;
Prop.Name := 'name';
Prop.Description := 'Name of the person to greet';
Prop.PropertyType := ptString;
Prop.Required := True;
end;
Note what RegisterTool does not do: it declares no input properties. The
method receives Args positionally, so the client has to know what to send —
and it only knows from the properties you declare. Add them through
Properties.Add as shown above, or use the builder, which keeps the arguments
next to the method that consumes them.
Using the fluent builder
TTMSMCPTool.CreateBuilder returns a builder class whose every method returns
the builder again, so a complete declaration reads as one expression:
procedure TForm1.RegisterSumTool;
var
Tool: TTMSMCPTool;
begin
Tool := TTMSMCPTool.CreateBuilder
.Name('calculate_sum')
.Title('Sum of two numbers')
.Description('Calculates the sum of two numbers')
.ReturnType(ptFloat)
.ReadOnlyHint(True)
.IdempotentHint(True)
.ExecuteCallback(
function(const Args: array of TValue): TValue
begin
Result := Args[0].AsExtended + Args[1].AsExtended;
end)
.AddProperty
.Name('first')
.Description('First number')
.PropertyType(ptFloat)
.Required(True)
.&End
.AddProperty
.Name('second')
.Description('Second number')
.PropertyType(ptFloat)
.Required(True)
.&End
.Build;
{ Add reparents the tool into the collection, which then owns it.
Do not free Tool afterwards. }
Server.Tools.Add(Tool);
end;
Two details of the syntax are worth stating plainly. AddProperty switches from
the tool builder to the property builder, and .&End switches back — the
ampersand is required because End is a reserved word. And Build returns the
finished TTMSMCPTool; passing it to Server.Tools.Add transfers ownership.
Input property types
TTMSMCPToolPropertyType (declared in TMS.MCP.Helpers) is the full set of
argument types:
| Value | JSON type | Use for |
|---|---|---|
ptString |
string |
Text, identifiers, dates you parse yourself. |
ptInteger |
integer |
Whole numbers, counts, indexes. |
ptFloat |
number |
Amounts, measurements, rates. |
ptBoolean |
boolean |
Flags. |
ptDateTime |
string |
A date/time the framework converts for you. |
ptJSON |
any | A raw JSON value you interpret in the method. |
ptObject |
object |
A structured value described by ObjectClass. |
ptArrayOfStrings |
array |
A list of strings. |
ptArrayOfIntegers |
array |
A list of whole numbers. |
ptArrayOfFloats |
array |
A list of numbers. |
ptArrayOfBooleans |
array |
A list of flags. |
ptArrayOfObjects |
array |
A list of records — see below. |
The float values are ptFloat and ptArrayOfFloats. There is no ptDouble.
The property builder also offers AsArrayOfStrings, AsArrayOfIntegers,
AsArrayOfFloats, AsArrayOfBooleans, and AsArrayOfObjects as readable
alternatives to PropertyType(ptArrayOf…).
Constraining an argument with enums
When an argument only accepts certain values, say so. AddEnum adds one value
and AddEnums adds several; they land in the tool's input schema, so the model
sees the permitted set instead of guessing at free text:
procedure TForm1.RegisterSetStatusTool;
var
Tool: TTMSMCPTool;
begin
Tool := TTMSMCPTool.CreateBuilder
.Name('set_status')
.Description('Sets the status of a work item')
.ReturnType(ptString)
.ExecuteCallback(
function(const Args: array of TValue): TValue
begin
Result := ApplyStatus(Args[0].AsString, Args[1].AsString);
end)
.AddProperty
.Name('item_id')
.Description('Work item identifier')
.PropertyType(ptString)
.Required(True)
.&End
.AddProperty
.Name('status')
.Description('New status')
.PropertyType(ptString)
.AddEnums(['open', 'in_progress', 'blocked', 'done'])
.Required(True)
.&End
.Build;
Server.Tools.Add(Tool);
end;
An enum property is still typed — the values constrain a ptString here — so
the method reads the argument the same way it would read any other string.
Accepting a list of records
ptArrayOfObjects describes a repeating structure. Each field of the structure
is declared with AddArrayItemProperty(name, type, description, required),
which maps to TTMSMCPArrayItemProperty entries on the property:
procedure TForm1.RegisterProcessOrderTool;
var
Tool: TTMSMCPTool;
begin
Tool := TTMSMCPTool.CreateBuilder
.Name('process_order')
.Title('Order processor')
.Description('Processes an order and its line items')
.ReturnType(ptBoolean)
.DestructiveHint(True)
.ExecuteCallback(
function(const Args: array of TValue): TValue
begin
{ Args[1] carries the line items as JSON. }
Result := ProcessOrder(Args[0].AsString, Args[1]);
end)
.AddProperty
.Name('order_id')
.Description('Order identifier')
.PropertyType(ptString)
.Required(True)
.&End
.AddProperty
.Name('items')
.Description('Order line items')
.AsArrayOfObjects
.AddArrayItemProperty('product_id', ptString, 'Product identifier', True)
.AddArrayItemProperty('quantity', ptInteger, 'Quantity ordered', True)
.AddArrayItemProperty('unit_price', ptFloat, 'Price per unit')
.&End
.Build;
Server.Tools.Add(Tool);
end;
The argument arrives in the method as JSON, so read it with the
System.JSON types rather than expecting a Delphi array. For a structure you
already have as a Delphi class, set ObjectClass on the property and let
TTMSMCPJSONSchemaGenerator
derive the schema from RTTI.
Behaviour hints
Four published Boolean properties tell a client how a call behaves. They do not change what your method does — they let a client decide whether to ask the user for confirmation, whether a retry is safe, and how to present the tool:
| Hint | Meaning when True |
|---|---|
ReadOnlyHint |
The tool does not modify anything. Safe to call freely. |
DestructiveHint |
The tool modifies or deletes data. A client may require confirmation. |
IdempotentHint |
Calling twice with the same arguments has the same effect as calling once. Safe to retry. |
OpenWorldHint |
The tool reaches outside the application — the network, another system — so its result is not fully predictable. |
Set them honestly. A destructive tool marked read-only is a tool a client will happily call in a loop.
Title is the human-readable label a client shows; Name is the protocol
identifier the model calls. Keep Name in the stable, lowercase, underscore
form other MCP servers use, and put the readable wording in Title.
Combining property types and hints
Most real tools use several of these at once. This one constrains two arguments to known value sets, takes a list of records as its payload, and declares all four hints so a client knows the call changes data and must not be retried blindly:
procedure TForm1.RegisterBulkPriceUpdateTool;
var
Tool: TTMSMCPTool;
begin
Tool := TTMSMCPTool.CreateBuilder
.Name('bulk_price_update')
.Title('Bulk price update')
.Description('Applies a price change to a list of products in one region')
.ReturnType(ptInteger)
.DestructiveHint(True)
.IdempotentHint(False)
.OpenWorldHint(False)
.DynamicExecuteCallback(
function(const Args: TDictionary<string, TValue>): TValue
begin
Result := ApplyPriceChanges(
Args['region'].AsString,
Args['rounding'].AsString,
Args['changes']);
end)
.AddProperty
.Name('region')
.Description('Sales region the change applies to')
.PropertyType(ptString)
.AddEnums(['emea', 'amer', 'apac'])
.Required(True)
.&End
.AddProperty
.Name('rounding')
.Description('How to round the resulting price')
.PropertyType(ptString)
.AddEnums(['none', 'nearest_cent', 'nearest_unit'])
.Required(False)
.&End
.AddProperty
.Name('changes')
.Description('Products to reprice and their new prices')
.AsArrayOfObjects
.AddArrayItemProperty('product_id', ptString, 'Product identifier', True)
.AddArrayItemProperty('new_price', ptFloat, 'New unit price', True)
.AddArrayItemProperty('effective_from', ptDateTime,
'Date the new price takes effect')
.&End
.Build;
Server.Tools.Add(Tool);
end;
Because the argument list is wide, it reads them by name through
DynamicExecuteCallback rather than positionally — adding a property later
cannot then shift an existing argument out from under the method.
Common mistakes
- Freeing a tool after adding it.
TTMSMCPTools.Addsets the tool'sCollection, so the collection owns it from that moment. Atry … finally Tool.Freearound theAddis a double free. Free only a tool you never added. ptDoubledoes not exist. The floating-point values areptFloatandptArrayOfFloats.- Registering without declaring properties.
RegisterToolalone gives the client no input schema, so the model has nothing to fill in. Declare every argument you read fromArgs. - Describing the tool for yourself.
Descriptionis the model's only guidance on when to use the tool. "Processes the order" tells it far less than "Marks an order as shipped and sends the customer a confirmation e-mail". - A name that changes. Clients and saved conversations refer to a tool by
Name. Rename throughTitle, notName, once a server has shipped.
See also
- Tool results — what a tool sends back
- Attributes — declare tools with RTTI attributes instead
- MCP Server — hosting and change notifications
TTMSMCPTool,TTMSMCPToolProperty