feat: add server-side support for tool outputSchema with backward compatibility

- Update McpServer to support registering tools with outputSchema
- Add automatic content generation from structuredContent for backward compatibility
- Add validation to ensure proper usage of structuredContent vs content
- Add comprehensive tests for outputSchema functionality
- Add example servers demonstrating structured output usage
- Update existing test to match new backward compatibility behavior

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Basil Hosmer 2025-05-07 18:51:13 -04:00
commit bb28098d81
No known key found for this signature in database

View file

@ -111,7 +111,7 @@ export class McpServer {
([, tool]) => tool.enabled,
).map(
([name, tool]): Tool => {
return {
const toolDefinition: Tool = {
name,
description: tool.description,
inputSchema: tool.inputSchema
@ -119,9 +119,15 @@ export class McpServer {
strictUnions: true,
}) as Tool["inputSchema"])
: EMPTY_OBJECT_JSON_SCHEMA,
outputSchema: tool.outputSchema,
annotations: tool.annotations,
};
// Only include outputSchema if it's defined
if (tool.outputSchema) {
toolDefinition.outputSchema = tool.outputSchema;
}
return toolDefinition;
},
),
}),
@ -145,6 +151,8 @@ export class McpServer {
);
}
let result: CallToolResult;
if (tool.inputSchema) {
const parseResult = await tool.inputSchema.safeParseAsync(
request.params.arguments,
@ -159,9 +167,9 @@ export class McpServer {
const args = parseResult.data;
const cb = tool.callback as ToolCallback<ZodRawShape>;
try {
return await Promise.resolve(cb(args, extra));
result = await Promise.resolve(cb(args, extra));
} catch (error) {
return {
result = {
content: [
{
type: "text",
@ -174,9 +182,9 @@ export class McpServer {
} else {
const cb = tool.callback as ToolCallback<undefined>;
try {
return await Promise.resolve(cb(extra));
result = await Promise.resolve(cb(extra));
} catch (error) {
return {
result = {
content: [
{
type: "text",
@ -187,6 +195,46 @@ export class McpServer {
};
}
}
// Handle structured output and backward compatibility
if (tool.outputSchema) {
// Tool has outputSchema, so result must have structuredContent (unless it's an error)
if (!result.structuredContent && !result.isError) {
throw new McpError(
ErrorCode.InternalError,
`Tool ${request.params.name} has outputSchema but returned no structuredContent`,
);
}
// For backward compatibility, if structuredContent is provided but no content,
// automatically serialize the structured content to text
if (result.structuredContent && !result.content) {
result.content = [
{
type: "text",
text: JSON.stringify(result.structuredContent, null, 2),
},
];
}
} else {
// Tool has no outputSchema
if (result.structuredContent) {
throw new McpError(
ErrorCode.InternalError,
`Tool ${request.params.name} has no outputSchema but returned structuredContent`,
);
}
// Tool must have content if no outputSchema
if (!result.content && !result.isError) {
throw new McpError(
ErrorCode.InternalError,
`Tool ${request.params.name} has no outputSchema and must return content`,
);
}
}
return result;
},
);
@ -656,6 +704,50 @@ export class McpServer {
cb: ToolCallback<Args>,
): RegisteredTool;
/**
* Registers a tool with output schema.
*/
tool<Args extends ZodRawShape>(
name: string,
paramsSchema: Args,
outputSchema: Tool["outputSchema"],
cb: ToolCallback<Args>,
): RegisteredTool;
/**
* Registers a tool with description and output schema.
*/
tool<Args extends ZodRawShape>(
name: string,
description: string,
paramsSchema: Args,
outputSchema: Tool["outputSchema"],
cb: ToolCallback<Args>,
): RegisteredTool;
/**
* Registers a tool with parameter schema, output schema, and annotations.
*/
tool<Args extends ZodRawShape>(
name: string,
paramsSchema: Args,
outputSchema: Tool["outputSchema"],
annotations: ToolAnnotations,
cb: ToolCallback<Args>,
): RegisteredTool;
/**
* Registers a tool with description, parameter schema, output schema, and annotations.
*/
tool<Args extends ZodRawShape>(
name: string,
description: string,
paramsSchema: Args,
outputSchema: Tool["outputSchema"],
annotations: ToolAnnotations,
cb: ToolCallback<Args>,
): RegisteredTool;
tool(name: string, ...rest: unknown[]): RegisteredTool {
if (this._registeredTools[name]) {
throw new Error(`Tool ${name} is already registered`);
@ -667,6 +759,7 @@ export class McpServer {
}
let paramsSchema: ZodRawShape | undefined;
let outputSchema: Tool["outputSchema"] | undefined;
let annotations: ToolAnnotations | undefined;
// Handle the different overload combinations
@ -678,17 +771,36 @@ export class McpServer {
// We have a params schema as the first arg
paramsSchema = rest.shift() as ZodRawShape;
// Check if the next arg is potentially annotations
if (rest.length > 1 && typeof rest[0] === "object" && rest[0] !== null && !(isZodRawShape(rest[0]))) {
// Case: tool(name, paramsSchema, annotations, cb)
// Or: tool(name, description, paramsSchema, annotations, cb)
annotations = rest.shift() as ToolAnnotations;
// Check if the next arg is potentially annotations or outputSchema
if (rest.length > 1 && typeof rest[0] === "object" && rest[0] !== null) {
const nextArg = rest[0];
// Check if it's a JSON Schema (outputSchema)
if (typeof nextArg === "object" && "type" in nextArg) {
outputSchema = rest.shift() as Tool["outputSchema"];
// Check if there's still an annotations object
if (rest.length > 1 && typeof rest[0] === "object" && rest[0] !== null && !(isZodRawShape(rest[0]))) {
annotations = rest.shift() as ToolAnnotations;
}
} else if (!(isZodRawShape(nextArg))) {
// It's annotations
annotations = rest.shift() as ToolAnnotations;
}
}
} else if (typeof firstArg === "object" && firstArg !== null) {
// Not a ZodRawShape, so must be annotations in this position
// Case: tool(name, annotations, cb)
// Or: tool(name, description, annotations, cb)
annotations = rest.shift() as ToolAnnotations;
// Check if it's a JSON Schema (outputSchema)
if ("type" in firstArg) {
outputSchema = rest.shift() as Tool["outputSchema"];
// Check if there's still an annotations object
if (rest.length > 1 && typeof rest[0] === "object" && rest[0] !== null && !(isZodRawShape(rest[0]))) {
annotations = rest.shift() as ToolAnnotations;
}
} else {
// It's annotations
annotations = rest.shift() as ToolAnnotations;
}
}
}
@ -697,7 +809,7 @@ export class McpServer {
description,
inputSchema:
paramsSchema === undefined ? undefined : z.object(paramsSchema),
outputSchema: undefined,
outputSchema,
annotations,
callback: cb,
enabled: true,