Skip to content

Commit 8642720

Browse files
refactor: narrow to StandardSchemaWithJSON, add fromJsonSchema, review fixes
Follow-up to the Standard Schema feature. Completes the schema-agnostic surface and addresses review findings. Scope completion: - Narrow registration types (inputSchema/outputSchema/argsSchema) from StandardJSONSchemaV1 to StandardSchemaWithJSON — the SDK needs both ~standard.validate and ~standard.jsonSchema, so the type should say so. validateStandardSchema drops to 11 lines, no branches. - Drop resultSchema from experimental.tasks.getTaskResult() — same pattern #1606 used for callTool. Returns GetTaskPayloadResult. - Delete 7 dead exports from schema.ts orphaned by the feature. - Add fromJsonSchema(schema, validator) adapter for raw JSON Schema input (TypeBox, hand-written schemas). Wraps as StandardSchemaWithJSON using the SDK's existing AJV/cfWorker validators. Bug fixes: - Include issue.path in validation error messages (was dropping the field path, e.g. "must be a number" instead of "config.retryCount: must be a number"). - Unwrap optional schema in handlePromptCompletion — registration checked isCompletable after unwrapping, request handler didn't. completable(z.string(), cb).optional() registered but silently returned empty. Pre-existing on main. - Null-safe isOptionalSchema so unwrapOptionalSchema(undefined) doesn't crash when the arg name doesn't exist in the schema shape. Docs and tests: - Migration guides document the removed exports and use z.object() consistently. - standardSchema.test.ts cleaned to v2 request() API (no schema arg; auto-resolved from method name). - Changeset + tests for fromJsonSchema.
1 parent c7d3161 commit 8642720

15 files changed

Lines changed: 453 additions & 342 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@modelcontextprotocol/core': minor
3+
'@modelcontextprotocol/server': minor
4+
'@modelcontextprotocol/client': minor
5+
---
6+
7+
Support Standard Schema for tool and prompt schemas
8+
9+
Tool and prompt registration now accepts any schema library that implements the [Standard Schema spec](https://standardschema.dev/): Zod v4, Valibot, ArkType, and others. `RegisteredTool.inputSchema`, `RegisteredTool.outputSchema`, and `RegisteredPrompt.argsSchema` now use `StandardSchemaWithJSON` (requires both `~standard.validate` and `~standard.jsonSchema`) instead of the Zod-specific `AnySchema` type.
10+
11+
**Zod v4 schemas continue to work unchanged** — Zod v4 implements the required interfaces natively.
12+
13+
```typescript
14+
import { type } from 'arktype';
15+
16+
server.registerTool('greet', {
17+
inputSchema: type({ name: 'string' })
18+
}, async ({ name }) => ({ content: [{ type: 'text', text: `Hello, ${name}!` }] }));
19+
```
20+
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+
31+
**Breaking changes:**
32+
- `experimental.tasks.getTaskResult()` no longer accepts a `resultSchema` parameter. Returns `GetTaskPayloadResult` (a loose `Result`); cast to the expected type at the call site.
33+
- Removed unused exports from `@modelcontextprotocol/core`: `SchemaInput`, `schemaToJson`, `parseSchemaAsync`, `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema`. Use the new `standardSchemaToJsonSchema` and `validateStandardSchema` instead.
34+
- `completable()` remains Zod-specific (it relies on Zod's `.shape` introspection).

docs/migration-SKILL.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ if (error instanceof OAuthError && error.code === OAuthErrorCode.InvalidClient)
209209

210210
The variadic `.tool()`, `.prompt()`, `.resource()` methods are removed. Use the `register*` methods with a config object.
211211

212-
**IMPORTANT**: v2 requires full Zod schemas — raw shapes like `{ name: z.string() }` are no longer supported. You must wrap with `z.object()`. This applies to `inputSchema`, `outputSchema`, and `argsSchema`.
212+
**IMPORTANT**: v2 requires schema objects implementing [Standard Schema](https://standardschema.dev/) — raw shapes like `{ name: z.string() }` are no longer supported. Wrap with `z.object()` (Zod v4), or use ArkType's `type({...})`, or Valibot. Applies to `inputSchema`, `outputSchema`, and `argsSchema`.
213213

214214
### Tools
215215

@@ -279,13 +279,22 @@ Note: the third argument (`metadata`) is required — pass `{}` if no metadata.
279279

280280
### Schema Migration Quick Reference
281281

282-
| v1 (raw shape) | v2 (Zod schema) |
282+
| v1 (raw shape) | v2 (Standard Schema object) |
283283
|----------------|-----------------|
284284
| `{ name: z.string() }` | `z.object({ name: z.string() })` |
285285
| `{ count: z.number().optional() }` | `z.object({ count: z.number().optional() })` |
286286
| `{}` (empty) | `z.object({})` |
287287
| `undefined` (no schema) | `undefined` or omit the field |
288288

289+
### Removed core exports
290+
291+
| Removed from `@modelcontextprotocol/core` | Replacement |
292+
|---|---|
293+
| `schemaToJson(schema)` | `standardSchemaToJsonSchema(schema)` |
294+
| `parseSchemaAsync(schema, data)` | `validateStandardSchema(schema, data)` |
295+
| `SchemaInput<T>` | `StandardSchemaWithJSON.InferInput<T>` |
296+
| `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema` | none (internal Zod introspection helpers) |
297+
289298
## 7. Headers API
290299

291300
Transport constructors and `RequestInfo.headers` now use the Web Standard `Headers` object instead of plain objects.

docs/migration.md

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -200,17 +200,17 @@ import * as z from 'zod/v4';
200200
const server = new McpServer({ name: 'demo', version: '1.0.0' });
201201

202202
// Tool with schema
203-
server.registerTool('greet', { inputSchema: { name: z.string() } }, async ({ name }) => {
203+
server.registerTool('greet', { inputSchema: z.object({ name: z.string() }) }, async ({ name }) => {
204204
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
205205
});
206206

207207
// Tool with description
208-
server.registerTool('greet', { description: 'Greet a user', inputSchema: { name: z.string() } }, async ({ name }) => {
208+
server.registerTool('greet', { description: 'Greet a user', inputSchema: z.object({ name: z.string() }) }, async ({ name }) => {
209209
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
210210
});
211211

212212
// Prompt
213-
server.registerPrompt('summarize', { argsSchema: { text: z.string() } }, async ({ text }) => {
213+
server.registerPrompt('summarize', { argsSchema: z.object({ text: z.string() }) }, async ({ text }) => {
214214
return { messages: [{ role: 'user', content: { type: 'text', text: `Summarize: ${text}` } }] };
215215
});
216216

@@ -220,9 +220,9 @@ server.registerResource('config', 'config://app', {}, async uri => {
220220
});
221221
```
222222

223-
### Zod schemas required (raw shapes no longer supported)
223+
### Standard Schema objects required (raw shapes no longer supported)
224224

225-
v2 requires full Zod schemas for `inputSchema` and `argsSchema`. Raw object shapes are no longer accepted.
225+
v2 requires schema objects implementing the [Standard Schema spec](https://standardschema.dev/) for `inputSchema`, `outputSchema`, and `argsSchema`. Raw object shapes are no longer accepted. Zod v4, ArkType, and Valibot all implement the spec.
226226

227227
**Before (v1):**
228228

@@ -240,9 +240,15 @@ server.registerTool('greet', {
240240
```typescript
241241
import * as z from 'zod/v4';
242242

243-
// Must wrap with z.object()
243+
// Wrap with z.object() (or use any Standard Schema library)
244244
server.registerTool('greet', {
245-
inputSchema: z.object({ name: z.string() }) // full Zod schema
245+
inputSchema: z.object({ name: z.string() })
246+
}, async ({ name }) => { ... });
247+
248+
// ArkType works too
249+
import { type } from 'arktype';
250+
server.registerTool('greet', {
251+
inputSchema: type({ name: 'string' })
246252
}, async ({ name }) => { ... });
247253

248254
// For tools with no parameters, use z.object({})
@@ -256,6 +262,15 @@ This applies to:
256262
- `outputSchema` in `registerTool()`
257263
- `argsSchema` in `registerPrompt()`
258264

265+
**Removed Zod-specific helpers** from `@modelcontextprotocol/core` (use Standard Schema equivalents):
266+
267+
| Removed | Replacement |
268+
|---|---|
269+
| `schemaToJson(schema)` | `standardSchemaToJsonSchema(schema)` |
270+
| `parseSchemaAsync(schema, data)` | `validateStandardSchema(schema, data)` |
271+
| `SchemaInput<T>` | `StandardSchemaWithJSON.InferInput<T>` |
272+
| `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema` | No replacement — these are now internal Zod introspection helpers |
273+
259274
### Host header validation moved
260275

261276
Express-specific middleware (`hostHeaderValidation()`, `localhostHostValidation()`) moved from the server package to `@modelcontextprotocol/express`. The server package now exports framework-agnostic functions instead: `validateHostHeader()`, `localhostAllowedHostnames()`,

packages/client/src/experimental/tasks/client.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,19 @@
66
*/
77

88
import type {
9-
AnyObjectSchema,
109
CallToolRequest,
1110
CallToolResult,
1211
CancelTaskResult,
1312
CreateTaskResult,
13+
GetTaskPayloadResult,
1414
GetTaskResult,
1515
ListTasksResult,
1616
RequestMethod,
1717
RequestOptions,
1818
ResponseMessage,
19-
ResultTypeMap,
20-
SchemaOutput
19+
ResultTypeMap
2120
} from '@modelcontextprotocol/core';
22-
import { ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/core';
21+
import { GetTaskPayloadResultSchema, ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/core';
2322

2423
import type { Client } from '../../client/client.js';
2524

@@ -185,23 +184,22 @@ export class ExperimentalClientTasks {
185184
* Retrieves the result of a completed task.
186185
*
187186
* @param taskId - The task identifier
188-
* @param resultSchema - Zod schema for validating the result
189187
* @param options - Optional request options
190-
* @returns The task result
188+
* @returns The task result. The payload structure matches the result type of the
189+
* original request (e.g., a `tools/call` task returns a `CallToolResult`).
191190
*
192191
* @experimental
193192
*/
194-
async getTaskResult<T extends AnyObjectSchema>(taskId: string, resultSchema?: T, options?: RequestOptions): Promise<SchemaOutput<T>> {
195-
// Delegate to the client's underlying Protocol method
193+
async getTaskResult(taskId: string, options?: RequestOptions): Promise<GetTaskPayloadResult> {
196194
return (
197195
this._client as unknown as {
198-
getTaskResult: <U extends AnyObjectSchema>(
196+
getTaskResult: (
199197
params: { taskId: string },
200-
resultSchema?: U,
198+
resultSchema: typeof GetTaskPayloadResultSchema,
201199
options?: RequestOptions
202-
) => Promise<SchemaOutput<U>>;
200+
) => Promise<GetTaskPayloadResult>;
203201
}
204-
).getTaskResult({ taskId }, resultSchema, options);
202+
).getTaskResult({ taskId }, GetTaskPayloadResultSchema, options);
205203
}
206204

207205
/**

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
*

packages/core/src/util/schema.ts

Lines changed: 0 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,11 @@ export type AnySchema = z.core.$ZodType;
1515
*/
1616
export type AnyObjectSchema = z.core.$ZodObject;
1717

18-
/**
19-
* Extracts the input type from a Zod schema.
20-
*/
21-
export type SchemaInput<T extends AnySchema> = z.input<T>;
22-
2318
/**
2419
* Extracts the output type from a Zod schema.
2520
*/
2621
export type SchemaOutput<T extends AnySchema> = z.output<T>;
2722

28-
/**
29-
* Converts a Zod schema to JSON Schema.
30-
*/
31-
export function schemaToJson(schema: AnySchema, options?: { io?: 'input' | 'output' }): Record<string, unknown> {
32-
return z.toJSONSchema(schema, options) as Record<string, unknown>;
33-
}
34-
3523
/**
3624
* Parses data against a Zod schema (synchronous).
3725
* Returns a discriminated union with success/error.
@@ -42,54 +30,3 @@ export function parseSchema<T extends AnySchema>(
4230
): { success: true; data: z.output<T> } | { success: false; error: z.core.$ZodError } {
4331
return z.safeParse(schema, data);
4432
}
45-
46-
/**
47-
* Parses data against a Zod schema (asynchronous).
48-
* Returns a discriminated union with success/error.
49-
*/
50-
export function parseSchemaAsync<T extends AnySchema>(
51-
schema: T,
52-
data: unknown
53-
): Promise<{ success: true; data: z.output<T> } | { success: false; error: z.core.$ZodError }> {
54-
return z.safeParseAsync(schema, data);
55-
}
56-
57-
/**
58-
* Gets the shape of an object schema.
59-
* Returns undefined if the schema is not an object schema.
60-
*/
61-
export function getSchemaShape(schema: AnySchema): Record<string, AnySchema> | undefined {
62-
const candidate = schema as { shape?: unknown };
63-
if (candidate.shape && typeof candidate.shape === 'object') {
64-
return candidate.shape as Record<string, AnySchema>;
65-
}
66-
return undefined;
67-
}
68-
69-
/**
70-
* Gets the description from a schema if it has one.
71-
*/
72-
export function getSchemaDescription(schema: AnySchema): string | undefined {
73-
const candidate = schema as { description?: string };
74-
return candidate.description;
75-
}
76-
77-
/**
78-
* Checks if a schema is optional (accepts undefined).
79-
*/
80-
export function isOptionalSchema(schema: AnySchema): boolean {
81-
const candidate = schema as { type?: string };
82-
return candidate.type === 'optional';
83-
}
84-
85-
/**
86-
* Unwraps an optional schema to get the inner schema.
87-
* If the schema is not optional, returns it unchanged.
88-
*/
89-
export function unwrapOptionalSchema(schema: AnySchema): AnySchema {
90-
if (!isOptionalSchema(schema)) {
91-
return schema;
92-
}
93-
const candidate = schema as { def?: { innerType?: AnySchema } };
94-
return candidate.def?.innerType ?? schema;
95-
}

packages/core/src/util/standardSchema.ts

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66

77
/* eslint-disable @typescript-eslint/no-namespace */
88

9-
import type { JsonSchemaType, jsonSchemaValidator } from '../validators/types.js';
10-
11-
// Standard Schema interfaces (from https://standardschema.dev)
9+
// Standard Schema interfaces — vendored from https://standardschema.dev (spec v1, Jan 2025)
1210

1311
export interface StandardTypedV1<Input = unknown, Output = Input> {
1412
readonly '~standard': StandardTypedV1.Props<Input, Output>;
@@ -92,11 +90,28 @@ export namespace StandardJSONSchemaV1 {
9290
export type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
9391
}
9492

95-
/** Combined interface for schemas with both validation and JSON Schema conversion (e.g., Zod v4). */
93+
/**
94+
* Combined interface for schemas with both validation and JSON Schema conversion —
95+
* the intersection of {@linkcode StandardSchemaV1} and {@linkcode StandardJSONSchemaV1}.
96+
*
97+
* This is the type accepted by `registerTool` / `registerPrompt`. The SDK needs
98+
* `~standard.jsonSchema` to advertise the tool's argument shape in `tools/list`, and
99+
* `~standard.validate` to check incoming arguments when a `tools/call` arrives.
100+
*
101+
* Zod v4, ArkType, and Valibot (via `@valibot/to-json-schema`'s `toStandardJsonSchema`)
102+
* all implement both interfaces.
103+
*
104+
* @see https://standardschema.dev/ for the Standard Schema specification
105+
*/
96106
export interface StandardSchemaWithJSON<Input = unknown, Output = Input> {
97107
readonly '~standard': StandardSchemaV1.Props<Input, Output> & StandardJSONSchemaV1.Props<Input, Output>;
98108
}
99109

110+
export namespace StandardSchemaWithJSON {
111+
export type InferInput<Schema extends StandardTypedV1> = StandardTypedV1.InferInput<Schema>;
112+
export type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
113+
}
114+
100115
// Type guards
101116

102117
export function isStandardJSONSchema(schema: unknown): schema is StandardJSONSchemaV1 {
@@ -131,35 +146,21 @@ export function standardSchemaToJsonSchema(schema: StandardJSONSchemaV1, io: 'in
131146

132147
export type StandardSchemaValidationResult<T> = { success: true; data: T } | { success: false; error: string };
133148

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);
149+
function formatIssue(issue: StandardSchemaV1.Issue): string {
150+
if (!issue.path?.length) return issue.message;
151+
const path = issue.path.map(p => String(typeof p === 'object' ? p.key : p)).join('.');
152+
return `${path}: ${issue.message}`;
153+
}
154154

155-
if (validationResult.valid) {
156-
return { success: true, data: validationResult.data };
157-
}
158-
return { success: false, error: validationResult.errorMessage ?? 'Validation failed' };
155+
export async function validateStandardSchema<T extends StandardSchemaWithJSON>(
156+
schema: T,
157+
data: unknown
158+
): Promise<StandardSchemaValidationResult<StandardSchemaWithJSON.InferOutput<T>>> {
159+
const result = await schema['~standard'].validate(data);
160+
if (result.issues && result.issues.length > 0) {
161+
return { success: false, error: result.issues.map(i => formatIssue(i)).join(', ') };
159162
}
160-
161-
// No validation - trust the data
162-
return { success: true, data: data as StandardJSONSchemaV1.InferOutput<T> };
163+
return { success: true, data: (result as StandardSchemaV1.SuccessResult<unknown>).value as StandardSchemaWithJSON.InferOutput<T> };
163164
}
164165

165166
// Prompt argument extraction
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+
}

0 commit comments

Comments
 (0)