Tool results
What a tool sends back is as much a part of its contract as what it accepts. A plain string is fine for a greeting, but a model consuming a customer record, a generated file, or a long-running rebuild needs more: typed fields it can read without parsing prose, links to content it can fetch separately, and a way to start work that will not finish inside one request. This guide covers the return type, structured output described by a JSON schema, native MCP content items, and opt-in execution as a long-running task.
The return type
ReturnType declares what the method's TValue result holds, using the same
TTMSMCPToolPropertyType values as input properties. The framework converts the
TValue to JSON accordingly, so a ptFloat tool returning `Args[0].AsExtended
- Args[1].AsExtended` arrives at the client as a JSON number rather than a string.
Three execution callbacks are available, and a tool uses one of them:
| Callback | Signature | Arguments arrive as |
|---|---|---|
ExecuteCallback / OnExecute |
TTMSMCPMethod / TTMSMCPMethodEvent |
array of TValue, positional |
DynamicExecuteCallback / OnDynamicExecute |
TTMSMCPDynamicMethod / TTMSMCPDynamicMethodEvent |
TDictionary<string, TValue>, by name |
StructuredExecuteCallback |
TTMSMCPDynamicStructuredMethod |
TDictionary<string, TValue>, by name; returns TJSONObject |
Prefer the dynamic forms once a tool has more than two or three arguments —
Args['customer_id'] survives a reordered property list, Args[0] does not.
Structured output
A tool that returns a record should say so. OutputSchema takes a JSON schema
string, and StructuredExecuteCallback returns a TJSONObject matching it. The
client then receives typed fields instead of text it has to parse:
procedure TForm1.RegisterLookupCustomerTool;
var
Tool: TTMSMCPTool;
begin
Tool := TTMSMCPTool.CreateBuilder
.Name('lookup_customer')
.Description('Looks up a customer and returns their record')
.ReadOnlyHint(True)
.OutputSchema(
'{"type":"object","properties":{' +
'"name":{"type":"string"},' +
'"city":{"type":"string"},' +
'"balance":{"type":"number"}},' +
'"required":["name"]}')
.StructuredExecuteCallback(
function(const Args: TDictionary<string, TValue>): TJSONObject
var
Id: string;
begin
Id := Args['customer_id'].AsString;
Result := TJSONObject.Create;
Result.AddPair('name', LookupName(Id));
Result.AddPair('city', LookupCity(Id));
Result.AddPair('balance', TJSONNumber.Create(LookupBalance(Id)));
end)
.AddProperty
.Name('customer_id')
.Description('Customer identifier')
.PropertyType(ptString)
.Required(True)
.&End
.Build;
Server.Tools.Add(Tool);
end;
The schema is advertised with the tool, so a model can plan around the shape of
the answer before calling. Keep required honest — a field you list but do not
always populate makes the schema a lie.
For a schema derived from an existing Delphi type rather than written by hand,
use TTMSMCPJSONSchemaGenerator
with TTMSMCPJSONSchemaOptions;
OnGenerateInputSchema gives the same control over the input schema.
Native content items
MCP defines content items — text, image, audio, resource, and resource link —
that a tool can return as a JSON array instead of a single scalar.
TTMSMCPContent builds them:
| Method | Produces |
|---|---|
TTMSMCPContent.Text(AText) |
A text content item. |
TTMSMCPContent.ResourceLink(AURI, AName, ADescription, AMimeType, ATitle) |
A link to a resource the client can fetch separately. |
TTMSMCPContent.ContentArray([…]) |
The array wrapping several content items. |
This is the right shape when the answer is "here is a summary, and here is where the actual artefact lives" — the model reads the text, and the client fetches the file only if it needs it:
procedure TForm1.RegisterBuildReportTool;
var
Tool: TTMSMCPTool;
begin
Tool := TTMSMCPTool.CreateBuilder
.Name('build_report')
.Description('Builds a report and returns a summary plus a link to the file')
.ReturnType(ptJSON)
.ExecuteCallback(
function(const Args: array of TValue): TValue
var
URI: string;
Content: TJSONArray;
begin
URI := BuildReport(Args[0].AsString);
Content := TTMSMCPContent.ContentArray([
TTMSMCPContent.Text('The report is ready.'),
TTMSMCPContent.ResourceLink(URI, 'report.pdf',
'Generated report', 'application/pdf')
]);
Result := Content.ToJSON;
end)
.AddProperty
.Name('period')
.Description('Reporting period, for example 2026-Q1')
.PropertyType(ptString)
.Required(True)
.&End
.Build;
Server.Tools.Add(Tool);
end;
A resource link points at something the Resources component serves, so the URI should be one the same server can resolve.
Long-running tasks
Some work does not fit in a request. TaskSupport opts a tool into the MCP
tasks capability:
| Value | Meaning |
|---|---|
tsForbidden |
The tool never runs as a task. This is the default. |
tsOptional |
The client may run the call as a task, or inline. |
tsRequired |
The call must be run as a task. |
With tsOptional or tsRequired, the client gets a task id back immediately and
polls for status, and the server can report progress, accept cancellation, and
even ask the user a question mid-task through elicitation. The server side of
that lifecycle — OnTaskExecute, SetTaskResult, SetTaskError, and
RequestTaskElicitation — belongs to
TTMSMCPServer and is covered in the
MCP Server guides.
Setting TaskSupport alone does not make a tool asynchronous. It advertises the
capability; the server's task handling is what actually runs the work off the
request.
Combining output schema, hints, enums, and task support
A production tool usually uses several of these at once. This one constrains its argument to a known set, promises idempotence so a client may safely retry, describes its result with an output schema, and allows the client to run it as a task because a reindex can take minutes:
procedure TForm1.RegisterReindexTool;
var
Tool: TTMSMCPTool;
begin
Tool := TTMSMCPTool.CreateBuilder
.Name('reindex_catalog')
.Title('Reindex catalog')
.Description('Rebuilds the search index for one catalog section')
.DestructiveHint(False)
.IdempotentHint(True)
.OpenWorldHint(False)
.TaskSupport(tsOptional)
.OutputSchema(
'{"type":"object","properties":{' +
'"section":{"type":"string"},' +
'"documents":{"type":"integer"},' +
'"seconds":{"type":"number"}}}')
.StructuredExecuteCallback(
function(const Args: TDictionary<string, TValue>): TJSONObject
var
Section: string;
Started: TDateTime;
Count: Integer;
begin
Section := Args['section'].AsString;
Started := Now;
Count := ReindexSection(Section);
Result := TJSONObject.Create;
Result.AddPair('section', Section);
Result.AddPair('documents', TJSONNumber.Create(Count));
Result.AddPair('seconds',
TJSONNumber.Create(SecondSpan(Started, Now)));
end)
.AddProperty
.Name('section')
.Description('Catalog section to reindex')
.PropertyType(ptString)
.AddEnums(['products', 'articles', 'media'])
.Required(True)
.&End
.Build;
Server.Tools.Add(Tool);
end;
Each declaration answers a different question a client has to resolve before calling: what may I send, is a retry safe, what will I get back, and how long will this take.
Common mistakes
- An output schema with no structured callback.
OutputSchemadescribes the result;StructuredExecuteCallbackproduces it. Declaring the schema while returning a plain string throughExecuteCallbackadvertises a contract the tool does not honour. - Freeing the
TJSONObjectyou return. The result is handed to the framework, which serialises and releases it. Freeing it in the callback leaves the caller with a dangling reference. - Expecting
TaskSupportto make the call asynchronous by itself. It advertises the capability. Without server-side task handling the work still runs inline. - Positional arguments on a wide tool.
Args[3]silently becomes the wrong value the day someone reorders the property list. Use the dynamic callbacks and read arguments by name.
See also
- Defining tools — names, properties, and hints
- MCP Server — tasks, progress, and elicitation
- Resources — what a resource link points at
TTMSMCPContent,TTMSMCPTool