Skip to content

Commit d59c8bb

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 d59c8bb

File tree

3 files changed

+79
-4
lines changed

3 files changed

+79
-4
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

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)