Add support for outputSchema and optional content fields in tools

- Add outputSchema field to Tool type and RegisteredTool interface
- Make content field optional in CallToolResult
- Update ListToolsRequestSchema handler to include outputSchema in tool list responses
- Add support for structuredContent in tool results
- Update examples to handle optional content field
- Add tests for new outputSchema and structuredContent functionality
- Update ToolCallback documentation to clarify when to use structuredContent vs content

This change enables tools to define structured output schemas and return
structured JSON content, providing better type safety and validation for
tool outputs.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Basil Hosmer 2025-05-05 16:17:57 -04:00
commit a85fee407a
No known key found for this signature in database
6 changed files with 154 additions and 24 deletions

View file

@ -61,13 +61,19 @@ async function main(): Promise<void> {
// Log the results from each tool call
for (const [caller, result] of Object.entries(toolResults)) {
console.log(`\n=== Tool result for ${caller} ===`);
result.content.forEach((item: { type: string; text?: string; }) => {
if (item.type === 'text') {
console.log(` ${item.text}`);
} else {
console.log(` ${item.type} content:`, item);
}
});
if (result.content) {
result.content.forEach((item: { type: string; text?: string; }) => {
if (item.type === 'text') {
console.log(` ${item.text}`);
} else {
console.log(` ${item.type} content:`, item);
}
});
} else if (result.structuredContent) {
console.log(` Structured content: ${result.structuredContent}`);
} else {
console.log(` No content returned`);
}
}
// 3. Wait for all notifications (10 seconds)

View file

@ -341,13 +341,19 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<vo
});
console.log('Tool result:');
result.content.forEach(item => {
if (item.type === 'text') {
console.log(` ${item.text}`);
} else {
console.log(` ${item.type} content:`, item);
}
});
if (result.content) {
result.content.forEach(item => {
if (item.type === 'text') {
console.log(` ${item.text}`);
} else {
console.log(` ${item.type} content:`, item);
}
});
} else if (result.structuredContent) {
console.log(` Structured content: ${result.structuredContent}`);
} else {
console.log(' No content returned');
}
} catch (error) {
console.log(`Error calling tool ${name}: ${error}`);
}

View file

@ -173,13 +173,19 @@ async function startNotificationTool(client: Client): Promise<void> {
const result = await client.request(request, CallToolResultSchema);
console.log('Tool result:');
result.content.forEach(item => {
if (item.type === 'text') {
console.log(` ${item.text}`);
} else {
console.log(` ${item.type} content:`, item);
}
});
if (result.content) {
result.content.forEach(item => {
if (item.type === 'text') {
console.log(` ${item.text}`);
} else {
console.log(` ${item.type} content:`, item);
}
});
} else if (result.structuredContent) {
console.log(` Structured content: ${result.structuredContent}`);
} else {
console.log(' No content returned');
}
} catch (error) {
console.log(`Error calling notification tool: ${error}`);
}

View file

@ -768,6 +768,104 @@ describe("tool()", () => {
mcpServer.tool("tool2", () => ({ content: [] }));
});
test("should support tool with outputSchema and structuredContent", async () => {
const mcpServer = new McpServer({
name: "test server",
version: "1.0",
});
const client = new Client(
{
name: "test client",
version: "1.0",
},
{
capabilities: {
tools: {},
},
},
);
// Register a tool with outputSchema
const registeredTool = mcpServer.tool(
"test",
"Test tool with structured output",
{
input: z.string(),
},
async ({ input }) => ({
// When outputSchema is defined, return structuredContent instead of content
structuredContent: JSON.stringify({
processedInput: input,
resultType: "structured",
timestamp: "2023-01-01T00:00:00Z"
}),
}),
);
// Update the tool to add outputSchema
registeredTool.update({
outputSchema: {
type: "object",
properties: {
processedInput: { type: "string" },
resultType: { type: "string" },
timestamp: { type: "string", format: "date-time" }
},
required: ["processedInput", "resultType", "timestamp"]
}
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
mcpServer.server.connect(serverTransport),
]);
// Verify the tool registration includes outputSchema
const listResult = await client.request(
{
method: "tools/list",
},
ListToolsResultSchema,
);
expect(listResult.tools).toHaveLength(1);
expect(listResult.tools[0].outputSchema).toEqual({
type: "object",
properties: {
processedInput: { type: "string" },
resultType: { type: "string" },
timestamp: { type: "string", format: "date-time" }
},
required: ["processedInput", "resultType", "timestamp"]
});
// Call the tool and verify it returns structuredContent
const result = await client.request(
{
method: "tools/call",
params: {
name: "test",
arguments: {
input: "hello",
},
},
},
CallToolResultSchema,
);
expect(result.structuredContent).toBeDefined();
expect(result.content).toBeUndefined(); // Should not have content when structuredContent is used
const parsed = JSON.parse(result.structuredContent || "{}");
expect(parsed.processedInput).toBe("hello");
expect(parsed.resultType).toBe("structured");
expect(parsed.timestamp).toBe("2023-01-01T00:00:00Z");
});
test("should pass sessionId to tool callback via RequestHandlerExtra", async () => {
const mcpServer = new McpServer({
name: "test server",
@ -871,7 +969,7 @@ describe("tool()", () => {
expect(receivedRequestId).toBeDefined();
expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true);
expect(result.content[0].text).toContain("Received request ID:");
expect(result.content && result.content[0].text).toContain("Received request ID:");
});
test("should provide sendNotification within tool call", async () => {

View file

@ -119,6 +119,7 @@ export class McpServer {
strictUnions: true,
}) as Tool["inputSchema"])
: EMPTY_OBJECT_JSON_SCHEMA,
outputSchema: tool.outputSchema,
annotations: tool.annotations,
};
},
@ -696,6 +697,7 @@ export class McpServer {
description,
inputSchema:
paramsSchema === undefined ? undefined : z.object(paramsSchema),
outputSchema: undefined,
annotations,
callback: cb,
enabled: true,
@ -709,6 +711,7 @@ export class McpServer {
}
if (typeof updates.description !== "undefined") registeredTool.description = updates.description
if (typeof updates.paramsSchema !== "undefined") registeredTool.inputSchema = z.object(updates.paramsSchema)
if (typeof updates.outputSchema !== "undefined") registeredTool.outputSchema = updates.outputSchema
if (typeof updates.callback !== "undefined") registeredTool.callback = updates.callback
if (typeof updates.annotations !== "undefined") registeredTool.annotations = updates.annotations
if (typeof updates.enabled !== "undefined") registeredTool.enabled = updates.enabled
@ -896,6 +899,11 @@ export class ResourceTemplate {
* Callback for a tool handler registered with Server.tool().
*
* Parameters will include tool arguments, if applicable, as well as other request handler context.
*
* The callback should return:
* - `structuredContent` if the tool has an outputSchema defined
* - `content` if the tool does not have an outputSchema
* - Both fields are optional but typically one should be provided
*/
export type ToolCallback<Args extends undefined | ZodRawShape = undefined> =
Args extends ZodRawShape
@ -908,12 +916,13 @@ export type ToolCallback<Args extends undefined | ZodRawShape = undefined> =
export type RegisteredTool = {
description?: string;
inputSchema?: AnyZodObject;
outputSchema?: Tool["outputSchema"];
annotations?: ToolAnnotations;
callback: ToolCallback<undefined | ZodRawShape>;
enabled: boolean;
enable(): void;
disable(): void;
update<Args extends ZodRawShape>(updates: { name?: string | null, description?: string, paramsSchema?: Args, callback?: ToolCallback<Args>, annotations?: ToolAnnotations, enabled?: boolean }): void
update<Args extends ZodRawShape>(updates: { name?: string | null, description?: string, paramsSchema?: Args, outputSchema?: Tool["outputSchema"], callback?: ToolCallback<Args>, annotations?: ToolAnnotations, enabled?: boolean }): void
remove(): void
};

View file

@ -831,6 +831,10 @@ export const ToolSchema = z
properties: z.optional(z.object({}).passthrough()),
})
.passthrough(),
/**
* A JSON Schema object defining the expected output for the tool.
*/
outputSchema: z.object({type: z.any()}).passthrough().optional(),
/**
* Optional additional tool information.
*/
@ -858,7 +862,8 @@ export const ListToolsResultSchema = PaginatedResultSchema.extend({
export const CallToolResultSchema = ResultSchema.extend({
content: z.array(
z.union([TextContentSchema, ImageContentSchema, AudioContentSchema, EmbeddedResourceSchema]),
),
).optional(),
structuredContent: z.string().optional(),
isError: z.boolean().default(false).optional(),
});