Skip to content

Type generics for CallToolResult - Require 'structuredContent' if 'outputSchema' defined. #859

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion src/examples/server/mcpServerOutputSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ 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
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
24 changes: 17 additions & 7 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,
} from "../types.js";
import { Completable, CompletableDef } from "./completable.js";
import { UriTemplate, Variables } from "../shared/uriTemplate.js";
Expand Down Expand Up @@ -920,7 +922,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 @@ -929,7 +931,7 @@ export class McpServer {
outputSchema?: OutputArgs;
annotations?: ToolAnnotations;
},
cb: ToolCallback<InputArgs>
cb: ToolCallback<InputArgs, OutputArgs>
): RegisteredTool {
if (this._registeredTools[name]) {
throw new Error(`Tool ${name} is already registered`);
Expand Down Expand Up @@ -1148,13 +1150,21 @@ 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 | 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>;
) => OutputArgs extends ZodRawShape
? CallToolResultStructured<OutputArgs> | Promise<CallToolResultStructured<OutputArgs>>
: OutputArgs extends undefined
? CallToolResultUnstructured | Promise<CallToolResultUnstructured>
: never
: (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => OutputArgs extends ZodRawShape
? CallToolResultStructured<OutputArgs> | Promise<CallToolResultStructured<OutputArgs>>
: OutputArgs extends undefined
? CallToolResultUnstructured | Promise<CallToolResultUnstructured>
: never;

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 @@ -951,25 +951,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 @@ -987,6 +976,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 @@ -1595,6 +1595,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> & {
structuredContent: z.infer<z.ZodObject<OArgs, 'strip'>>;
}
export type CompatibilityCallToolResult = Infer<typeof CompatibilityCallToolResultSchema>;
export type CallToolRequest = Infer<typeof CallToolRequestSchema>;
export type ToolListChangedNotification = Infer<typeof ToolListChangedNotificationSchema>;
Expand Down