Table of Contents

Attaching tool sets

A tool set is a component that already holds a group of function-calling tools: their names, descriptions, parameter schemas, and the handlers that answer them. You do not register the tools, wire the handlers, or add anything to the component's own Tools collection — you set one property, AI, and from the next request onwards the model can call everything the set declares. That makes a tool set the cheapest way to give a model real capability, and also the easiest way to give it more capability than you intended. This guide covers what registration actually does, how a set's tools reach the model on each request, how to inspect a set and withhold individual tools, how to move a set between components and tear it down safely, and how to derive from a shipped set when you need one more tool alongside the stock ones.

Attaching a set to a component

A tool set is a plain TComponent, so it can sit on the form at design time or be created in code. Either way, the step that matters is assigning AI:

procedure TForm1.AttachDataSetTools;
begin
  { A tool set is a plain TComponent, so it can be dropped on the form at
    design time or created in code. It is owned by the form here, so the
    form frees it. }
  FDataTools := TTMSMCPCloudAIDataSet.Create(Self);

  { Configure the set before the first request - the tools read these
    properties while they run, not while they are declared. }
  FDataTools.DataSource := dsCustomers;

  { Assigning AI registers the set with the component. Assigning nil
    unregisters it again. }
  FDataTools.AI := AI;
end;

procedure TForm1.DetachDataSetTools;
begin
  { Unregister without destroying: the set keeps its tools and can be
    attached to another component later. }
  if Assigned(FDataTools) then
    FDataTools.AI := nil;
end;

AI is a two-way switch, not just a reference. Assigning a component calls RegisterToolSet on it; assigning nil calls UnRegisterToolSet. Assigning a different component does both in one step. A set that has never been attached contributes nothing, and there is no separate "activate" call to forget.

Configure the set before the first request. The tools read properties such as DataSource and LogFileName at the moment they run, not at the moment they are declared, so a property set after attaching still takes effect — but a property left empty surfaces as a failed tool call in the middle of a conversation, which is a far worse place to discover it.

How a set's tools reach the model

Each Execute call rebuilds the tool list from scratch. The component copies its own Tools collection, then appends the Tools of every registered set in registration order, and offers that union to the service. Two consequences follow.

First, a set attached after a request was issued affects the next request, not the one in flight. Second, the merge is by collection, not by name: two sets that both declare a tool called GetFiles both reach the model, and which one answers is not something you want to depend on. Keep tool names distinct across the sets you attach to the same component.

Before each merge, the component also reattaches each tool's original handler by name — the handler the set recorded when it declared the tool. This is what keeps a set working after its Tools collection has been assigned or copied around, and it is why assigning your own OnExecute to a stock tool does not survive: the next Execute puts the set's own handler back. To answer a tool yourself, declare it under a new name instead (see Extending a shipped set).

You can see exactly what a set brings by walking its Tools collection — useful when a model keeps choosing an unexpected tool, because the descriptions in that collection are all the model has to choose from:

procedure TForm1.LogToolSetContents(AToolSet: TTMSMCPCloudAIToolSet);
var
  I, J: Integer;
  Tool: TTMSMCPCloudAITool;
  Param: TTMSMCPCloudAIParameter;
begin
  memoLog.Lines.Add(Format('%s contributes %d tools, %d of them enabled',
    [AToolSet.ClassName, AToolSet.Tools.Count, AToolSet.Tools.ActiveCount]));

  for I := 0 to AToolSet.Tools.Count - 1 do
  begin
    Tool := AToolSet.Tools[I];
    memoLog.Lines.Add(Format('  %s - %s', [Tool.Name, Tool.Description]));

    for J := 0 to Tool.Parameters.Count - 1 do
    begin
      Param := Tool.Parameters[J];
      memoLog.Lines.Add(Format('    %s (%s)',
        [Param.Name,
         BoolToStr(Param.Required, True)]));
    end;
  end;
end;

Withholding individual tools

A shipped set is deliberately broad. The file system set can write, copy, and move as readily as it can read, and nothing in it asks for confirmation. When you want only part of a set, clear Enabled on the tools you are not willing to offer:

procedure TForm1.MakeFileSystemReadOnly;
var
  I: Integer;
  Tool: TTMSMCPCloudAITool;
begin
  for I := 0 to FFiles.Tools.Count - 1 do
  begin
    Tool := FFiles.Tools[I];

    { Enabled is the supported way to withhold a tool. A disabled tool stays
      in the collection but is not offered to the model, so the model cannot
      ask for it. }
    Tool.Enabled :=
      SameText(Tool.Name, 'GetFiles') or
      SameText(Tool.Name, 'GetFolders') or
      SameText(Tool.Name, 'ReadTextFile');
  end;

  memoLog.Lines.Add(Format('Offering %d of %d file system tools',
    [FFiles.Tools.ActiveCount, FFiles.Tools.Count]));
end;

A disabled tool stays in the collection but is not sent to the service, so the model cannot call what it has never been told about. Tools.ActiveCount reports how many survive the filter — worth logging next to the request, since an off-by-one in the name comparison silently offers a tool you meant to withhold.

Enabled is a per-tool switch, so it is set on the set's own Tools items rather than on the component. Re-enabling is just as cheap, which makes this a reasonable way to vary what a model may do per operation: read-only while drafting, writable once the user has approved a plan.

Moving a set and tearing it down

One set can serve several components over its lifetime, and the registration follows the AI property rather than the construction order:

procedure TForm1.MoveLoggerToDrafting;
begin
  { Assigning a different component unregisters from the previous one and
    registers with the new one in a single step. }
  FLogger.AI := AIDrafting;
end;

procedure TForm1.ReleaseToolSets;
begin
  { Destroying a tool set clears AI first, so the component it was attached
    to drops the registration. The tools the set owns go with it. }
  FreeAndNil(FLogger);

  { The reverse direction is handled as well: when the TTMSMCPCloudAI is
    freed first, every attached set has its AI reference cleared, so
    teardown is safe in either order. }
  FreeAndNil(AIDrafting);
end;

Teardown is safe in either direction. Destroying the tool set clears AI first, so the component drops the registration; destroying the component notifies every attached set, which clears its own AI reference. Neither leaves a dangling pointer, so you are free to let the form own both and free them in whatever order it likes.

What a set does not survive is being attached to two components at once. AI holds a single reference, so the second assignment unregisters from the first. When two components genuinely need the same capability, create two sets.

Extending a shipped set

When a stock set is nearly right but missing one operation, derive from it and override DefineTools. Call inherited first so the stock tools are declared, then add yours:

type
  TArchiveFileSystem = class(TTMSMCPCloudAIFileSystem)
  protected
    procedure DefineTools; override;
    procedure DoArchiveFile(Sender: TObject; Args: TJSONObject;
      var Result: string);
  end;

procedure TArchiveFileSystem.DefineTools;
var
  Tool: TTMSMCPCloudAITool;
  Param: TTMSMCPCloudAIParameter;
begin
  { inherited declares the eight file system tools first. }
  inherited;

  { BeginUpdate / EndUpdate wrap the declarations. EndUpdate records the
    handler of every tool by name, and the component reattaches those
    handlers before each request - so declare the tools and assign
    OnExecute between the two calls. }
  BeginUpdate;

  Tool := Tools.Add;
  Tool.Name := 'ArchiveFile';
  Tool.Description := 'move a file into the dated archive folder';

  Param := Tool.Parameters.Add;
  Param.Name := 'Filename';
  Param.Description := 'the full path of the file to archive';
  Param.&Type := ptString;
  Param.Required := True;

  Tool.OnExecute := DoArchiveFile;

  EndUpdate;
end;

procedure TArchiveFileSystem.DoArchiveFile(Sender: TObject; Args: TJSONObject;
  var Result: string);
var
  Source, Target: string;
begin
  Source := Args.GetValue<string>('Filename');
  Target := TPath.Combine(ArchiveFolderFor(Now), TPath.GetFileName(Source));

  TFile.Move(Source, Target);
  Result := 'Archived to ' + Target;
end;

DefineTools runs from the constructor, so the collection is complete by the time the component exists — there is no initialization step to call. Wrap the additions in BeginUpdate and EndUpdate: EndUpdate records each tool's handler by name, which is what lets the component reattach handlers before every request. A tool declared outside that pair keeps working until something reassigns the collection, and then quietly stops.

Parameters follow the same model as hand-declared tools — TTMSMCPCloudAIParameter with a Name, a Description, a &Type, and Required — so the function calling guide covers enums, formats, nested objects, and arrays without change. The description is the part that decides whether the model ever calls your tool, so write it for the model, not for a colleague reading the source.

Combining a shipped set with tools of your own

A realistic request usually mixes both: a stock set narrowed to the operations you are prepared to allow, plus one or two tools that only your application can answer. They live in different collections and are merged at request time, so nothing has to be copied between them:

procedure TForm1.RunDocumentAssistant(const AQuestion: string);
var
  I: Integer;
  Tool: TTMSMCPCloudAITool;
  Param: TTMSMCPCloudAIParameter;
  Services: TStringList;
begin
  { 1. Attach the stock file system tool set once. }
  if not Assigned(FFiles) then
  begin
    FFiles := TTMSMCPCloudAIFileSystem.Create(Self);
    FFiles.AI := AI;
  end;

  { 2. Narrow it: the model may read the disk but never write to it. }
  for I := 0 to FFiles.Tools.Count - 1 do
  begin
    Tool := FFiles.Tools[I];
    Tool.Enabled := SameText(Tool.Name, 'GetFiles') or
      SameText(Tool.Name, 'ReadTextFile');
  end;

  { 3. Add a tool of your own on the component itself. Tools declared here
       and tools coming from attached sets are merged on every Execute. }
  AI.Tools.Clear;
  Tool := AI.Tools.Add;
  Tool.Name := 'lookup_customer';
  Tool.Description := 'Returns the customer record for an account number';

  Param := Tool.Parameters.Add;
  Param.Name := 'account';
  Param.Description := 'The account number to look up';
  Param.&Type := ptString;
  Param.Required := True;

  Tool.OnExecute := DoLookupCustomer;

  { 4. Function calling is not offered by every service, so ask for the
       list rather than assuming. Never free what it returns: the component
       hands back its own internal list, not a copy. }
  Services := AI.GetActiveServices(True);
  if Services.IndexOf('Claude') >= 0 then
    AI.Service := aiClaude;

  AI.SystemRole.Text :=
    'Use the file tools for anything on disk and lookup_customer for ' +
    'account data. Never guess a value you can look up.';
  AI.Context.Text := AQuestion;
  AI.OnExecuted := AIExecuted;
  AI.Execute;
end;

Note the last step. Function calling is not offered by every service, so the example asks GetActiveServices(True) which of the configured providers supports it rather than hard-coding one.

Common mistakes

  • Freeing the list returned by GetActiveServices or GetServices. Both return the component's internal list, not a copy. Freeing it corrupts the component; read the entries, or copy them into a list of your own, and never call Free on the result.
  • Forgetting to set AI. A tool set that is constructed but never attached is invisible. There is no error and no warning — the model simply never sees the tools. Check Tools.ActiveCount on the set and the request's tool count when a set appears to do nothing.
  • Assigning OnExecute to a stock tool. The component restores each set's recorded handler before every request, so an override assigned from outside is replaced on the next Execute. Declare a tool under a new name instead.
  • Declaring tools outside BeginUpdate / EndUpdate. The handler is then never recorded, so it is not reattached when the collection is rebuilt. The tool works at first and stops later, which is the hardest version of this bug to find.
  • Configuring the set after the model has already been asked. DataSource, LogFileName, and the mail server properties are read while a tool runs. An unset property fails the tool call mid-conversation rather than at start-up.
  • Attaching one set to two components. AI holds one reference; the second assignment silently detaches the first. Create one set per component.
  • Relying on the set to enforce a boundary. No shipped tool restricts a path, a table, or a recipient. Enabled and the system role are the two levers you have — use both.

See also