Table of Contents

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.Add sets the tool's Collection, so the collection owns it from that moment. A try … finally Tool.Free around the Add is a double free. Free only a tool you never added.
  • ptDouble does not exist. The floating-point values are ptFloat and ptArrayOfFloats.
  • Registering without declaring properties. RegisterTool alone gives the client no input schema, so the model has nothing to fill in. Declare every argument you read from Args.
  • Describing the tool for yourself. Description is 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 through Title, not Name, once a server has shipped.

See also