Skip to content
Open
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
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: could you update this comment? (cf. https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content)

- `content`: if an outputSchema is defined, content *SHOULD* have the serialized JSON structuredContent in a text content for backwards compatibility

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

* - 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
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you extract the return type to avoid this repetition?

export type ToolCallback<InputArgs extends undefined | ZodRawShape = undefined, OutputArgs extends undefined | ZodRawShape = undefined> =
  InputArgs extends ZodRawShape
  ? (args: z.objectOutputType<InputArgs, ZodTypeAny>, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResult<OutputArgs>
  : (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResult<OutputArgs>;

type CallToolResult<OutputArgs extends undefined | ZodRawShape = undefined> =
  OutputArgs extends ZodRawShape
    ? CallToolResultStructured<OutputArgs> | Promise<CallToolResultStructured<OutputArgs>>
    : OutputArgs extends undefined
    ? CallToolResultUnstructured | Promise<CallToolResultUnstructured>
    : never;

Copy link
Contributor Author

@KKonstantinov KKonstantinov Sep 29, 2025

Choose a reason for hiding this comment

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

Done. Have called it "CallToolResultByOutputArgsType" for a lack of a better name (open to suggestions..).

CallToolResult is already used and imported - that is the union type between structured and unstructured.

Unfortunately I don't think we can make CallToolResult generic in the core types.ts (although I very much wish we could) because it then won't match the non-generic type defined in https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-06-18/schema.ts

The reality is the CallToolResult in https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-06-18/schema.ts NOT being generic is a bit misleading, because it implies returning structuredContent even when there is no output schema defined.

? 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> & {
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, 'strip'>>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure what 'strip' adds here? (the actual runtime zod schema won't strip extra fields)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have removed it, frankly I don't recall why I had added it at the time of this PR, might have been related to the removal of passthrough from runtime but yes, just doing it at compile time doesn't help, removed.

}
export type CompatibilityCallToolResult = Infer<typeof CompatibilityCallToolResultSchema>;
export type CallToolRequest = Infer<typeof CallToolRequestSchema>;
export type ToolListChangedNotification = Infer<typeof ToolListChangedNotificationSchema>;
Expand Down