Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
135 changes: 135 additions & 0 deletions src/server/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()', () => {
Expand Down
25 changes: 22 additions & 3 deletions src/server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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,
Expand All @@ -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'];
}

Expand Down Expand Up @@ -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().
*
Expand Down
Loading