Table of Contents

Function calling

A language model cannot read your database, price an order, or send an email — but it can ask your application to. Function calling is the mechanism: you declare a set of callable functions with typed arguments, the component advertises them with every request, and when the model decides one is needed it sends back a call instead of an answer. TTMSMCPCloudAI runs that call through your OnExecute handler, feeds the result back to the model, and repeats until the model produces a final answer — all inside one Execute, with the answer arriving in OnExecuted as usual. This guide covers declaring tools and their parameters, constraining arguments with enums and formats, structured arguments (nested objects and arrays of objects), packaging tools into reusable tool sets, and running several calls in parallel.

Declaring a tool

Start with the simplest useful shape — a name, a description, one typed argument, and a handler. The description is not decoration: it is the only thing the model has to decide whether the tool applies, so write it for a reader who cannot see your code.

procedure TForm1.RegisterStockTool;
var
  Tool: TTMSMCPCloudAITool;
  Param: TTMSMCPCloudAIParameter;
begin
  Tool := AI.Tools.Add;
  Tool.Name := 'get_stock_level';
  Tool.Description := 'Returns the number of units currently in stock for an article';
  Tool.Enabled := True;

  Param := Tool.Parameters.Add;
  Param.Name := 'article_code';
  Param.Description := 'The article code, for example A-1043';
  Param.&Type := ptString;
  Param.Required := True;

  Tool.OnExecute := GetStockLevel;
end;

procedure TForm1.GetStockLevel(Sender: TObject; Args: TJSONObject;
  var Result: string);
var
  ArticleCode: string;
begin
  { Args carries the arguments the model chose, keyed by parameter name. }
  ArticleCode := Args.GetValue<string>('article_code');

  { Whatever is assigned to Result is sent back to the model as the tool
    result, and the model continues from there. Plain text is fine; JSON is
    easier for the model to use precisely. }
  Result := Format('{"article":"%s","units":%d}',
    [ArticleCode, LookupStock(ArticleCode)]);
end;

TTMSMCPCloudAITool items live in the component's Tools collection:

Property Meaning
Name The identifier the model calls. Keep it lowercase with underscores.
Description What the tool does, in prose, for the model.
Parameters The declared arguments — see below.
Enabled When False the tool is not advertised. Default True.
&Type TTMSMCPCloudAIToolType; currently ttFunction.
Tag A NativeInt of your own, untouched by the component.
OnExecute The handler that runs the call.

Tools.ActiveCount reports how many are currently enabled, which is what actually gets advertised.

The handler is a TTMSMCPCloudAIToolExecuteEvent: Args is a TJSONObject keyed by parameter name, and whatever you assign to the var Result: string parameter is returned to the model as the tool result. Plain text works; a small JSON object is easier for the model to use precisely.

Constraining arguments: enums and formats

A free-text argument invites the model to invent a value. Constraining it is usually cheaper than validating it afterwards.

procedure TForm1.RegisterReportTool;
var
  Tool: TTMSMCPCloudAITool;
  Param: TTMSMCPCloudAIParameter;
begin
  Tool := AI.Tools.Add;
  Tool.Name := 'email_sales_report';
  Tool.Description := 'Emails the sales report for one region to a recipient';

  { A fixed choice. The model may only pick one of these values. }
  Param := Tool.Parameters.Add;
  Param.Name := 'region';
  Param.Description := 'Sales region the report covers';
  Param.&Type := ptEnum;
  Param.Required := True;
  Param.Enum.Add('emea');
  Param.Enum.Add('apac');
  Param.Enum.Add('americas');

  { Format sharpens a plain string - the model is told this is an address,
    not free text. }
  Param := Tool.Parameters.Add;
  Param.Name := 'recipient';
  Param.Description := 'Email address that receives the report';
  Param.&Type := ptString;
  Param.Format := fmtEmail;
  Param.Required := True;

  Param := Tool.Parameters.Add;
  Param.Name := 'period_end';
  Param.Description := 'Last day covered by the report';
  Param.&Type := ptString;
  Param.Format := fmtDate;
  Param.Required := False;

  Tool.OnExecute := EmailSalesReport;
end;

procedure TForm1.EmailSalesReport(Sender: TObject; Args: TJSONObject;
  var Result: string);
var
  Region, Recipient: string;
begin
  Region := Args.GetValue<string>('region');
  Recipient := Args.GetValue<string>('recipient');

  SendReport(Region, Recipient);
  Result := Format('Report for %s sent to %s.', [Region, Recipient]);
end;

TTMSMCPCloudAIParameter describes one argument. Its &Type takes a TTMSMCPCloudAIParameterType value:

Value Argument is
ptString Text.
ptNumber A number — integer or floating point.
ptBoolean true or false.
ptEnum One of the values listed in Enum.
ptObject A nested object described by Properties. This is the default.
ptArray A list whose element type is ArrayType.

Format narrows a string further with a TTMSMCPCloudAIFormat value — fmtEmail, fmtURI, fmtDate, fmtDateTime, fmtTime, fmtHostName, fmtipv4, fmtipv6, fmtuuid, fmtregex, fmtPhone, or fmtNone.

Required defaults to True. Mark genuinely optional arguments False, otherwise the model will invent a value rather than omit it.

Structured arguments: objects and arrays

Some operations do not decompose into flat scalars. An order has a customer and a repeating set of lines, and flattening that into article_1, quantity_1, article_2 … is worse for the model than declaring the real shape.

procedure TForm1.RegisterOrderTool;
var
  Tool: TTMSMCPCloudAITool;
  Customer, Lines, Field: TTMSMCPCloudAIParameter;
begin
  Tool := AI.Tools.Add;
  Tool.Name := 'create_order';
  Tool.Description := 'Creates a sales order for a customer';

  { A nested object: Type is ptObject and its shape lives in Properties. }
  Customer := Tool.Parameters.Add;
  Customer.Name := 'customer';
  Customer.Description := 'Customer the order belongs to';
  Customer.&Type := ptObject;
  Customer.Required := True;

  Field := Customer.Properties.Add;
  Field.Name := 'id';
  Field.Description := 'Customer identifier';
  Field.&Type := ptString;
  Field.Required := True;

  Field := Customer.Properties.Add;
  Field.Name := 'purchase_order';
  Field.Description := 'Customer purchase order reference';
  Field.&Type := ptString;
  Field.Required := False;

  { An array of objects: Type is ptArray, ArrayType says what an element is,
    and ArrayProperties describes the element. }
  Lines := Tool.Parameters.Add;
  Lines.Name := 'lines';
  Lines.Description := 'Order lines';
  Lines.&Type := ptArray;
  Lines.ArrayType := ptObject;
  Lines.Required := True;

  Field := Lines.ArrayProperties.Add;
  Field.Name := 'article_code';
  Field.Description := 'Article being ordered';
  Field.&Type := ptString;
  Field.Required := True;

  Field := Lines.ArrayProperties.Add;
  Field.Name := 'quantity';
  Field.Description := 'Number of units';
  Field.&Type := ptNumber;
  Field.Required := True;

  Tool.OnExecute := CreateOrder;
end;

procedure TForm1.CreateOrder(Sender: TObject; Args: TJSONObject;
  var Result: string);
var
  Customer: TJSONObject;
  Lines: TJSONArray;
begin
  Customer := Args.GetValue<TJSONObject>('customer');
  Lines := Args.GetValue<TJSONArray>('lines');

  { Args owns both - read them, do not free them. }
  Result := Format('{"order":"%s","lines":%d}',
    [StoreOrder(Customer.GetValue<string>('id'), Lines), Lines.Count]);
end;

Two collections do the work, and mixing them up is the usual mistake:

  • Properties describes the fields of a ptObject parameter.
  • ArrayProperties describes the fields of one element of a ptArray parameter whose ArrayType is ptObject.

For an array of scalars, set ArrayType to ptString or ptNumber and leave ArrayProperties empty. Both collections are TTMSMCPCloudAIParameters, so nesting can go deeper where a service supports it.

In the handler, read a nested object with Args.GetValue<TJSONObject>(...) and a list with Args.GetValue<TJSONArray>(...). Both belong to Args — read them, do not free them.

Reusable tool sets

Tools that belong together — inventory, reporting, a CRM — are worth packaging so they can be reused across forms and applications instead of being re-declared in each one.

type
  TInventoryToolSet = class(TTMSMCPCloudAIToolSet)
  protected
    procedure DefineTools; override;
    procedure GetStockLevel(Sender: TObject; Args: TJSONObject; var Result: string);
    procedure ReserveStock(Sender: TObject; Args: TJSONObject; var Result: string);
  end;

procedure TInventoryToolSet.DefineTools;
var
  Tool: TTMSMCPCloudAITool;
  Param: TTMSMCPCloudAIParameter;
begin
  inherited;

  { BeginUpdate / EndUpdate wrap the declarations. EndUpdate records each
    tool's handler so it can be reattached before every request - declare
    the tools and their OnExecute handlers between the two calls. }
  BeginUpdate;

  Tool := Tools.Add;
  Tool.Name := 'get_stock_level';
  Tool.Description := 'Returns the number of units in stock for an article';
  Param := Tool.Parameters.Add;
  Param.Name := 'article_code';
  Param.Description := 'The article code to look up';
  Param.&Type := ptString;
  Param.Required := True;
  Tool.OnExecute := GetStockLevel;

  Tool := Tools.Add;
  Tool.Name := 'reserve_stock';
  Tool.Description := 'Reserves units of an article for an order';
  Param := Tool.Parameters.Add;
  Param.Name := 'article_code';
  Param.Description := 'The article code to reserve';
  Param.&Type := ptString;
  Param.Required := True;
  Param := Tool.Parameters.Add;
  Param.Name := 'quantity';
  Param.Description := 'Number of units to reserve';
  Param.&Type := ptNumber;
  Param.Required := True;
  Tool.OnExecute := ReserveStock;

  EndUpdate;
end;

procedure TInventoryToolSet.GetStockLevel(Sender: TObject; Args: TJSONObject;
  var Result: string);
begin
  Result := IntToStr(LookupStock(Args.GetValue<string>('article_code')));
end;

procedure TInventoryToolSet.ReserveStock(Sender: TObject; Args: TJSONObject;
  var Result: string);
begin
  Result := ReserveUnits(Args.GetValue<string>('article_code'),
    Args.GetValue<Integer>('quantity'));
end;

procedure TForm1.AttachInventoryTools;
begin
  { Setting AI registers the set. Its tools are merged with the component's
    own Tools collection on every Execute call. }
  FInventory := TInventoryToolSet.Create(Self);
  FInventory.AI := AI;
end;

TTMSMCPCloudAIToolSet is a component with its own Tools collection. Override the protected DefineTools method and declare the tools there; it is called from the constructor, so a set is fully populated the moment it is created. Wrap the declarations in BeginUpdate / EndUpdateEndUpdate records each tool's OnExecute handler so it can be reattached before every request.

Setting the AI property registers the set with a TTMSMCPCloudAI. On each Execute the component merges its own Tools with the tools of every registered set, so both sources are advertised together. Setting AI to nil unregisters the set, and so does freeing the component it points at.

Cloud AI Tool Sets ships ready-made descendants for logging, file system access, dataset queries, and email.

Parallel tool execution

When a question needs three independent lookups, one round trip per lookup is three times the latency.

Settings.ParallelToolExecution := True lets the model request several tool calls in a single turn where the service supports it — currently OpenAI, Claude, Grok, and llama.cpp. The component executes each requested call and returns all results together. Leave it False (the default) when your handlers touch shared state that is not safe to interleave.

Not every service supports function calling at all. Rather than hard-coding which do, ask: GetServices(True) — or GetActiveServices(True) for services that also have a key — returns only the services that do.

Combining tools, a tool set, and parallel execution

A production request usually mixes hand-declared tools with a packaged set, enables parallel execution, and then reads one final answer:

procedure TForm1.RunStockAssistant(const AQuestion: string);
begin
  { 1. Hand-declared tools on the component itself. }
  AI.Tools.Clear;
  RegisterStockTool;
  RegisterReportTool;

  { 2. A reusable tool set contributes its own tools to the same request. }
  if not Assigned(FInventory) then
  begin
    FInventory := TInventoryToolSet.Create(Self);
    FInventory.AI := AI;
  end;

  { 3. Let the model call several of them in one turn where the service
       supports it. Only tools with Enabled set are offered. }
  AI.Settings.ParallelToolExecution := True;

  { Function calling is not offered by every service - ask for the list of
    services that support it rather than assuming. }
  if AI.GetActiveServices(True).IndexOf('Claude') >= 0 then
    AI.Service := aiClaude;

  AI.SystemRole.Text :=
    'Use the supplied tools to answer stock questions. Never guess a number.';
  AI.Context.Text := AQuestion;

  { 4. The final answer - after all tool round trips - arrives in OnExecuted. }
  AI.OnExecuted := AIExecuted;

  memoLog.Lines.Add(Format('Offering %d tools', [AI.Tools.ActiveCount]));
  AI.Execute;
end;

OnExecuted fires once, after the whole exchange — including every tool round trip — has completed. The intermediate tool calls do not surface there; they surface in your OnExecute handlers.

Common mistakes

  • A vague Description. The model chooses tools from the description alone. "Handles stock" gets called for the wrong questions; "Returns the number of units currently in stock for an article" does not.
  • Confusing Properties with ArrayProperties. Properties describes an object's fields; ArrayProperties describes one element of an array. Filling in the wrong one produces a schema the model cannot satisfy.
  • Leaving Required at its default for optional arguments. It defaults to True, so an argument you meant to be optional will be invented rather than omitted.
  • Freeing what Args hands you. The TJSONObject and any nested TJSONObject / TJSONArray read out of it are owned by the framework.
  • Declaring tools but calling a service that has none. Perplexity, for instance, is excluded from GetServices(True). Check the capability instead of assuming it.
  • Declaring a tool set's tools outside BeginUpdate / EndUpdate. EndUpdate is what records the handlers for reattachment; without it a tool can reach the model with no OnExecute behind it.
  • Blocking inside OnExecute. The model is waiting on that result. Keep handlers short and deterministic; long work belongs behind a job the tool starts and a second tool queries.

See also