Skip to content

Commit c7d3161

Browse files
feat: support Standard JSON Schema (StandardJSONSchemaV1) for tool/prompt schemas
Replace the Zod-specific AnySchema type with StandardJSONSchemaV1 from the Standard Schema spec for user-provided tool and prompt schemas. Enables any schema library implementing the spec (Zod v4, Valibot, ArkType) to be used. - New packages/core/src/util/standardSchema.ts with spec interfaces and utils - RegisteredTool/RegisteredPrompt accept StandardJSONSchemaV1 - Tool callbacks use StandardJSONSchemaV1.InferOutput<T> for inference - New ArkType and Valibot examples - New integration tests for mixed schema libraries completable() remains Zod-specific (relies on .shape introspection). Co-authored-by: Matt Carey <mcarey@cloudflare.com>
1 parent ccb78f2 commit c7d3161

14 files changed

Lines changed: 1115 additions & 117 deletions

File tree

examples/server/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,17 @@
3434
"dependencies": {
3535
"@hono/node-server": "catalog:runtimeServerOnly",
3636
"@modelcontextprotocol/examples-shared": "workspace:^",
37-
"@modelcontextprotocol/node": "workspace:^",
38-
"@modelcontextprotocol/server": "workspace:^",
3937
"@modelcontextprotocol/express": "workspace:^",
4038
"@modelcontextprotocol/hono": "workspace:^",
39+
"@modelcontextprotocol/node": "workspace:^",
40+
"@modelcontextprotocol/server": "workspace:^",
41+
"@valibot/to-json-schema": "catalog:devTools",
42+
"arktype": "catalog:devTools",
4143
"better-auth": "^1.4.17",
4244
"cors": "catalog:runtimeServerOnly",
4345
"express": "catalog:runtimeServerOnly",
4446
"hono": "catalog:runtimeServerOnly",
47+
"valibot": "catalog:devTools",
4548
"zod": "catalog:runtimeShared"
4649
},
4750
"devDependencies": {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Minimal MCP server using ArkType for schema validation.
4+
* ArkType implements the Standard Schema spec with built-in JSON Schema conversion.
5+
*/
6+
7+
import { McpServer, StdioServerTransport } from '@modelcontextprotocol/server';
8+
import { type } from 'arktype';
9+
10+
const server = new McpServer({
11+
name: 'arktype-example',
12+
version: '1.0.0'
13+
});
14+
15+
// Register a tool with ArkType schema
16+
server.registerTool(
17+
'greet',
18+
{
19+
description: 'Generate a greeting',
20+
inputSchema: type({ name: 'string' })
21+
},
22+
async ({ name }) => ({
23+
content: [{ type: 'text', text: `Hello, ${name}!` }]
24+
})
25+
);
26+
27+
const transport = new StdioServerTransport();
28+
await server.connect(transport);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Minimal MCP server using Valibot for schema validation.
4+
* Use toStandardJsonSchema() from @valibot/to-json-schema to create
5+
* StandardJSONSchemaV1-compliant schemas.
6+
*/
7+
8+
import { McpServer, StdioServerTransport } from '@modelcontextprotocol/server';
9+
import { toStandardJsonSchema } from '@valibot/to-json-schema';
10+
import * as v from 'valibot';
11+
12+
const server = new McpServer({
13+
name: 'valibot-example',
14+
version: '1.0.0'
15+
});
16+
17+
// Register a tool with Valibot schema
18+
server.registerTool(
19+
'greet',
20+
{
21+
description: 'Generate a greeting',
22+
inputSchema: toStandardJsonSchema(v.object({ name: v.string() }))
23+
},
24+
async ({ name }) => ({
25+
content: [{ type: 'text', text: `Hello, ${name}!` }]
26+
})
27+
);
28+
29+
const transport = new StdioServerTransport();
30+
await server.connect(transport);

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export * from './shared/uriTemplate.js';
1212
export * from './types/types.js';
1313
export * from './util/inMemory.js';
1414
export * from './util/schema.js';
15+
export * from './util/standardSchema.js';
1516

1617
// experimental exports
1718
export * from './experimental/index.js';

packages/core/src/util/schema.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1+
/**
2+
* Internal Zod schema utilities for protocol handling.
3+
* These are used internally by the SDK for protocol message validation.
4+
*/
5+
16
import * as z from 'zod/v4';
27

38
/**
49
* Base type for any Zod schema.
5-
* This is the canonical type to use when accepting user-provided schemas.
610
*/
711
export type AnySchema = z.core.$ZodType;
812

913
/**
10-
* A Zod schema for objects specifically (not unions).
11-
* Use this when you need to constrain to ZodObject schemas.
14+
* A Zod schema for objects specifically.
1215
*/
1316
export type AnyObjectSchema = z.core.$ZodObject;
1417

@@ -73,7 +76,6 @@ export function getSchemaDescription(schema: AnySchema): string | undefined {
7376

7477
/**
7578
* Checks if a schema is optional (accepts undefined).
76-
* Uses the public .type property which works in both zod/v4 and zod/v4/mini.
7779
*/
7880
export function isOptionalSchema(schema: AnySchema): boolean {
7981
const candidate = schema as { type?: string };
@@ -83,7 +85,6 @@ export function isOptionalSchema(schema: AnySchema): boolean {
8385
/**
8486
* Unwraps an optional schema to get the inner schema.
8587
* If the schema is not optional, returns it unchanged.
86-
* Uses the public .def.innerType property which works in both zod/v4 and zod/v4/mini.
8788
*/
8889
export function unwrapOptionalSchema(schema: AnySchema): AnySchema {
8990
if (!isOptionalSchema(schema)) {
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/**
2+
* Standard Schema utilities for user-provided schemas.
3+
* Supports Zod v4, Valibot, ArkType, and other Standard Schema implementations.
4+
* @see https://standardschema.dev
5+
*/
6+
7+
/* eslint-disable @typescript-eslint/no-namespace */
8+
9+
import type { JsonSchemaType, jsonSchemaValidator } from '../validators/types.js';
10+
11+
// Standard Schema interfaces (from https://standardschema.dev)
12+
13+
export interface StandardTypedV1<Input = unknown, Output = Input> {
14+
readonly '~standard': StandardTypedV1.Props<Input, Output>;
15+
}
16+
17+
export namespace StandardTypedV1 {
18+
export interface Props<Input = unknown, Output = Input> {
19+
readonly version: 1;
20+
readonly vendor: string;
21+
readonly types?: Types<Input, Output> | undefined;
22+
}
23+
24+
export interface Types<Input = unknown, Output = Input> {
25+
readonly input: Input;
26+
readonly output: Output;
27+
}
28+
29+
export type InferInput<Schema extends StandardTypedV1> = NonNullable<Schema['~standard']['types']>['input'];
30+
export type InferOutput<Schema extends StandardTypedV1> = NonNullable<Schema['~standard']['types']>['output'];
31+
}
32+
33+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
34+
readonly '~standard': StandardSchemaV1.Props<Input, Output>;
35+
}
36+
37+
export namespace StandardSchemaV1 {
38+
export interface Props<Input = unknown, Output = Input> extends StandardTypedV1.Props<Input, Output> {
39+
readonly validate: (value: unknown, options?: Options | undefined) => Result<Output> | Promise<Result<Output>>;
40+
}
41+
42+
export interface Options {
43+
readonly libraryOptions?: Record<string, unknown> | undefined;
44+
}
45+
46+
export type Result<Output> = SuccessResult<Output> | FailureResult;
47+
48+
export interface SuccessResult<Output> {
49+
readonly value: Output;
50+
readonly issues?: undefined;
51+
}
52+
53+
export interface FailureResult {
54+
readonly issues: ReadonlyArray<Issue>;
55+
}
56+
57+
export interface Issue {
58+
readonly message: string;
59+
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
60+
}
61+
62+
export interface PathSegment {
63+
readonly key: PropertyKey;
64+
}
65+
66+
export type InferInput<Schema extends StandardTypedV1> = StandardTypedV1.InferInput<Schema>;
67+
export type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
68+
}
69+
70+
export interface StandardJSONSchemaV1<Input = unknown, Output = Input> {
71+
readonly '~standard': StandardJSONSchemaV1.Props<Input, Output>;
72+
}
73+
74+
export namespace StandardJSONSchemaV1 {
75+
export interface Props<Input = unknown, Output = Input> extends StandardTypedV1.Props<Input, Output> {
76+
readonly jsonSchema: Converter;
77+
}
78+
79+
export interface Converter {
80+
readonly input: (options: Options) => Record<string, unknown>;
81+
readonly output: (options: Options) => Record<string, unknown>;
82+
}
83+
84+
export type Target = 'draft-2020-12' | 'draft-07' | 'openapi-3.0' | (object & string);
85+
86+
export interface Options {
87+
readonly target: Target;
88+
readonly libraryOptions?: Record<string, unknown> | undefined;
89+
}
90+
91+
export type InferInput<Schema extends StandardTypedV1> = StandardTypedV1.InferInput<Schema>;
92+
export type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
93+
}
94+
95+
/** Combined interface for schemas with both validation and JSON Schema conversion (e.g., Zod v4). */
96+
export interface StandardSchemaWithJSON<Input = unknown, Output = Input> {
97+
readonly '~standard': StandardSchemaV1.Props<Input, Output> & StandardJSONSchemaV1.Props<Input, Output>;
98+
}
99+
100+
// Type guards
101+
102+
export function isStandardJSONSchema(schema: unknown): schema is StandardJSONSchemaV1 {
103+
if (schema == null) return false;
104+
const schemaType = typeof schema;
105+
if (schemaType !== 'object' && schemaType !== 'function') return false;
106+
if (!('~standard' in (schema as object))) return false;
107+
const std = (schema as StandardJSONSchemaV1)['~standard'];
108+
return typeof std?.jsonSchema?.input === 'function' && typeof std?.jsonSchema?.output === 'function';
109+
}
110+
111+
export function isStandardSchema(schema: unknown): schema is StandardSchemaV1 {
112+
if (schema == null) return false;
113+
const schemaType = typeof schema;
114+
if (schemaType !== 'object' && schemaType !== 'function') return false;
115+
if (!('~standard' in (schema as object))) return false;
116+
const std = (schema as StandardSchemaV1)['~standard'];
117+
return typeof std?.validate === 'function';
118+
}
119+
120+
export function isStandardSchemaWithJSON(schema: unknown): schema is StandardSchemaWithJSON {
121+
return isStandardJSONSchema(schema) && isStandardSchema(schema);
122+
}
123+
124+
// JSON Schema conversion
125+
126+
export function standardSchemaToJsonSchema(schema: StandardJSONSchemaV1, io: 'input' | 'output' = 'input'): Record<string, unknown> {
127+
return schema['~standard'].jsonSchema[io]({ target: 'draft-2020-12' });
128+
}
129+
130+
// Validation
131+
132+
export type StandardSchemaValidationResult<T> = { success: true; data: T } | { success: false; error: string };
133+
134+
export async function validateStandardSchema<T extends StandardJSONSchemaV1>(
135+
schema: T,
136+
data: unknown,
137+
jsonSchemaValidatorInstance?: jsonSchemaValidator
138+
): Promise<StandardSchemaValidationResult<StandardJSONSchemaV1.InferOutput<T>>> {
139+
// Use native validation if available
140+
if (isStandardSchema(schema)) {
141+
const result = await schema['~standard'].validate(data);
142+
if (result.issues && result.issues.length > 0) {
143+
const errorMessage = result.issues.map((i: StandardSchemaV1.Issue) => i.message).join(', ');
144+
return { success: false, error: errorMessage };
145+
}
146+
return { success: true, data: (result as StandardSchemaV1.SuccessResult<unknown>).value as StandardJSONSchemaV1.InferOutput<T> };
147+
}
148+
149+
// Fall back to JSON Schema validation
150+
if (jsonSchemaValidatorInstance) {
151+
const jsonSchema = standardSchemaToJsonSchema(schema, 'input');
152+
const validator = jsonSchemaValidatorInstance.getValidator<StandardJSONSchemaV1.InferOutput<T>>(jsonSchema as JsonSchemaType);
153+
const validationResult = validator(data);
154+
155+
if (validationResult.valid) {
156+
return { success: true, data: validationResult.data };
157+
}
158+
return { success: false, error: validationResult.errorMessage ?? 'Validation failed' };
159+
}
160+
161+
// No validation - trust the data
162+
return { success: true, data: data as StandardJSONSchemaV1.InferOutput<T> };
163+
}
164+
165+
// Prompt argument extraction
166+
167+
export function promptArgumentsFromStandardSchema(
168+
schema: StandardJSONSchemaV1
169+
): Array<{ name: string; description?: string; required: boolean }> {
170+
const jsonSchema = standardSchemaToJsonSchema(schema, 'input');
171+
const properties = (jsonSchema.properties as Record<string, { description?: string }>) || {};
172+
const required = (jsonSchema.required as string[]) || [];
173+
174+
return Object.entries(properties).map(([name, prop]) => ({
175+
name,
176+
description: prop?.description,
177+
required: required.includes(name)
178+
}));
179+
}

packages/server/src/experimental/tasks/interfaces.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
*/
55

66
import type {
7-
AnySchema,
87
CallToolResult,
98
CreateTaskResult,
109
CreateTaskServerContext,
1110
GetTaskResult,
1211
Result,
12+
StandardJSONSchemaV1,
1313
TaskServerContext
1414
} from '@modelcontextprotocol/core';
1515

@@ -23,18 +23,17 @@ import type { BaseToolCallback } from '../../server/mcp.js';
2323
* Handler for creating a task.
2424
* @experimental
2525
*/
26-
export type CreateTaskRequestHandler<ResultT extends Result, Args extends AnySchema | undefined = undefined> = BaseToolCallback<
27-
ResultT,
28-
CreateTaskServerContext,
29-
Args
30-
>;
26+
export type CreateTaskRequestHandler<
27+
SendResultT extends Result,
28+
Args extends StandardJSONSchemaV1 | undefined = undefined
29+
> = BaseToolCallback<SendResultT, CreateTaskServerContext, Args>;
3130

3231
/**
3332
* Handler for task operations (`get`, `getResult`).
3433
* @experimental
3534
*/
36-
export type TaskRequestHandler<ResultT extends Result, Args extends AnySchema | undefined = undefined> = BaseToolCallback<
37-
ResultT,
35+
export type TaskRequestHandler<SendResultT extends Result, Args extends StandardJSONSchemaV1 | undefined = undefined> = BaseToolCallback<
36+
SendResultT,
3837
TaskServerContext,
3938
Args
4039
>;
@@ -48,7 +47,7 @@ export type TaskRequestHandler<ResultT extends Result, Args extends AnySchema |
4847
* @see {@linkcode @modelcontextprotocol/server!experimental/tasks/mcpServer.ExperimentalMcpServerTasks#registerToolTask | registerToolTask} for registration.
4948
* @experimental
5049
*/
51-
export interface ToolTaskHandler<Args extends AnySchema | undefined = undefined> {
50+
export interface ToolTaskHandler<Args extends StandardJSONSchemaV1 | undefined = undefined> {
5251
/**
5352
* Called on the initial `tools/call` request.
5453
*

0 commit comments

Comments
 (0)