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
GetActiveServicesorGetServices. 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 callFreeon 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. CheckTools.ActiveCounton the set and the request's tool count when a set appears to do nothing. - Assigning
OnExecuteto a stock tool. The component restores each set's recorded handler before every request, so an override assigned from outside is replaced on the nextExecute. 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.
AIholds 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.
Enabledand the system role are the two levers you have — use both.
See also
- The four shipped tool sets — every tool and its arguments
- Function calling — the tool and parameter model this builds on
- Cloud AI — the component a tool set attaches to
TTMSMCPCloudAIToolSet,TTMSMCPCloudAITools,TTMSMCPCloudAITool,TTMSMCPCloudAIParameter