Skip to content

Commit 85510c2

Browse files
committed
type generics for CallToolResult - type require structuredContent return if outputSchema defined
1 parent a1608a6 commit 85510c2

File tree

4 files changed

+38
-21
lines changed

4 files changed

+38
-21
lines changed

src/examples/server/mcpServerOutputSchema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ server.registerTool(
4343
void country;
4444
// Simulate weather API call
4545
const temp_c = Math.round((Math.random() * 35 - 5) * 10) / 10;
46-
const conditions = ["sunny", "cloudy", "rainy", "stormy", "snowy"][Math.floor(Math.random() * 5)];
46+
const conditions = ["sunny", "cloudy", "rainy", "stormy", "snowy"][Math.floor(Math.random() * 5)] as "sunny" | "cloudy" | "rainy" | "stormy" | "snowy";
4747

4848
const structuredContent = {
4949
temperature: {

src/server/mcp.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,6 +1175,7 @@ describe("tool()", () => {
11751175
resultType: z.string(),
11761176
},
11771177
},
1178+
// @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.
11781179
async ({ input }) => ({
11791180
// Only return content without structuredContent
11801181
content: [
@@ -1230,6 +1231,7 @@ describe("tool()", () => {
12301231
resultType: z.string(),
12311232
},
12321233
},
1234+
// @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.
12331235
async ({ input }) => ({
12341236
content: [
12351237
{
@@ -1295,6 +1297,7 @@ describe("tool()", () => {
12951297
timestamp: z.string()
12961298
},
12971299
},
1300+
// @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.
12981301
async ({ input }) => ({
12991302
content: [
13001303
{

src/server/mcp.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import {
4141
ServerRequest,
4242
ServerNotification,
4343
ToolAnnotations,
44+
CallToolResultUnstructured,
45+
CallToolResultStructured,
4446
} from "../types.js";
4547
import { Completable, CompletableDef } from "./completable.js";
4648
import { UriTemplate, Variables } from "../shared/uriTemplate.js";
@@ -920,7 +922,7 @@ export class McpServer {
920922
/**
921923
* Registers a tool with a config object and callback.
922924
*/
923-
registerTool<InputArgs extends ZodRawShape, OutputArgs extends ZodRawShape>(
925+
registerTool<InputArgs extends ZodRawShape, OutputArgs extends ZodRawShape | undefined>(
924926
name: string,
925927
config: {
926928
title?: string;
@@ -929,7 +931,7 @@ export class McpServer {
929931
outputSchema?: OutputArgs;
930932
annotations?: ToolAnnotations;
931933
},
932-
cb: ToolCallback<InputArgs>
934+
cb: ToolCallback<InputArgs, OutputArgs>
933935
): RegisteredTool {
934936
if (this._registeredTools[name]) {
935937
throw new Error(`Tool ${name} is already registered`);
@@ -1148,13 +1150,21 @@ export class ResourceTemplate {
11481150
* - `content` if the tool does not have an outputSchema
11491151
* - Both fields are optional but typically one should be provided
11501152
*/
1151-
export type ToolCallback<Args extends undefined | ZodRawShape = undefined> =
1152-
Args extends ZodRawShape
1153+
export type ToolCallback<InputArgs extends undefined | ZodRawShape = undefined, OutputArgs extends undefined | ZodRawShape = undefined> =
1154+
InputArgs extends ZodRawShape
11531155
? (
1154-
args: z.objectOutputType<Args, ZodTypeAny>,
1156+
args: z.objectOutputType<InputArgs, ZodTypeAny>,
11551157
extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
1156-
) => CallToolResult | Promise<CallToolResult>
1157-
: (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResult | Promise<CallToolResult>;
1158+
) => OutputArgs extends ZodRawShape
1159+
? CallToolResultStructured<OutputArgs> | Promise<CallToolResultStructured<OutputArgs>>
1160+
: OutputArgs extends undefined
1161+
? CallToolResultUnstructured | Promise<CallToolResultUnstructured>
1162+
: never
1163+
: (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => OutputArgs extends ZodRawShape
1164+
? CallToolResultStructured<OutputArgs> | Promise<CallToolResultStructured<OutputArgs>>
1165+
: OutputArgs extends undefined
1166+
? CallToolResultUnstructured | Promise<CallToolResultUnstructured>
1167+
: never;
11581168

11591169
export type RegisteredTool = {
11601170
title?: string;

src/types.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { z, ZodTypeAny } from "zod";
1+
import { z, ZodRawShape, ZodTypeAny } from "zod";
22
import { AuthInfo } from "./server/auth/types.js";
33

44
export const LATEST_PROTOCOL_VERSION = "2025-06-18";
@@ -951,25 +951,14 @@ export const ListToolsResultSchema = PaginatedResultSchema.extend({
951951
tools: z.array(ToolSchema),
952952
});
953953

954-
/**
955-
* The server's response to a tool call.
956-
*/
957-
export const CallToolResultSchema = ResultSchema.extend({
954+
export const CallToolResultUnstructuredSchema = ResultSchema.extend({
958955
/**
959956
* A list of content objects that represent the result of the tool call.
960957
*
961958
* If the Tool does not define an outputSchema, this field MUST be present in the result.
962959
* For backwards compatibility, this field is always present, but it may be empty.
963960
*/
964961
content: z.array(ContentBlockSchema).default([]),
965-
966-
/**
967-
* An object containing structured tool output.
968-
*
969-
* If the Tool defines an outputSchema, this field MUST be present in the result, and contain a JSON object that matches the schema.
970-
*/
971-
structuredContent: z.object({}).passthrough().optional(),
972-
973962
/**
974963
* Whether the tool call ended in an error.
975964
*
@@ -987,6 +976,17 @@ export const CallToolResultSchema = ResultSchema.extend({
987976
isError: z.optional(z.boolean()),
988977
});
989978

979+
export const CallToolResultStructuredSchema = CallToolResultUnstructuredSchema.extend({
980+
/**
981+
* An object containing structured tool output.
982+
*
983+
* If the Tool defines an outputSchema, this field MUST be present in the result, and contain a JSON object that matches the schema.
984+
*/
985+
structuredContent: z.object({}).passthrough().optional(),
986+
});
987+
988+
export const CallToolResultSchema = z.union([CallToolResultUnstructuredSchema, CallToolResultStructuredSchema]);
989+
990990
/**
991991
* CallToolResultSchema extended with backwards compatibility to protocol version 2024-10-07.
992992
*/
@@ -1595,6 +1595,10 @@ export type Tool = Infer<typeof ToolSchema>;
15951595
export type ListToolsRequest = Infer<typeof ListToolsRequestSchema>;
15961596
export type ListToolsResult = Infer<typeof ListToolsResultSchema>;
15971597
export type CallToolResult = Infer<typeof CallToolResultSchema>;
1598+
export type CallToolResultUnstructured = Infer<typeof CallToolResultUnstructuredSchema>;
1599+
export type CallToolResultStructured<OArgs extends ZodRawShape> = Infer<typeof CallToolResultStructuredSchema> & {
1600+
structuredContent: z.infer<z.ZodObject<OArgs, 'strip'>>;
1601+
}
15981602
export type CompatibilityCallToolResult = Infer<typeof CompatibilityCallToolResultSchema>;
15991603
export type CallToolRequest = Infer<typeof CallToolRequestSchema>;
16001604
export type ToolListChangedNotification = Infer<typeof ToolListChangedNotificationSchema>;

0 commit comments

Comments
 (0)