Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/examples/server/mcpServerOutputSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ server.registerTool(
void country;
// Simulate weather API call
const temp_c = Math.round((Math.random() * 35 - 5) * 10) / 10;
const conditions = ['sunny', 'cloudy', 'rainy', 'stormy', 'snowy'][Math.floor(Math.random() * 5)];
const conditions = ['sunny', 'cloudy', 'rainy', 'stormy', 'snowy'][Math.floor(Math.random() * 5)] as
| 'sunny'
| 'cloudy'
| 'rainy'
| 'stormy'
| 'snowy';

const structuredContent = {
temperature: {
Expand Down
38 changes: 20 additions & 18 deletions src/server/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1283,25 +1283,27 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
timestamp: z.string()
}
},
async ({ input }) => ({
content: [
{
type: 'text',
text: JSON.stringify({
processedInput: input,
resultType: 'structured',
// Missing required 'timestamp' field
someExtraField: 'unexpected' // Extra field not in schema
})
async ({ input }) =>
// @ts-expect-error - Intentionally returning invalid data to test runtime validation
({
content: [
{
type: 'text',
text: JSON.stringify({
processedInput: input,
resultType: 'structured',
// Missing required 'timestamp' field
someExtraField: 'unexpected' // Extra field not in schema
})
}
],
structuredContent: {
processedInput: input,
resultType: 'structured',
// Missing required 'timestamp' field
someExtraField: 'unexpected' // Extra field not in schema
}
],
structuredContent: {
processedInput: input,
resultType: 'structured',
// Missing required 'timestamp' field
someExtraField: 'unexpected' // Extra field not in schema
}
})
})
);

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
Expand Down
55 changes: 37 additions & 18 deletions src/server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
Implementation,
Tool,
ListToolsResult,
CallToolResult,
CallToolResult as BaseCallToolResult,
McpError,
ErrorCode,
CompleteResult,
Expand Down Expand Up @@ -56,6 +56,19 @@ import { RequestHandlerExtra } from '../shared/protocol.js';
import { Transport } from '../shared/transport.js';
import { validateAndWarnToolName } from '../shared/toolNameValidation.js';

/**
* A result of a tool call, with structured content typed according to the tool's output schema.
*/
export type CallToolResult<T extends ZodRawShapeCompat | AnySchema | undefined = undefined> = T extends ZodRawShapeCompat
? Omit<BaseCallToolResult, 'structuredContent'> & {
structuredContent?: ShapeOutput<T>;
}
: T extends AnySchema
? Omit<BaseCallToolResult, 'structuredContent'> & {
structuredContent?: SchemaOutput<T>;
}
: BaseCallToolResult;

/**
* High-level MCP server that provides a simpler API for working with resources, tools, and prompts.
* For advanced usage (like sending notifications or setting custom request handlers), use the underlying
Expand Down Expand Up @@ -148,10 +161,10 @@ export class McpServer {
})
);

this.server.setRequestHandler(CallToolRequestSchema, async (request, extra): Promise<CallToolResult> => {
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra): Promise<BaseCallToolResult> => {
const tool = this._registeredTools[request.params.name];

let result: CallToolResult;
let result: BaseCallToolResult;

try {
if (!tool) {
Expand All @@ -178,10 +191,10 @@ export class McpServer {

const args = parseResult.data;

result = await Promise.resolve(cb(args, extra));
result = (await Promise.resolve(cb(args, extra))) as BaseCallToolResult;
} else {
const cb = tool.callback as ToolCallback<undefined>;
result = await Promise.resolve(cb(extra));
result = (await Promise.resolve(cb(extra))) as BaseCallToolResult;
}

if (tool.outputSchema && !result.isError) {
Expand Down Expand Up @@ -223,7 +236,7 @@ export class McpServer {
* @param errorMessage - The error message.
* @returns The tool error result.
*/
private createToolError(errorMessage: string): CallToolResult {
private createToolError(errorMessage: string): BaseCallToolResult {
return {
content: [
{
Expand Down Expand Up @@ -698,7 +711,7 @@ export class McpServer {
outputSchema: ZodRawShapeCompat | AnySchema | undefined,
annotations: ToolAnnotations | undefined,
_meta: Record<string, unknown> | undefined,
callback: ToolCallback<ZodRawShapeCompat | undefined>
callback: ToolCallback<ZodRawShapeCompat | undefined, ZodRawShapeCompat | undefined>
): RegisteredTool {
// Validate tool name according to SEP specification
validateAndWarnToolName(name);
Expand Down Expand Up @@ -867,7 +880,7 @@ export class McpServer {
annotations?: ToolAnnotations;
_meta?: Record<string, unknown>;
},
cb: ToolCallback<InputArgs>
cb: ToolCallback<InputArgs, OutputArgs>
): RegisteredTool {
if (this._registeredTools[name]) {
throw new Error(`Tool ${name} is already registered`);
Expand All @@ -883,7 +896,7 @@ export class McpServer {
outputSchema,
annotations,
_meta,
cb as ToolCallback<ZodRawShapeCompat | undefined>
cb as ToolCallback<ZodRawShapeCompat | undefined, ZodRawShapeCompat | undefined>
);
}

Expand Down Expand Up @@ -1086,14 +1099,20 @@ export class ResourceTemplate {
* - `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 | ZodRawShapeCompat | AnySchema = undefined> = Args extends ZodRawShapeCompat
? (args: ShapeOutput<Args>, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResult | Promise<CallToolResult>
: Args extends AnySchema
export type ToolCallback<
InputArgs extends undefined | ZodRawShapeCompat | AnySchema = undefined,
OutputArgs extends undefined | ZodRawShapeCompat | AnySchema = undefined
> = InputArgs extends ZodRawShapeCompat
? (
args: ShapeOutput<InputArgs>,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
) => CallToolResult<OutputArgs> | Promise<CallToolResult<OutputArgs>>
: InputArgs extends AnySchema
? (
args: SchemaOutput<Args>,
args: SchemaOutput<InputArgs>,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
) => CallToolResult | Promise<CallToolResult>
: (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResult | Promise<CallToolResult>;
) => CallToolResult<OutputArgs> | Promise<CallToolResult<OutputArgs>>
: (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResult<OutputArgs> | Promise<CallToolResult<OutputArgs>>;

export type RegisteredTool = {
title?: string;
Expand All @@ -1102,19 +1121,19 @@ export type RegisteredTool = {
outputSchema?: AnySchema;
annotations?: ToolAnnotations;
_meta?: Record<string, unknown>;
callback: ToolCallback<undefined | ZodRawShapeCompat>;
callback: ToolCallback<undefined | ZodRawShapeCompat, undefined | ZodRawShapeCompat>;
enabled: boolean;
enable(): void;
disable(): void;
update<InputArgs extends ZodRawShapeCompat, OutputArgs extends ZodRawShapeCompat>(updates: {
update<InputArgs extends ZodRawShapeCompat | undefined, OutputArgs extends ZodRawShapeCompat | undefined>(updates: {
name?: string | null;
title?: string;
description?: string;
paramsSchema?: InputArgs;
outputSchema?: OutputArgs;
annotations?: ToolAnnotations;
_meta?: Record<string, unknown>;
callback?: ToolCallback<InputArgs>;
callback?: ToolCallback<InputArgs, OutputArgs>;
enabled?: boolean;
}): void;
remove(): void;
Expand Down
Loading