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:
parent
882e0b7d05
commit
bb28098d81
1 changed files with 987 additions and 100 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue