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:
parent
41c9996019
commit
a85fee407a
6 changed files with 154 additions and 24 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue