Skip to content

Commit 73ee54c

Browse files
Merge branch 'main' into fix/rpc-error-message-prefix-v2
2 parents bfdd662 + 0784be1 commit 73ee54c

File tree

21 files changed

+1327
-212
lines changed

21 files changed

+1327
-212
lines changed
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. For raw JSON Schema, wrap with `fromJsonSchema(schema, validator)` from `@modelcontextprotocol/core`. 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: 28 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,11 +240,23 @@ 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() })
246246
}, async ({ name }) => { ... });
247247

248+
// ArkType works too
249+
import { type } from 'arktype';
250+
server.registerTool('greet', {
251+
inputSchema: type({ name: 'string' })
252+
}, async ({ name }) => { ... });
253+
254+
// Raw JSON Schema via fromJsonSchema
255+
import { fromJsonSchema, AjvJsonSchemaValidator } from '@modelcontextprotocol/core';
256+
server.registerTool('greet', {
257+
inputSchema: fromJsonSchema({ type: 'object', properties: { name: { type: 'string' } } }, new AjvJsonSchemaValidator())
258+
}, handler);
259+
248260
// For tools with no parameters, use z.object({})
249261
server.registerTool('ping', {
250262
inputSchema: z.object({})
@@ -256,6 +268,15 @@ This applies to:
256268
- `outputSchema` in `registerTool()`
257269
- `argsSchema` in `registerPrompt()`
258270

271+
**Removed Zod-specific helpers** from `@modelcontextprotocol/core` (use Standard Schema equivalents):
272+
273+
| Removed | Replacement |
274+
|---|---|
275+
| `schemaToJson(schema)` | `standardSchemaToJsonSchema(schema)` |
276+
| `parseSchemaAsync(schema, data)` | `validateStandardSchema(schema, data)` |
277+
| `SchemaInput<T>` | `StandardSchemaWithJSON.InferInput<T>` |
278+
| `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema` | No replacement — these are now internal Zod introspection helpers |
279+
259280
### Host header validation moved
260281

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

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/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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ 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';
1819
export * from './validators/ajvProvider.js';
1920
export * from './validators/cfWorkerProvider.js';
21+
export * from './validators/fromJsonSchema.js';
2022
/**
2123
* JSON Schema validation
2224
*

packages/core/src/util/schema.ts

Lines changed: 6 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,25 @@
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

15-
/**
16-
* Extracts the input type from a Zod schema.
17-
*/
18-
export type SchemaInput<T extends AnySchema> = z.input<T>;
19-
2018
/**
2119
* Extracts the output type from a Zod schema.
2220
*/
2321
export type SchemaOutput<T extends AnySchema> = z.output<T>;
2422

25-
/**
26-
* Converts a Zod schema to JSON Schema.
27-
*/
28-
export function schemaToJson(schema: AnySchema, options?: { io?: 'input' | 'output' }): Record<string, unknown> {
29-
return z.toJSONSchema(schema, options) as Record<string, unknown>;
30-
}
31-
3223
/**
3324
* Parses data against a Zod schema (synchronous).
3425
* Returns a discriminated union with success/error.
@@ -39,56 +30,3 @@ export function parseSchema<T extends AnySchema>(
3930
): { success: true; data: z.output<T> } | { success: false; error: z.core.$ZodError } {
4031
return z.safeParse(schema, data);
4132
}
42-
43-
/**
44-
* Parses data against a Zod schema (asynchronous).
45-
* Returns a discriminated union with success/error.
46-
*/
47-
export function parseSchemaAsync<T extends AnySchema>(
48-
schema: T,
49-
data: unknown
50-
): Promise<{ success: true; data: z.output<T> } | { success: false; error: z.core.$ZodError }> {
51-
return z.safeParseAsync(schema, data);
52-
}
53-
54-
/**
55-
* Gets the shape of an object schema.
56-
* Returns undefined if the schema is not an object schema.
57-
*/
58-
export function getSchemaShape(schema: AnySchema): Record<string, AnySchema> | undefined {
59-
const candidate = schema as { shape?: unknown };
60-
if (candidate.shape && typeof candidate.shape === 'object') {
61-
return candidate.shape as Record<string, AnySchema>;
62-
}
63-
return undefined;
64-
}
65-
66-
/**
67-
* Gets the description from a schema if it has one.
68-
*/
69-
export function getSchemaDescription(schema: AnySchema): string | undefined {
70-
const candidate = schema as { description?: string };
71-
return candidate.description;
72-
}
73-
74-
/**
75-
* Checks if a schema is optional (accepts undefined).
76-
* Uses the public .type property which works in both zod/v4 and zod/v4/mini.
77-
*/
78-
export function isOptionalSchema(schema: AnySchema): boolean {
79-
const candidate = schema as { type?: string };
80-
return candidate.type === 'optional';
81-
}
82-
83-
/**
84-
* Unwraps an optional schema to get the inner schema.
85-
* 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.
87-
*/
88-
export function unwrapOptionalSchema(schema: AnySchema): AnySchema {
89-
if (!isOptionalSchema(schema)) {
90-
return schema;
91-
}
92-
const candidate = schema as { def?: { innerType?: AnySchema } };
93-
return candidate.def?.innerType ?? schema;
94-
}

0 commit comments

Comments
 (0)