diff --git a/README.md b/README.md index 92f56786f..e0bb82ef2 100644 --- a/README.md +++ b/README.md @@ -1399,6 +1399,24 @@ This setup allows you to: - Provide custom documentation URLs - Maintain control over the OAuth flow while delegating to an external provider +### Schema generation compatibility + +Some MCP clients doesn't support schemas with $refs very well, specially with large schemas. You can pass options to the McpServer constructor that explicitly request generating an input / output schema with no $ref. + +```typescript +const mcpServer = new McpServer( + { + name: 'test server', + version: '1.0' + }, + { + zodToJsonSchemaOptions: { + $refStrategy: 'none' + } + } +); +``` + ### Backwards Compatibility Clients and servers with StreamableHttp transport can maintain [backwards compatibility](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#backwards-compatibility) with the deprecated HTTP+SSE transport (from protocol version 2024-11-05) as follows diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index f3669fa64..6ccc0d0f6 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -1685,6 +1685,141 @@ describe('tool()', () => { expect(result.tools[0].name).toBe('test-without-meta'); expect(result.tools[0]._meta).toBeUndefined(); }); + + /*** + * Test: Tool Registration with a complex schema containing $refs should keep the $refs by default + */ + test('should register tool with a schema with $refs by default', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + const addressType = z.object({ + street: z.string(), + city: z.string() + }); + + mcpServer.registerTool( + 'test-with-complex-schema', + { + description: 'A tool with a complex schema with refs', + inputSchema: { + name: z.string(), + primary_address: addressType, + secondary_address: addressType + }, + outputSchema: { + name: z.string(), + primary_address: addressType, + secondary_address: addressType + } + }, + async ({ name }) => ({ + content: [{ type: 'text', text: `Hello, ${name}!` }] + }) + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + const result = await client.request({ method: 'tools/list' }, ListToolsResultSchema); + + expect(result.tools).toHaveLength(1); + expect(result.tools[0].name).toBe('test-with-complex-schema'); + expect(result.tools[0].inputSchema).toMatchObject({ + type: 'object', + properties: { + name: { type: 'string' }, + primary_address: { type: 'object', properties: { street: { type: 'string' }, city: { type: 'string' } } }, + secondary_address: { $ref: '#/properties/primary_address' } + } + }); + expect(result.tools[0].outputSchema).toMatchObject({ + type: 'object', + properties: { + name: { type: 'string' }, + primary_address: { type: 'object', properties: { street: { type: 'string' }, city: { type: 'string' } } }, + secondary_address: { $ref: '#/properties/primary_address' } + } + }); + }); + + /*** + * Test: Tool Registration with a complex schema containing $refs should not use $refs when disabled + */ + test('should register tool with a schema with not $refs if configured', async () => { + const mcpServer = new McpServer( + { + name: 'test server', + version: '1.0' + }, + { + zodToJsonSchemaOptions: { + $refStrategy: 'none' + } + } + ); + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + const addressType = z.object({ + street: z.string(), + city: z.string() + }); + + mcpServer.registerTool( + 'test-with-complex-schema', + { + description: 'A tool with a complex schema with refs', + inputSchema: { + name: z.string(), + primary_address: addressType, + secondary_address: addressType + }, + outputSchema: { + name: z.string(), + primary_address: addressType, + secondary_address: addressType + } + }, + async ({ name }) => ({ + content: [{ type: 'text', text: `Hello, ${name}!` }] + }) + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + const result = await client.request({ method: 'tools/list' }, ListToolsResultSchema); + + expect(result.tools).toHaveLength(1); + expect(result.tools[0].name).toBe('test-with-complex-schema'); + expect(result.tools[0].inputSchema).toMatchObject({ + type: 'object', + properties: { + name: { type: 'string' }, + primary_address: { type: 'object', properties: { street: { type: 'string' }, city: { type: 'string' } } }, + secondary_address: { type: 'object', properties: { street: { type: 'string' }, city: { type: 'string' } } } + } + }); + expect(result.tools[0].outputSchema).toMatchObject({ + type: 'object', + properties: { + name: { type: 'string' }, + primary_address: { type: 'object', properties: { street: { type: 'string' }, city: { type: 'string' } } }, + secondary_address: { type: 'object', properties: { street: { type: 'string' }, city: { type: 'string' } } } + } + }); + }); }); describe('resource()', () => { diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 015f518a1..1eafb82c3 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -55,8 +55,10 @@ export class McpServer { } = {}; private _registeredTools: { [name: string]: RegisteredTool } = {}; private _registeredPrompts: { [name: string]: RegisteredPrompt } = {}; + private _schemaGenerationOptions: SchemaGenerationOption = {}; - constructor(serverInfo: Implementation, options?: ServerOptions) { + constructor(serverInfo: Implementation, options?: ServerOptions & SchemaGenerationOption) { + this._schemaGenerationOptions = options || {}; this.server = new Server(serverInfo, options); } @@ -105,7 +107,8 @@ export class McpServer { inputSchema: tool.inputSchema ? (zodToJsonSchema(tool.inputSchema, { strictUnions: true, - pipeStrategy: 'input' + pipeStrategy: 'input', + ...(this._schemaGenerationOptions.zodToJsonSchemaOptions ?? {}) }) as Tool['inputSchema']) : EMPTY_OBJECT_JSON_SCHEMA, annotations: tool.annotations, @@ -115,7 +118,8 @@ export class McpServer { if (tool.outputSchema) { toolDefinition.outputSchema = zodToJsonSchema(tool.outputSchema, { strictUnions: true, - pipeStrategy: 'output' + pipeStrategy: 'output', + ...(this._schemaGenerationOptions.zodToJsonSchemaOptions ?? {}) }) as Tool['outputSchema']; } @@ -1017,6 +1021,21 @@ export class ResourceTemplate { } } +/** + * Options for schema generation. + */ +export type SchemaGenerationOption = { + /** + * Options for zod-to-json-schema conversion. + * Only one field is currently supported: `$refStrategy`, and with two possible values where + * the library accepts two others which are not relevant here. + * Useful to set to 'none' to avoid $ref generation because some clients do not support them well. + */ + zodToJsonSchemaOptions?: { + $refStrategy?: 'root' | 'none'; + }; +}; + /** * Callback for a tool handler registered with Server.tool(). *