Skip to content

Commit dbe528b

Browse files
feat: add fromJsonSchema adapter for raw JSON Schema input
Wraps a raw JSON Schema object as StandardSchemaWithJSON so it can be passed to registerTool/registerPrompt. Useful for TypeBox users or anyone working at the protocol level with hand-written JSON Schema. Validator is required (not defaulted) so importing fromJsonSchema doesn't pull in AJV. Callback args typed via the generic or as unknown.
1 parent ce40cc5 commit dbe528b

5 files changed

Lines changed: 134 additions & 1 deletion

File tree

.changeset/support-standard-json-schema.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ server.registerTool('greet', {
1818
}, async ({ name }) => ({ content: [{ type: 'text', text: `Hello, ${name}!` }] }));
1919
```
2020

21+
For raw JSON Schema (e.g. TypeBox output), use the new `fromJsonSchema` adapter:
22+
23+
```typescript
24+
import { fromJsonSchema, AjvJsonSchemaValidator } from '@modelcontextprotocol/core';
25+
26+
server.registerTool('greet', {
27+
inputSchema: fromJsonSchema({ type: 'object', properties: { name: { type: 'string' } } }, new AjvJsonSchemaValidator())
28+
}, handler);
29+
```
30+
2131
**Breaking changes:**
2232
- `experimental.tasks.getTaskResult()` no longer accepts a `resultSchema` parameter. Returns `GetTaskPayloadResult` (a loose `Result`); cast to the expected type at the call site.
2333
- Removed unused exports from `@modelcontextprotocol/core`: `SchemaInput`, `schemaToJson`, `parseSchemaAsync`, `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema`. Use the new `standardSchemaToJsonSchema` and `validateStandardSchema` instead.

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export * from './util/standardSchema.js';
1818
export * from './experimental/index.js';
1919
export * from './validators/ajvProvider.js';
2020
export * from './validators/cfWorkerProvider.js';
21+
export * from './validators/fromJsonSchema.js';
2122
/**
2223
* JSON Schema validation
2324
*
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Type-checked examples for `fromJsonSchema.ts`.
3+
*
4+
* These examples are synced into JSDoc comments via the sync-snippets script.
5+
*
6+
* @module
7+
*/
8+
9+
import { AjvJsonSchemaValidator } from './ajvProvider.js';
10+
import { fromJsonSchema } from './fromJsonSchema.js';
11+
12+
/**
13+
* Example: wrap a raw JSON Schema object for use with registerTool.
14+
*/
15+
function fromJsonSchema_basicUsage() {
16+
//#region fromJsonSchema_basicUsage
17+
const inputSchema = fromJsonSchema<{ name: string }>(
18+
{ type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
19+
new AjvJsonSchemaValidator()
20+
);
21+
// Use with server.registerTool('greet', { inputSchema }, handler)
22+
//#endregion fromJsonSchema_basicUsage
23+
return inputSchema;
24+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { StandardSchemaV1, StandardSchemaWithJSON } from '../util/standardSchema.js';
2+
import type { JsonSchemaType, jsonSchemaValidator } from './types.js';
3+
4+
/**
5+
* Wrap a raw JSON Schema object as a {@linkcode StandardSchemaWithJSON} so it can be
6+
* passed to `registerTool` / `registerPrompt`. Use this when you already have JSON
7+
* Schema (e.g. from TypeBox, or hand-written) and want to register it without going
8+
* through a Standard Schema library.
9+
*
10+
* The callback arguments will be typed `unknown` (raw JSON Schema has no TypeScript
11+
* types attached). Cast at the call site, or use the generic `fromJsonSchema<MyType>(...)`.
12+
*
13+
* @example
14+
* ```ts source="./fromJsonSchema.examples.ts#fromJsonSchema_basicUsage"
15+
* const inputSchema = fromJsonSchema<{ name: string }>(
16+
* { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
17+
* new AjvJsonSchemaValidator()
18+
* );
19+
* // Use with server.registerTool('greet', { inputSchema }, handler)
20+
* ```
21+
*/
22+
export function fromJsonSchema<T = unknown>(schema: JsonSchemaType, validator: jsonSchemaValidator): StandardSchemaWithJSON<T, T> {
23+
const check = validator.getValidator<T>(schema);
24+
return {
25+
'~standard': {
26+
version: 1,
27+
vendor: 'mcp',
28+
jsonSchema: {
29+
input: () => schema as Record<string, unknown>,
30+
output: () => schema as Record<string, unknown>
31+
},
32+
validate: (data: unknown): StandardSchemaV1.Result<T> => {
33+
const result = check(data);
34+
return result.valid ? { value: result.data } : { issues: [{ message: result.errorMessage }] };
35+
}
36+
}
37+
};
38+
}

test/integration/test/standardSchema.test.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@
55

66
import { Client } from '@modelcontextprotocol/client';
77
import type { TextContent } from '@modelcontextprotocol/core';
8-
import { CallToolResultSchema, CompleteResultSchema, InMemoryTransport, ListToolsResultSchema } from '@modelcontextprotocol/core';
8+
import {
9+
AjvJsonSchemaValidator,
10+
CallToolResultSchema,
11+
CompleteResultSchema,
12+
fromJsonSchema,
13+
InMemoryTransport,
14+
ListToolsResultSchema
15+
} from '@modelcontextprotocol/core';
916
import { completable, McpServer } from '@modelcontextprotocol/server';
1017
import { toStandardJsonSchema } from '@valibot/to-json-schema';
1118
import { type } from 'arktype';
@@ -411,6 +418,59 @@ describe('Standard Schema Support', () => {
411418
});
412419
});
413420

421+
describe('Raw JSON Schema via fromJsonSchema', () => {
422+
const validator = new AjvJsonSchemaValidator();
423+
424+
test('should register tool with raw JSON Schema input', async () => {
425+
const inputSchema = fromJsonSchema<{ name: string }>(
426+
{ type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
427+
validator
428+
);
429+
430+
mcpServer.registerTool('greet', { inputSchema }, async ({ name }) => ({
431+
content: [{ type: 'text', text: `Hello, ${name}!` }]
432+
}));
433+
434+
await connectClientAndServer();
435+
436+
const listed = await client.request({ method: 'tools/list' }, ListToolsResultSchema);
437+
expect(listed.tools[0].inputSchema).toMatchObject({
438+
type: 'object',
439+
properties: { name: { type: 'string' } },
440+
required: ['name']
441+
});
442+
443+
const result = await client.request(
444+
{ method: 'tools/call', params: { name: 'greet', arguments: { name: 'World' } } },
445+
CallToolResultSchema
446+
);
447+
expect((result.content[0] as TextContent).text).toBe('Hello, World!');
448+
});
449+
450+
test('should reject invalid input via AJV validation', async () => {
451+
const inputSchema = fromJsonSchema(
452+
{ type: 'object', properties: { count: { type: 'number' } }, required: ['count'] },
453+
validator
454+
);
455+
456+
mcpServer.registerTool('double', { inputSchema }, async args => {
457+
const { count } = args as { count: number };
458+
return { content: [{ type: 'text', text: `${count * 2}` }] };
459+
});
460+
461+
await connectClientAndServer();
462+
463+
const result = await client.request(
464+
{ method: 'tools/call', params: { name: 'double', arguments: { count: 'not a number' } } },
465+
CallToolResultSchema
466+
);
467+
468+
expect(result.isError).toBe(true);
469+
const errorText = (result.content[0] as TextContent).text;
470+
expect(errorText).toContain('Input validation error');
471+
});
472+
});
473+
414474
describe('Prompt completions with Zod completable', () => {
415475
// Note: completable() is currently Zod-specific
416476
// These tests verify that Zod schemas with completable still work

0 commit comments

Comments
 (0)