Skip to content

Commit 84fb308

Browse files
committed
fix: apply same type: object default to outputSchema, add tests
Address review feedback: - outputSchema has the same exposure as inputSchema when using z.discriminatedUnion(). Apply the same fix. - Add tests for both inputSchema and outputSchema with discriminated unions, verifying type: object and oneOf are both present.
1 parent b0ee18c commit 84fb308

File tree

4 files changed

+215
-82
lines changed

4 files changed

+215
-82
lines changed

.changeset/fix-discriminated-union-schema.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"@modelcontextprotocol/server": patch
33
---
44

5-
fix: ensure tool inputSchema includes type: object for discriminated unions
5+
fix: ensure tool inputSchema and outputSchema include type: object for discriminated unions

package.json

Lines changed: 136 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,140 @@
11
{
2-
"name": "@modelcontextprotocol/sdk",
3-
"private": true,
4-
"version": "2.0.0-alpha.0",
5-
"description": "Model Context Protocol implementation for TypeScript",
6-
"license": "SEE LICENSE IN LICENSE",
7-
"author": "Model Context Protocol a Series of LF Projects, LLC.",
8-
"homepage": "https://modelcontextprotocol.io",
9-
"bugs": "https://github.com/modelcontextprotocol/typescript-sdk/issues",
10-
"type": "module",
11-
"repository": {
12-
"type": "git",
13-
"url": "git+https://github.com/modelcontextprotocol/typescript-sdk.git"
14-
},
15-
"engines": {
16-
"node": ">=20"
17-
},
18-
"packageManager": "pnpm@10.26.1",
19-
"keywords": [
20-
"modelcontextprotocol",
21-
"mcp"
2+
"name": "@modelcontextprotocol/sdk",
3+
"private": true,
4+
"version": "2.0.0-alpha.0",
5+
"description": "Model Context Protocol implementation for TypeScript",
6+
"license": "SEE LICENSE IN LICENSE",
7+
"author": "Model Context Protocol a Series of LF Projects, LLC.",
8+
"homepage": "https://modelcontextprotocol.io",
9+
"bugs": "https://github.com/modelcontextprotocol/typescript-sdk/issues",
10+
"type": "module",
11+
"repository": {
12+
"type": "git",
13+
"url": "git+https://github.com/modelcontextprotocol/typescript-sdk.git"
14+
},
15+
"engines": {
16+
"node": ">=20"
17+
},
18+
"packageManager": "pnpm@10.26.1",
19+
"keywords": [
20+
"modelcontextprotocol",
21+
"mcp"
22+
],
23+
"scripts": {
24+
"fetch:spec-types": "tsx scripts/fetch-spec-types.ts",
25+
"sync:snippets": "tsx scripts/sync-snippets.ts",
26+
"examples:simple-server:w": "pnpm --filter @modelcontextprotocol/examples-server exec tsx --watch src/simpleStreamableHttp.ts --oauth",
27+
"docs": "typedoc",
28+
"docs:multi": "bash scripts/generate-multidoc.sh",
29+
"docs:check": "typedoc",
30+
"typecheck:all": "pnpm -r typecheck",
31+
"build:all": "pnpm -r build",
32+
"prepack:all": "pnpm -r prepack",
33+
"lint:all": "pnpm sync:snippets --check && pnpm -r lint",
34+
"lint:fix:all": "pnpm sync:snippets && pnpm -r lint:fix",
35+
"check:all": "pnpm -r typecheck && pnpm -r lint && pnpm run docs:check",
36+
"test:all": "pnpm -r test",
37+
"test:conformance:client": "pnpm --filter @modelcontextprotocol/test-conformance run test:conformance:client",
38+
"test:conformance:client:all": "pnpm --filter @modelcontextprotocol/test-conformance run test:conformance:client:all",
39+
"test:conformance:client:run": "pnpm --filter @modelcontextprotocol/test-conformance run test:conformance:client:run",
40+
"test:conformance:server": "pnpm --filter @modelcontextprotocol/test-conformance run test:conformance:server",
41+
"test:conformance:server:all": "pnpm --filter @modelcontextprotocol/test-conformance run test:conformance:server:all",
42+
"test:conformance:server:run": "pnpm --filter @modelcontextprotocol/test-conformance run test:conformance:server:run",
43+
"test:conformance:all": "pnpm run test:conformance:client:all && pnpm run test:conformance:server:all"
44+
},
45+
"devDependencies": {
46+
"@cfworker/json-schema": "catalog:runtimeShared",
47+
"@changesets/changelog-github": "^0.5.2",
48+
"@changesets/cli": "^2.29.8",
49+
"@eslint/js": "catalog:devTools",
50+
"@modelcontextprotocol/client": "workspace:^",
51+
"@modelcontextprotocol/server": "workspace:^",
52+
"@modelcontextprotocol/node": "workspace:^",
53+
"@types/content-type": "catalog:devTools",
54+
"@types/cors": "catalog:devTools",
55+
"@types/cross-spawn": "catalog:devTools",
56+
"@types/eventsource": "catalog:devTools",
57+
"@types/express": "catalog:devTools",
58+
"@types/express-serve-static-core": "catalog:devTools",
59+
"@types/node": "^24.10.1",
60+
"@types/supertest": "catalog:devTools",
61+
"@types/ws": "catalog:devTools",
62+
"@typescript/native-preview": "catalog:devTools",
63+
"eslint": "catalog:devTools",
64+
"eslint-config-prettier": "catalog:devTools",
65+
"eslint-plugin-n": "catalog:devTools",
66+
"fast-glob": "^3.3.3",
67+
"prettier": "catalog:devTools",
68+
"supertest": "catalog:devTools",
69+
"tsdown": "catalog:devTools",
70+
"tslib": "^2.8.1",
71+
"tsx": "catalog:devTools",
72+
"typedoc": "catalog:devTools",
73+
"typescript": "catalog:devTools",
74+
"typescript-eslint": "catalog:devTools",
75+
"vitest": "catalog:devTools",
76+
"ws": "catalog:devTools",
77+
"zod": "catalog:runtimeShared"
78+
},
79+
"resolutions": {
80+
"strip-ansi": "6.0.1"
81+
},
82+
"workspaces": {
83+
"packages": [
84+
"packages/**/*",
85+
"common/**/*",
86+
"examples/**/*",
87+
"test/**/*"
2288
],
23-
"scripts": {
24-
"fetch:spec-types": "tsx scripts/fetch-spec-types.ts",
25-
"sync:snippets": "tsx scripts/sync-snippets.ts",
26-
"examples:simple-server:w": "pnpm --filter @modelcontextprotocol/examples-server exec tsx --watch src/simpleStreamableHttp.ts --oauth",
27-
"docs": "typedoc",
28-
"docs:multi": "bash scripts/generate-multidoc.sh",
29-
"docs:check": "typedoc",
30-
"typecheck:all": "pnpm -r typecheck",
31-
"build:all": "pnpm -r build",
32-
"prepack:all": "pnpm -r prepack",
33-
"lint:all": "pnpm sync:snippets --check && pnpm -r lint",
34-
"lint:fix:all": "pnpm sync:snippets && pnpm -r lint:fix",
35-
"check:all": "pnpm -r typecheck && pnpm -r lint && pnpm run docs:check",
36-
"test:all": "pnpm -r test",
37-
"test:conformance:client": "pnpm --filter @modelcontextprotocol/test-conformance run test:conformance:client",
38-
"test:conformance:client:all": "pnpm --filter @modelcontextprotocol/test-conformance run test:conformance:client:all",
39-
"test:conformance:client:run": "pnpm --filter @modelcontextprotocol/test-conformance run test:conformance:client:run",
40-
"test:conformance:server": "pnpm --filter @modelcontextprotocol/test-conformance run test:conformance:server",
41-
"test:conformance:server:all": "pnpm --filter @modelcontextprotocol/test-conformance run test:conformance:server:all",
42-
"test:conformance:server:run": "pnpm --filter @modelcontextprotocol/test-conformance run test:conformance:server:run",
43-
"test:conformance:all": "pnpm run test:conformance:client:all && pnpm run test:conformance:server:all"
44-
},
45-
"devDependencies": {
46-
"@cfworker/json-schema": "catalog:runtimeShared",
47-
"@changesets/changelog-github": "^0.5.2",
48-
"@changesets/cli": "^2.29.8",
49-
"@eslint/js": "catalog:devTools",
50-
"@modelcontextprotocol/client": "workspace:^",
51-
"@modelcontextprotocol/server": "workspace:^",
52-
"@modelcontextprotocol/node": "workspace:^",
53-
"@types/content-type": "catalog:devTools",
54-
"@types/cors": "catalog:devTools",
55-
"@types/cross-spawn": "catalog:devTools",
56-
"@types/eventsource": "catalog:devTools",
57-
"@types/express": "catalog:devTools",
58-
"@types/express-serve-static-core": "catalog:devTools",
59-
"@types/node": "^24.10.1",
60-
"@types/supertest": "catalog:devTools",
61-
"@types/ws": "catalog:devTools",
62-
"@typescript/native-preview": "catalog:devTools",
63-
"eslint": "catalog:devTools",
64-
"eslint-config-prettier": "catalog:devTools",
65-
"eslint-plugin-n": "catalog:devTools",
66-
"fast-glob": "^3.3.3",
67-
"prettier": "catalog:devTools",
68-
"supertest": "catalog:devTools",
69-
"tsdown": "catalog:devTools",
70-
"tslib": "^2.8.1",
71-
"tsx": "catalog:devTools",
72-
"typedoc": "catalog:devTools",
73-
"typescript": "catalog:devTools",
74-
"typescript-eslint": "catalog:devTools",
75-
"vitest": "catalog:devTools",
76-
"ws": "catalog:devTools",
77-
"zod": "catalog:runtimeShared"
78-
},
79-
"resolutions": {
80-
"strip-ansi": "6.0.1"
89+
"catalogs": {
90+
"devTools": {
91+
"@eslint/js": "^9.39.2",
92+
"wrangler": "^4.14.4",
93+
"@types/content-type": "^1.1.8",
94+
"@types/cors": "^2.8.17",
95+
"@types/cross-spawn": "^6.0.6",
96+
"@types/eventsource": "^1.1.15",
97+
"@types/express": "^5.0.6",
98+
"@types/express-serve-static-core": "^5.1.0",
99+
"@types/supertest": "^6.0.2",
100+
"@types/ws": "^8.5.12",
101+
"@typescript/native-preview": "^7.0.0-dev.20251217.1",
102+
"eslint": "^9.39.2",
103+
"eslint-config-prettier": "^10.1.8",
104+
"eslint-plugin-n": "^17.23.1",
105+
"prettier": "3.6.2",
106+
"supertest": "^7.0.0",
107+
"tsdown": "^0.18.0",
108+
"typedoc": "^0.28.14",
109+
"tsx": "^4.16.5",
110+
"typescript": "^5.9.3",
111+
"typescript-eslint": "^8.48.1",
112+
"vite-tsconfig-paths": "^5.1.4",
113+
"vitest": "^4.0.15",
114+
"ws": "^8.18.0"
115+
},
116+
"runtimeClientOnly": {
117+
"cross-spawn": "^7.0.5",
118+
"eventsource": "^3.0.2",
119+
"eventsource-parser": "^3.0.0",
120+
"jose": "^6.1.3"
121+
},
122+
"runtimeServerOnly": {
123+
"@hono/node-server": "^1.19.9",
124+
"content-type": "^1.0.5",
125+
"cors": "^2.8.5",
126+
"express": "^5.2.1",
127+
"hono": "^4.11.4",
128+
"raw-body": "^3.0.0"
129+
},
130+
"runtimeShared": {
131+
"@cfworker/json-schema": "^4.1.1",
132+
"ajv": "^8.17.1",
133+
"ajv-formats": "^3.0.1",
134+
"json-schema-typed": "^8.0.2",
135+
"pkce-challenge": "^5.0.0",
136+
"zod": "^4.0"
137+
}
81138
}
139+
}
82140
}

packages/server/src/server/mcp.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,10 @@ export class McpServer {
155155
};
156156

157157
if (tool.outputSchema) {
158-
toolDefinition.outputSchema = schemaToJson(tool.outputSchema, {
159-
io: 'output'
160-
}) as Tool['outputSchema'];
158+
toolDefinition.outputSchema = {
159+
type: 'object' as const,
160+
...schemaToJson(tool.outputSchema, { io: 'output' })
161+
} as Tool['outputSchema'];
161162
}
162163

163164
return toolDefinition;

test/integration/test/server/mcp.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1965,6 +1965,80 @@ describe('Zod v4', () => {
19651965
expect(result.tools[0]!._meta).toBeUndefined();
19661966
});
19671967

1968+
test('should include type: object in inputSchema for discriminated union schemas', async () => {
1969+
const mcpServer = new McpServer({
1970+
name: 'test server',
1971+
version: '1.0'
1972+
});
1973+
const client = new Client({
1974+
name: 'test client',
1975+
version: '1.0'
1976+
});
1977+
1978+
const schema = z.discriminatedUnion('action', [
1979+
z.object({ action: z.literal('create'), name: z.string() }),
1980+
z.object({ action: z.literal('delete'), id: z.number() })
1981+
]);
1982+
1983+
mcpServer.registerTool(
1984+
'discriminated-tool',
1985+
{
1986+
description: 'A tool with discriminated union input',
1987+
inputSchema: schema
1988+
},
1989+
async args => ({
1990+
content: [{ type: 'text', text: JSON.stringify(args) }]
1991+
})
1992+
);
1993+
1994+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
1995+
await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]);
1996+
1997+
const result = await client.request({ method: 'tools/list' });
1998+
1999+
expect(result.tools).toHaveLength(1);
2000+
expect(result.tools[0]!.inputSchema).toHaveProperty('type', 'object');
2001+
expect(result.tools[0]!.inputSchema).toHaveProperty('oneOf');
2002+
});
2003+
2004+
test('should include type: object in outputSchema for discriminated union schemas', async () => {
2005+
const mcpServer = new McpServer({
2006+
name: 'test server',
2007+
version: '1.0'
2008+
});
2009+
const client = new Client({
2010+
name: 'test client',
2011+
version: '1.0'
2012+
});
2013+
2014+
const outputSchema = z.discriminatedUnion('status', [
2015+
z.object({ status: z.literal('ok'), data: z.string() }),
2016+
z.object({ status: z.literal('error'), message: z.string() })
2017+
]);
2018+
2019+
mcpServer.registerTool(
2020+
'output-union-tool',
2021+
{
2022+
description: 'A tool with discriminated union output',
2023+
inputSchema: z.object({ query: z.string() }),
2024+
outputSchema: outputSchema
2025+
},
2026+
async ({ query }) => ({
2027+
content: [{ type: 'text', text: query }],
2028+
structuredContent: { status: 'ok' as const, data: query }
2029+
})
2030+
);
2031+
2032+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
2033+
await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]);
2034+
2035+
const result = await client.request({ method: 'tools/list' });
2036+
2037+
expect(result.tools).toHaveLength(1);
2038+
expect(result.tools[0]!.outputSchema).toHaveProperty('type', 'object');
2039+
expect(result.tools[0]!.outputSchema).toHaveProperty('oneOf');
2040+
});
2041+
19682042
test('should include execution field in listTools response when tool has execution settings', async () => {
19692043
const taskStore = new InMemoryTaskStore();
19702044

0 commit comments

Comments
 (0)