Skip to content

Commit ff653ba

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 <[email protected]>
1 parent 048bc4f commit ff653ba

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
@@ -768,6 +768,104 @@ describe("tool()", () => {
768768
mcpServer.tool("tool2", () => ({ content: [] }));
769769
});
770770

771+
test("should support tool with outputSchema and structuredContent", async () => {
772+
const mcpServer = new McpServer({
773+
name: "test server",
774+
version: "1.0",
775+
});
776+
777+
const client = new Client(
778+
{
779+
name: "test client",
780+
version: "1.0",
781+
},
782+
{
783+
capabilities: {
784+
tools: {},
785+
},
786+
},
787+
);
788+
789+
// Register a tool with outputSchema
790+
const registeredTool = mcpServer.tool(
791+
"test",
792+
"Test tool with structured output",
793+
{
794+
input: z.string(),
795+
},
796+
async ({ input }) => ({
797+
// When outputSchema is defined, return structuredContent instead of content
798+
structuredContent: JSON.stringify({
799+
processedInput: input,
800+
resultType: "structured",
801+
timestamp: "2023-01-01T00:00:00Z"
802+
}),
803+
}),
804+
);
805+
806+
// Update the tool to add outputSchema
807+
registeredTool.update({
808+
outputSchema: {
809+
type: "object",
810+
properties: {
811+
processedInput: { type: "string" },
812+
resultType: { type: "string" },
813+
timestamp: { type: "string", format: "date-time" }
814+
},
815+
required: ["processedInput", "resultType", "timestamp"]
816+
}
817+
});
818+
819+
const [clientTransport, serverTransport] =
820+
InMemoryTransport.createLinkedPair();
821+
822+
await Promise.all([
823+
client.connect(clientTransport),
824+
mcpServer.server.connect(serverTransport),
825+
]);
826+
827+
// Verify the tool registration includes outputSchema
828+
const listResult = await client.request(
829+
{
830+
method: "tools/list",
831+
},
832+
ListToolsResultSchema,
833+
);
834+
835+
expect(listResult.tools).toHaveLength(1);
836+
expect(listResult.tools[0].outputSchema).toEqual({
837+
type: "object",
838+
properties: {
839+
processedInput: { type: "string" },
840+
resultType: { type: "string" },
841+
timestamp: { type: "string", format: "date-time" }
842+
},
843+
required: ["processedInput", "resultType", "timestamp"]
844+
});
845+
846+
// Call the tool and verify it returns structuredContent
847+
const result = await client.request(
848+
{
849+
method: "tools/call",
850+
params: {
851+
name: "test",
852+
arguments: {
853+
input: "hello",
854+
},
855+
},
856+
},
857+
CallToolResultSchema,
858+
);
859+
860+
expect(result.structuredContent).toBeDefined();
861+
expect(result.content).toBeUndefined(); // Should not have content when structuredContent is used
862+
863+
const parsed = JSON.parse(result.structuredContent || "{}");
864+
expect(parsed.processedInput).toBe("hello");
865+
expect(parsed.resultType).toBe("structured");
866+
expect(parsed.timestamp).toBe("2023-01-01T00:00:00Z");
867+
});
868+
771869
test("should pass sessionId to tool callback via RequestHandlerExtra", async () => {
772870
const mcpServer = new McpServer({
773871
name: "test server",
@@ -871,7 +969,7 @@ describe("tool()", () => {
871969

872970
expect(receivedRequestId).toBeDefined();
873971
expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true);
874-
expect(result.content[0].text).toContain("Received request ID:");
972+
expect(result.content && result.content[0].text).toContain("Received request ID:");
875973
});
876974

877975
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
},
@@ -696,6 +697,7 @@ export class McpServer {
696697
description,
697698
inputSchema:
698699
paramsSchema === undefined ? undefined : z.object(paramsSchema),
700+
outputSchema: undefined,
699701
annotations,
700702
callback: cb,
701703
enabled: true,
@@ -709,6 +711,7 @@ export class McpServer {
709711
}
710712
if (typeof updates.description !== "undefined") registeredTool.description = updates.description
711713
if (typeof updates.paramsSchema !== "undefined") registeredTool.inputSchema = z.object(updates.paramsSchema)
714+
if (typeof updates.outputSchema !== "undefined") registeredTool.outputSchema = updates.outputSchema
712715
if (typeof updates.callback !== "undefined") registeredTool.callback = updates.callback
713716
if (typeof updates.annotations !== "undefined") registeredTool.annotations = updates.annotations
714717
if (typeof updates.enabled !== "undefined") registeredTool.enabled = updates.enabled
@@ -896,6 +899,11 @@ export class ResourceTemplate {
896899
* Callback for a tool handler registered with Server.tool().
897900
*
898901
* Parameters will include tool arguments, if applicable, as well as other request handler context.
902+
*
903+
* The callback should return:
904+
* - `structuredContent` if the tool has an outputSchema defined
905+
* - `content` if the tool does not have an outputSchema
906+
* - Both fields are optional but typically one should be provided
899907
*/
900908
export type ToolCallback<Args extends undefined | ZodRawShape = undefined> =
901909
Args extends ZodRawShape
@@ -908,12 +916,13 @@ export type ToolCallback<Args extends undefined | ZodRawShape = undefined> =
908916
export type RegisteredTool = {
909917
description?: string;
910918
inputSchema?: AnyZodObject;
919+
outputSchema?: Tool["outputSchema"];
911920
annotations?: ToolAnnotations;
912921
callback: ToolCallback<undefined | ZodRawShape>;
913922
enabled: boolean;
914923
enable(): void;
915924
disable(): void;
916-
update<Args extends ZodRawShape>(updates: { name?: string | null, description?: string, paramsSchema?: Args, callback?: ToolCallback<Args>, annotations?: ToolAnnotations, enabled?: boolean }): void
925+
update<Args extends ZodRawShape>(updates: { name?: string | null, description?: string, paramsSchema?: Args, outputSchema?: Tool["outputSchema"], callback?: ToolCallback<Args>, annotations?: ToolAnnotations, enabled?: boolean }): void
917926
remove(): void
918927
};
919928

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)