Skip to content

Commit 9533972

Browse files
authored
Merge pull request #670 from sushichan044/feat/structured-content-with-type-safety
fix: add type safety for tool output schemas in ToolCallback
2 parents a1a486d + a40b353 commit 9533972

File tree

3 files changed

+54
-27
lines changed

3 files changed

+54
-27
lines changed

src/examples/server/mcpServerOutputSchema.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,14 @@ 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 conditionCandidates = [
47+
"sunny",
48+
"cloudy",
49+
"rainy",
50+
"stormy",
51+
"snowy",
52+
] as const;
53+
const conditions = conditionCandidates[Math.floor(Math.random() * conditionCandidates.length)];
4754

4855
const structuredContent = {
4956
temperature: {
@@ -77,4 +84,4 @@ async function main() {
7784
main().catch((error) => {
7885
console.error("Server error:", error);
7986
process.exit(1);
80-
});
87+
});

src/server/mcp.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1312,7 +1312,7 @@ describe("tool()", () => {
13121312
resultType: "structured",
13131313
// Missing required 'timestamp' field
13141314
someExtraField: "unexpected" // Extra field not in schema
1315-
},
1315+
} as unknown as { processedInput: string; resultType: string; timestamp: string }, // Type assertion to bypass TypeScript validation for testing purposes
13161316
})
13171317
);
13181318

src/server/mcp.ts

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export class McpServer {
169169
}
170170

171171
const args = parseResult.data;
172-
const cb = tool.callback as ToolCallback<ZodRawShape>;
172+
const cb = tool.callback as ToolCallback<ZodRawShape, ZodRawShape>;
173173
try {
174174
result = await Promise.resolve(cb(args, extra));
175175
} catch (error) {
@@ -184,7 +184,7 @@ export class McpServer {
184184
};
185185
}
186186
} else {
187-
const cb = tool.callback as ToolCallback<undefined>;
187+
const cb = tool.callback as ToolCallback<undefined, ZodRawShape>;
188188
try {
189189
result = await Promise.resolve(cb(extra));
190190
} catch (error) {
@@ -772,7 +772,7 @@ export class McpServer {
772772
inputSchema: ZodRawShape | undefined,
773773
outputSchema: ZodRawShape | undefined,
774774
annotations: ToolAnnotations | undefined,
775-
callback: ToolCallback<ZodRawShape | undefined>
775+
callback: ToolCallback<ZodRawShape | undefined, ZodRawShape | undefined>
776776
): RegisteredTool {
777777
const registeredTool: RegisteredTool = {
778778
title,
@@ -929,7 +929,7 @@ export class McpServer {
929929
outputSchema?: OutputArgs;
930930
annotations?: ToolAnnotations;
931931
},
932-
cb: ToolCallback<InputArgs>
932+
cb: ToolCallback<InputArgs, OutputArgs>
933933
): RegisteredTool {
934934
if (this._registeredTools[name]) {
935935
throw new Error(`Tool ${name} is already registered`);
@@ -944,7 +944,7 @@ export class McpServer {
944944
inputSchema,
945945
outputSchema,
946946
annotations,
947-
cb as ToolCallback<ZodRawShape | undefined>
947+
cb as ToolCallback<ZodRawShape | undefined, ZodRawShape | undefined>
948948
);
949949
}
950950

@@ -1138,6 +1138,16 @@ export class ResourceTemplate {
11381138
}
11391139
}
11401140

1141+
/**
1142+
* Type helper to create a strongly-typed CallToolResult with structuredContent
1143+
*/
1144+
type TypedCallToolResult<OutputArgs extends undefined | ZodRawShape> =
1145+
OutputArgs extends ZodRawShape
1146+
? CallToolResult & {
1147+
structuredContent?: z.objectOutputType<OutputArgs, ZodTypeAny>;
1148+
}
1149+
: CallToolResult;
1150+
11411151
/**
11421152
* Callback for a tool handler registered with Server.tool().
11431153
*
@@ -1148,36 +1158,46 @@ export class ResourceTemplate {
11481158
* - `content` if the tool does not have an outputSchema
11491159
* - Both fields are optional but typically one should be provided
11501160
*/
1151-
export type ToolCallback<Args extends undefined | ZodRawShape = undefined> =
1152-
Args extends ZodRawShape
1161+
export type ToolCallback<
1162+
InputArgs extends undefined | ZodRawShape = undefined,
1163+
OutputArgs extends undefined | ZodRawShape = undefined
1164+
> = InputArgs extends ZodRawShape
11531165
? (
1154-
args: z.objectOutputType<Args, ZodTypeAny>,
1155-
extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
1156-
) => CallToolResult | Promise<CallToolResult>
1157-
: (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResult | Promise<CallToolResult>;
1166+
args: z.objectOutputType<InputArgs, ZodTypeAny>,
1167+
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
1168+
) =>
1169+
| TypedCallToolResult<OutputArgs>
1170+
| Promise<TypedCallToolResult<OutputArgs>>
1171+
: (
1172+
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
1173+
) =>
1174+
| TypedCallToolResult<OutputArgs>
1175+
| Promise<TypedCallToolResult<OutputArgs>>;
11581176

11591177
export type RegisteredTool = {
11601178
title?: string;
11611179
description?: string;
11621180
inputSchema?: AnyZodObject;
11631181
outputSchema?: AnyZodObject;
11641182
annotations?: ToolAnnotations;
1165-
callback: ToolCallback<undefined | ZodRawShape>;
1183+
callback: ToolCallback<ZodRawShape | undefined, ZodRawShape | undefined>;
11661184
enabled: boolean;
11671185
enable(): void;
11681186
disable(): void;
1169-
update<InputArgs extends ZodRawShape, OutputArgs extends ZodRawShape>(
1170-
updates: {
1171-
name?: string | null,
1172-
title?: string,
1173-
description?: string,
1174-
paramsSchema?: InputArgs,
1175-
outputSchema?: OutputArgs,
1176-
annotations?: ToolAnnotations,
1177-
callback?: ToolCallback<InputArgs>,
1178-
enabled?: boolean
1179-
}): void
1180-
remove(): void
1187+
update<
1188+
InputArgs extends ZodRawShape,
1189+
OutputArgs extends ZodRawShape
1190+
>(updates: {
1191+
name?: string | null;
1192+
title?: string;
1193+
description?: string;
1194+
paramsSchema?: InputArgs;
1195+
outputSchema?: OutputArgs;
1196+
annotations?: ToolAnnotations;
1197+
callback?: ToolCallback<InputArgs, OutputArgs>
1198+
enabled?: boolean
1199+
}): void;
1200+
remove(): void;
11811201
};
11821202

11831203
const EMPTY_OBJECT_JSON_SCHEMA = {

0 commit comments

Comments
 (0)