Skip to content
Open
15 changes: 10 additions & 5 deletions src/examples/server/mcpServerOutputSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const server = new McpServer(
}
);

const weatherConditions = ["sunny", "cloudy", "rainy", "stormy", "snowy"] as const;
const windDirections = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] as const;

// Define a tool with structured output - Weather data
server.registerTool(
"get_weather",
Expand All @@ -29,11 +32,11 @@ server.registerTool(
celsius: z.number(),
fahrenheit: z.number()
}),
conditions: z.enum(["sunny", "cloudy", "rainy", "stormy", "snowy"]),
conditions: z.enum(weatherConditions),
humidity: z.number().min(0).max(100),
wind: z.object({
speed_kmh: z.number(),
direction: z.string()
direction: z.enum(windDirections)
})
},
},
Expand All @@ -43,7 +46,8 @@ 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 =
weatherConditions[Math.floor(Math.random() * weatherConditions.length)];

const structuredContent = {
temperature: {
Expand All @@ -54,7 +58,8 @@ server.registerTool(
humidity: Math.round(Math.random() * 100),
wind: {
speed_kmh: Math.round(Math.random() * 50),
direction: ["N", "NE", "E", "SE", "S", "SW", "W", "NW"][Math.floor(Math.random() * 8)]
direction:
windDirections[Math.floor(Math.random() * windDirections.length)]
}
};

Expand All @@ -77,4 +82,4 @@ async function main() {
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});
});
3 changes: 3 additions & 0 deletions src/server/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,7 @@ describe("tool()", () => {
resultType: z.string(),
},
},
// @ts-expect-error - This is a test - we are not providing structuredContent. The type system is not able to infer the correct type, so we need ts-expect-error for the test.
async ({ input }) => ({
// Only return content without structuredContent
content: [
Expand Down Expand Up @@ -1230,6 +1231,7 @@ describe("tool()", () => {
resultType: z.string(),
},
},
// @ts-expect-error - This is a test - we are not providing structuredContent. The type system is not able to infer the correct type, so we need ts-expect-error for the test.
async ({ input }) => ({
content: [
{
Expand Down Expand Up @@ -1295,6 +1297,7 @@ describe("tool()", () => {
timestamp: z.string()
},
},
// @ts-expect-error - This is a test - we are not providing structuredContent. The type system is not able to infer the correct type, so we need ts-expect-error for the test.
async ({ input }) => ({
content: [
{
Expand Down
30 changes: 21 additions & 9 deletions src/server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import {
ServerRequest,
ServerNotification,
ToolAnnotations,
CallToolResultUnstructured,
CallToolResultStructured,
LoggingMessageNotification,
} from "../types.js";
import { Completable, CompletableDef } from "./completable.js";
Expand Down Expand Up @@ -925,7 +927,7 @@ export class McpServer {
/**
* Registers a tool with a config object and callback.
*/
registerTool<InputArgs extends ZodRawShape, OutputArgs extends ZodRawShape>(
registerTool<InputArgs extends ZodRawShape, OutputArgs extends ZodRawShape | undefined>(
name: string,
config: {
title?: string;
Expand All @@ -935,7 +937,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 Down Expand Up @@ -1161,17 +1163,27 @@ export class ResourceTemplate {
* Parameters will include tool arguments, if applicable, as well as other request handler context.
*
* The callback should return:
* - `structuredContent` if the tool has an outputSchema defined
* - `content` if the tool does not have an outputSchema
* - `structuredContent` if the tool has an outputSchema defined.
* - `content` if the tool does not have an outputSchema OR if an outputSchema is defined, content *SHOULD* have the serialized JSON structuredContent in a text content for backwards compatibility
* - Both fields are optional but typically one should be provided
*/
export type ToolCallback<Args extends undefined | ZodRawShape = undefined> =
Args extends ZodRawShape
export type ToolCallback<InputArgs extends undefined | ZodRawShape = undefined, OutputArgs extends undefined | ZodRawShape = undefined> =
InputArgs extends ZodRawShape
? (
args: z.objectOutputType<Args, ZodTypeAny>,
args: z.objectOutputType<InputArgs, ZodTypeAny>,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
) => CallToolResult | Promise<CallToolResult>
: (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResult | Promise<CallToolResult>;
) => CallToolResultByOutputArgsType<OutputArgs>
: (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResultByOutputArgsType<OutputArgs>;

/**
* CallToolResult type generated based on OutputArgs.
*/
export type CallToolResultByOutputArgsType<OutputArgs extends undefined | ZodRawShape = undefined> =
OutputArgs extends ZodRawShape
? CallToolResultStructured<OutputArgs> | Promise<CallToolResultStructured<OutputArgs>>
: OutputArgs extends undefined
? CallToolResultUnstructured | Promise<CallToolResultUnstructured>
: never;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we'd never reach this never given the bounds on OutputArgs?


export type RegisteredTool = {
title?: string;
Expand Down
30 changes: 17 additions & 13 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z, ZodTypeAny } from "zod";
import { z, ZodRawShape, ZodTypeAny } from "zod";
import { AuthInfo } from "./server/auth/types.js";

export const LATEST_PROTOCOL_VERSION = "2025-06-18";
Expand Down Expand Up @@ -1000,25 +1000,14 @@ export const ListToolsResultSchema = PaginatedResultSchema.extend({
tools: z.array(ToolSchema),
});

/**
* The server's response to a tool call.
*/
export const CallToolResultSchema = ResultSchema.extend({
export const CallToolResultUnstructuredSchema = ResultSchema.extend({
/**
* A list of content objects that represent the result of the tool call.
*
* If the Tool does not define an outputSchema, this field MUST be present in the result.
* For backwards compatibility, this field is always present, but it may be empty.
*/
content: z.array(ContentBlockSchema).default([]),

/**
* An object containing structured tool output.
*
* If the Tool defines an outputSchema, this field MUST be present in the result, and contain a JSON object that matches the schema.
*/
structuredContent: z.object({}).passthrough().optional(),

/**
* Whether the tool call ended in an error.
*
Expand All @@ -1036,6 +1025,17 @@ export const CallToolResultSchema = ResultSchema.extend({
isError: z.optional(z.boolean()),
});

export const CallToolResultStructuredSchema = CallToolResultUnstructuredSchema.extend({
/**
* An object containing structured tool output.
*
* If the Tool defines an outputSchema, this field MUST be present in the result, and contain a JSON object that matches the schema.
*/
structuredContent: z.object({}).passthrough().optional(),
});

export const CallToolResultSchema = z.union([CallToolResultUnstructuredSchema, CallToolResultStructuredSchema]);

/**
* CallToolResultSchema extended with backwards compatibility to protocol version 2024-10-07.
*/
Expand Down Expand Up @@ -1645,6 +1645,10 @@ export type Tool = Infer<typeof ToolSchema>;
export type ListToolsRequest = Infer<typeof ListToolsRequestSchema>;
export type ListToolsResult = Infer<typeof ListToolsResultSchema>;
export type CallToolResult = Infer<typeof CallToolResultSchema>;
export type CallToolResultUnstructured = Infer<typeof CallToolResultUnstructuredSchema>;
export type CallToolResultStructured<OArgs extends ZodRawShape> = Infer<typeof CallToolResultStructuredSchema> & {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you keep the unabbreviated name OutputArgs used elsewhere for consistency? (although arguably these should be InputType, OutputType, the args concept is redundant w/ input and at odds w/ outputs)

structuredContent: z.infer<z.ZodObject<OArgs>>;
}
export type CompatibilityCallToolResult = Infer<typeof CompatibilityCallToolResultSchema>;
export type CallToolRequest = Infer<typeof CallToolRequestSchema>;
export type ToolListChangedNotification = Infer<typeof ToolListChangedNotificationSchema>;
Expand Down