Skip to content

Commit a95df72

Browse files
bhosmer-antclaude
andcommitted
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>
1 parent 621ccea commit a95df72

File tree

6 files changed

+154
-24
lines changed

6 files changed

+154
-24
lines changed

src/examples/client/parallelToolCallsClient.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,19 @@ async function main(): Promise<void> {
6161
// Log the results from each tool call
6262
for (const [caller, result] of Object.entries(toolResults)) {
6363
console.log(`\n=== Tool result for ${caller} ===`);
64-
result.content.forEach((item: { type: string; text?: string; }) => {
65-
if (item.type === 'text') {
66-
console.log(` ${item.text}`);
67-
} else {
68-
console.log(` ${item.type} content:`, item);
69-
}
70-
});
64+
if (result.content) {
65+
result.content.forEach((item: { type: string; text?: string; }) => {
66+
if (item.type === 'text') {
67+
console.log(` ${item.text}`);
68+
} else {
69+
console.log(` ${item.type} content:`, item);
70+
}
71+
});
72+
} else if (result.structuredContent) {
73+
console.log(` Structured content: ${result.structuredContent}`);
74+
} else {
75+
console.log(` No content returned`);
76+
}
7177
}
7278

7379
// 3. Wait for all notifications (10 seconds)

src/examples/client/simpleStreamableHttp.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -341,13 +341,19 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<vo
341341
});
342342

343343
console.log('Tool result:');
344-
result.content.forEach(item => {
345-
if (item.type === 'text') {
346-
console.log(` ${item.text}`);
347-
} else {
348-
console.log(` ${item.type} content:`, item);
349-
}
350-
});
344+
if (result.content) {
345+
result.content.forEach(item => {
346+
if (item.type === 'text') {
347+
console.log(` ${item.text}`);
348+
} else {
349+
console.log(` ${item.type} content:`, item);
350+
}
351+
});
352+
} else if (result.structuredContent) {
353+
console.log(` Structured content: ${result.structuredContent}`);
354+
} else {
355+
console.log(' No content returned');
356+
}
351357
} catch (error) {
352358
console.log(`Error calling tool ${name}: ${error}`);
353359
}

src/examples/client/streamableHttpWithSseFallbackClient.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,13 +173,19 @@ async function startNotificationTool(client: Client): Promise<void> {
173173
const result = await client.request(request, CallToolResultSchema);
174174

175175
console.log('Tool result:');
176-
result.content.forEach(item => {
177-
if (item.type === 'text') {
178-
console.log(` ${item.text}`);
179-
} else {
180-
console.log(` ${item.type} content:`, item);
181-
}
182-
});
176+
if (result.content) {
177+
result.content.forEach(item => {
178+
if (item.type === 'text') {
179+
console.log(` ${item.text}`);
180+
} else {
181+
console.log(` ${item.type} content:`, item);
182+
}
183+
});
184+
} else if (result.structuredContent) {
185+
console.log(` Structured content: ${result.structuredContent}`);
186+
} else {
187+
console.log(' No content returned');
188+
}
183189
} catch (error) {
184190
console.log(`Error calling notification tool: ${error}`);
185191
}

src/server/mcp.test.ts

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,104 @@ describe("tool()", () => {
721721
mcpServer.tool("tool2", () => ({ content: [] }));
722722
});
723723

724+
test("should support tool with outputSchema and structuredContent", async () => {
725+
const mcpServer = new McpServer({
726+
name: "test server",
727+
version: "1.0",
728+
});
729+
730+
const client = new Client(
731+
{
732+
name: "test client",
733+
version: "1.0",
734+
},
735+
{
736+
capabilities: {
737+
tools: {},
738+
},
739+
},
740+
);
741+
742+
// Register a tool with outputSchema
743+
const registeredTool = mcpServer.tool(
744+
"test",
745+
"Test tool with structured output",
746+
{
747+
input: z.string(),
748+
},
749+
async ({ input }) => ({
750+
// When outputSchema is defined, return structuredContent instead of content
751+
structuredContent: JSON.stringify({
752+
processedInput: input,
753+
resultType: "structured",
754+
timestamp: "2023-01-01T00:00:00Z"
755+
}),
756+
}),
757+
);
758+
759+
// Update the tool to add outputSchema
760+
registeredTool.update({
761+
outputSchema: {
762+
type: "object",
763+
properties: {
764+
processedInput: { type: "string" },
765+
resultType: { type: "string" },
766+
timestamp: { type: "string", format: "date-time" }
767+
},
768+
required: ["processedInput", "resultType", "timestamp"]
769+
}
770+
});
771+
772+
const [clientTransport, serverTransport] =
773+
InMemoryTransport.createLinkedPair();
774+
775+
await Promise.all([
776+
client.connect(clientTransport),
777+
mcpServer.server.connect(serverTransport),
778+
]);
779+
780+
// Verify the tool registration includes outputSchema
781+
const listResult = await client.request(
782+
{
783+
method: "tools/list",
784+
},
785+
ListToolsResultSchema,
786+
);
787+
788+
expect(listResult.tools).toHaveLength(1);
789+
expect(listResult.tools[0].outputSchema).toEqual({
790+
type: "object",
791+
properties: {
792+
processedInput: { type: "string" },
793+
resultType: { type: "string" },
794+
timestamp: { type: "string", format: "date-time" }
795+
},
796+
required: ["processedInput", "resultType", "timestamp"]
797+
});
798+
799+
// Call the tool and verify it returns structuredContent
800+
const result = await client.request(
801+
{
802+
method: "tools/call",
803+
params: {
804+
name: "test",
805+
arguments: {
806+
input: "hello",
807+
},
808+
},
809+
},
810+
CallToolResultSchema,
811+
);
812+
813+
expect(result.structuredContent).toBeDefined();
814+
expect(result.content).toBeUndefined(); // Should not have content when structuredContent is used
815+
816+
const parsed = JSON.parse(result.structuredContent || "{}");
817+
expect(parsed.processedInput).toBe("hello");
818+
expect(parsed.resultType).toBe("structured");
819+
expect(parsed.timestamp).toBe("2023-01-01T00:00:00Z");
820+
});
821+
724822
test("should pass sessionId to tool callback via RequestHandlerExtra", async () => {
725823
const mcpServer = new McpServer({
726824
name: "test server",
@@ -824,7 +922,7 @@ describe("tool()", () => {
824922

825923
expect(receivedRequestId).toBeDefined();
826924
expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true);
827-
expect(result.content[0].text).toContain("Received request ID:");
925+
expect(result.content && result.content[0].text).toContain("Received request ID:");
828926
});
829927

830928
test("should provide sendNotification within tool call", async () => {

src/server/mcp.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export class McpServer {
119119
strictUnions: true,
120120
}) as Tool["inputSchema"])
121121
: EMPTY_OBJECT_JSON_SCHEMA,
122+
outputSchema: tool.outputSchema,
122123
annotations: tool.annotations,
123124
};
124125
},
@@ -703,6 +704,7 @@ export class McpServer {
703704
description,
704705
inputSchema:
705706
paramsSchema === undefined ? undefined : z.object(paramsSchema),
707+
outputSchema: undefined,
706708
annotations,
707709
callback: cb,
708710
enabled: true,
@@ -716,6 +718,7 @@ export class McpServer {
716718
}
717719
if (typeof updates.description !== "undefined") registeredTool.description = updates.description
718720
if (typeof updates.paramsSchema !== "undefined") registeredTool.inputSchema = z.object(updates.paramsSchema)
721+
if (typeof updates.outputSchema !== "undefined") registeredTool.outputSchema = updates.outputSchema
719722
if (typeof updates.callback !== "undefined") registeredTool.callback = updates.callback
720723
if (typeof updates.annotations !== "undefined") registeredTool.annotations = updates.annotations
721724
if (typeof updates.enabled !== "undefined") registeredTool.enabled = updates.enabled
@@ -903,6 +906,11 @@ export class ResourceTemplate {
903906
* Callback for a tool handler registered with Server.tool().
904907
*
905908
* Parameters will include tool arguments, if applicable, as well as other request handler context.
909+
*
910+
* The callback should return:
911+
* - `structuredContent` if the tool has an outputSchema defined
912+
* - `content` if the tool does not have an outputSchema
913+
* - Both fields are optional but typically one should be provided
906914
*/
907915
export type ToolCallback<Args extends undefined | ZodRawShape = undefined> =
908916
Args extends ZodRawShape
@@ -915,12 +923,13 @@ export type ToolCallback<Args extends undefined | ZodRawShape = undefined> =
915923
export type RegisteredTool = {
916924
description?: string;
917925
inputSchema?: AnyZodObject;
926+
outputSchema?: Tool["outputSchema"];
918927
annotations?: ToolAnnotations;
919928
callback: ToolCallback<undefined | ZodRawShape>;
920929
enabled: boolean;
921930
enable(): void;
922931
disable(): void;
923-
update<Args extends ZodRawShape>(updates: { name?: string | null, description?: string, paramsSchema?: Args, callback?: ToolCallback<Args>, annotations?: ToolAnnotations, enabled?: boolean }): void
932+
update<Args extends ZodRawShape>(updates: { name?: string | null, description?: string, paramsSchema?: Args, outputSchema?: Tool["outputSchema"], callback?: ToolCallback<Args>, annotations?: ToolAnnotations, enabled?: boolean }): void
924933
remove(): void
925934
};
926935

src/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,10 @@ export const ToolSchema = z
831831
properties: z.optional(z.object({}).passthrough()),
832832
})
833833
.passthrough(),
834+
/**
835+
* A JSON Schema object defining the expected output for the tool.
836+
*/
837+
outputSchema: z.object({type: z.any()}).passthrough().optional(),
834838
/**
835839
* Optional additional tool information.
836840
*/
@@ -858,7 +862,8 @@ export const ListToolsResultSchema = PaginatedResultSchema.extend({
858862
export const CallToolResultSchema = ResultSchema.extend({
859863
content: z.array(
860864
z.union([TextContentSchema, ImageContentSchema, AudioContentSchema, EmbeddedResourceSchema]),
861-
),
865+
).optional(),
866+
structuredContent: z.string().optional(),
862867
isError: z.boolean().default(false).optional(),
863868
});
864869

0 commit comments

Comments
 (0)