Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .changeset/spec-type-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
'@modelcontextprotocol/server': minor
---

Export `isSpecType` and `specTypeSchemas` records for runtime validation of any MCP spec type by name. `isSpecType.ContentBlock(value)` is a type predicate; `specTypeSchemas.ContentBlock` is a `StandardSchemaV1<ContentBlock>` validator. Guards are standalone functions, so `arr.filter(isSpecType.ContentBlock)` works. Also export the `SpecTypeName` and `SpecTypes` types.
Export `isSpecType` and `specTypeSchemas` records for runtime validation of any MCP spec type by name. `isSpecType.ContentBlock(value)` is a type predicate; `specTypeSchemas.ContentBlock` is a `StandardSchemaV1Sync<ContentBlock>` validator — `validate()` returns the result synchronously. Guards are standalone functions, so `arr.filter(isSpecType.ContentBlock)` works. Also export the `SpecTypeName`, `SpecTypes`, and `StandardSchemaV1Sync` types.
6 changes: 3 additions & 3 deletions docs/migration-SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Notes:
| `WebSocketClientTransport` | REMOVED (use `StreamableHTTPClientTransport` or `StdioClientTransport`) |

All other **type** symbols from `@modelcontextprotocol/sdk/types.js` retain their original names. **Zod schemas** (e.g., `CallToolResultSchema`, `ListToolsResultSchema`) are no longer part of the public API — they are internal to the SDK. For runtime validation, use
`isSpecType.TypeName(value)` (e.g., `isSpecType.CallToolResult(v)`) or `specTypeSchemas.TypeName` for the `StandardSchemaV1` validator object. The keys are typed as `SpecTypeName`, a literal union of all spec type names.
`isSpecType.TypeName(value)` (e.g., `isSpecType.CallToolResult(v)`) or `specTypeSchemas.TypeName` for the `StandardSchemaV1Sync` validator object. The keys are typed as `SpecTypeName`, a literal union of all spec type names.

### Error class changes

Expand Down Expand Up @@ -468,8 +468,8 @@ If a `*Schema` constant was used for **runtime validation** (not just as a `requ
| -------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `CallToolResultSchema.safeParse(value).success` | `isSpecType.CallToolResult(value)` |
| `<TypeName>Schema.safeParse(value).success` | `isSpecType.<TypeName>(value)` |
| `<TypeName>Schema.parse(value)` | `await specTypeSchemas.<TypeName>['~standard'].validate(value)` (returns a `Result`, not the value) |
| Passing `<TypeName>Schema` as a validator argument | `specTypeSchemas.<TypeName>` (a `StandardSchemaV1<In, Out>`) |
| `<TypeName>Schema.parse(value)` | `specTypeSchemas.<TypeName>['~standard'].validate(value)` (returns a `Result` synchronously, not the value) |
| Passing `<TypeName>Schema` as a validator argument | `specTypeSchemas.<TypeName>` (a `StandardSchemaV1Sync<In, Out>`) |

`isCallToolResult(value)` still works, but `isSpecType` covers every spec type by name.

Expand Down
6 changes: 3 additions & 3 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,12 +505,12 @@ if (isSpecType.CallToolResult(value)) {
}
const blocks = mixed.filter(isSpecType.ContentBlock);

// v2: or get the StandardSchemaV1 validator object directly
// v2: or get the StandardSchemaV1Sync validator object directly
import { specTypeSchemas } from '@modelcontextprotocol/client';
const result = await specTypeSchemas.CallToolResult['~standard'].validate(value);
const result = specTypeSchemas.CallToolResult['~standard'].validate(value);
```

`isSpecType` and `specTypeSchemas` are keyed by `SpecTypeName` — a literal union of every named type in the MCP spec — so you get autocomplete and a compile error on typos. `specTypeSchemas.X` is a `StandardSchemaV1<In, Out>`, which composes with any Standard-Schema-aware library. The pre-existing `isCallToolResult(value)` guard still works.
`isSpecType` and `specTypeSchemas` are keyed by `SpecTypeName` — a literal union of every named type in the MCP spec — so you get autocomplete and a compile error on typos. `specTypeSchemas.X` is a `StandardSchemaV1Sync<In, Out>` — `validate()` returns the result synchronously, so you can access `.issues` / `.value` without `await`. It composes with any Standard-Schema-aware library. The pre-existing `isCallToolResult(value)` guard still works.

### Client list methods return empty results for missing capabilities

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/exports/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export { InMemoryTaskMessageQueue, InMemoryTaskStore } from '../../experimental/
// Validator types and classes
export type { SpecTypeName, SpecTypes } from '../../types/specTypeSchema.js';
export { isSpecType, specTypeSchemas } from '../../types/specTypeSchema.js';
export type { StandardSchemaV1, StandardSchemaWithJSON } from '../../util/standardSchema.js';
export type { StandardSchemaV1, StandardSchemaV1Sync, StandardSchemaWithJSON } from '../../util/standardSchema.js';
Comment thread
claude[bot] marked this conversation as resolved.
export { AjvJsonSchemaValidator } from '../../validators/ajvProvider.js';
export type { CfWorkerSchemaDraft } from '../../validators/cfWorkerProvider.js';
// fromJsonSchema is intentionally NOT exported here — the server and client packages
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/types/specTypeSchema.examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ declare const untrusted: unknown;
declare const value: unknown;
declare const mixed: unknown[];

async function specTypeSchemas_basicUsage() {
function specTypeSchemas_basicUsage() {
//#region specTypeSchemas_basicUsage
const result = await specTypeSchemas.CallToolResult['~standard'].validate(untrusted);
const result = specTypeSchemas.CallToolResult['~standard'].validate(untrusted);
if (result.issues === undefined) {
// result.value is CallToolResult
}
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/types/specTypeSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
OpenIdProviderDiscoveryMetadataSchema,
OpenIdProviderMetadataSchema
} from '../shared/auth.js';
import type { StandardSchemaV1 } from '../util/standardSchema.js';
import type { StandardSchemaV1, StandardSchemaV1Sync } from '../util/standardSchema.js';
import * as schemas from './schemas.js';

/**
Expand Down Expand Up @@ -235,7 +235,7 @@ type SpecTypeInputs = {
[K in SchemaKey as StripSchemaSuffix<K>]: SchemaFor<K> extends z.ZodType ? z.input<SchemaFor<K>> : never;
};

type SchemaRecord = { readonly [K in SpecTypeName]: StandardSchemaV1<SpecTypeInputs[K], SpecTypes[K]> };
type SchemaRecord = { readonly [K in SpecTypeName]: StandardSchemaV1Sync<SpecTypeInputs[K], SpecTypes[K]> };
Comment thread
claude[bot] marked this conversation as resolved.
Comment thread
claude[bot] marked this conversation as resolved.
type GuardRecord = { readonly [K in SpecTypeName]: (value: unknown) => value is SpecTypeInputs[K] };

const _specTypeSchemas: Record<string, StandardSchemaV1> = {};
Expand Down Expand Up @@ -265,7 +265,7 @@ for (const [key, schema] of Object.entries(authSchemas)) {
*
* @example
* ```ts source="./specTypeSchema.examples.ts#specTypeSchemas_basicUsage"
* const result = await specTypeSchemas.CallToolResult['~standard'].validate(untrusted);
* const result = specTypeSchemas.CallToolResult['~standard'].validate(untrusted);
* if (result.issues === undefined) {
* // result.value is CallToolResult
* }
Expand Down
22 changes: 22 additions & 0 deletions packages/core/src/util/standardSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,28 @@ export namespace StandardSchemaWithJSON {
export type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
}

/**
* Narrowing of {@linkcode StandardSchemaV1} whose `validate` is guaranteed synchronous.
*
* The Zod schemas backing `specTypeSchemas` contain no async refinements or transforms,
* so every entry satisfies this interface. Consumers can call `validate()` and access
* `.issues` / `.value` on the result without `await`.
*
* `StandardSchemaV1Sync` is assignable to `StandardSchemaV1` — it is a strict subtype.
*/
export interface StandardSchemaV1Sync<Input = unknown, Output = Input> extends StandardSchemaV1<Input, Output> {
readonly '~standard': StandardSchemaV1Sync.Props<Input, Output>;
}

export namespace StandardSchemaV1Sync {
export interface Props<Input = unknown, Output = Input> extends StandardSchemaV1.Props<Input, Output> {
readonly validate: (value: unknown, options?: StandardSchemaV1.Options | undefined) => StandardSchemaV1.Result<Output>;
}

export type InferInput<Schema extends StandardTypedV1> = StandardTypedV1.InferInput<Schema>;
export type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
}
Comment thread
claude[bot] marked this conversation as resolved.

// Type guards

export function isStandardJSONSchema(schema: unknown): schema is StandardJSONSchemaV1 {
Expand Down
12 changes: 6 additions & 6 deletions packages/core/test/types/specTypeSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ import type {
} from '../../src/types/types.js';

describe('specTypeSchemas', () => {
it('returns a StandardSchemaV1 validator that accepts valid values', () => {
it('returns a StandardSchemaV1Sync validator that accepts valid values', () => {
const result = specTypeSchemas.Implementation['~standard'].validate({ name: 'x', version: '1.0.0' });
expect((result as { issues?: unknown }).issues).toBeUndefined();
expect(result.issues).toBeUndefined();
});

it('returns a validator that rejects invalid values with issues', () => {
const result = specTypeSchemas.Implementation['~standard'].validate({ name: 'x' });
expect((result as { issues?: readonly unknown[] }).issues?.length).toBeGreaterThan(0);
expect(result.issues?.length).toBeGreaterThan(0);
});

it('rejects unknown names at compile time and is undefined at runtime', () => {
Expand All @@ -33,14 +33,14 @@ describe('specTypeSchemas', () => {

it('covers JSON-RPC envelope types', () => {
const ok = specTypeSchemas.JSONRPCRequest['~standard'].validate({ jsonrpc: '2.0', id: 1, method: 'ping' });
expect((ok as { issues?: unknown }).issues).toBeUndefined();
expect(ok.issues).toBeUndefined();
});

it('covers OAuth types from shared/auth.ts', () => {
const ok = specTypeSchemas.OAuthTokens['~standard'].validate({ access_token: 'x', token_type: 'Bearer' });
expect((ok as { issues?: unknown }).issues).toBeUndefined();
expect(ok.issues).toBeUndefined();
const bad = specTypeSchemas.OAuthTokens['~standard'].validate({ token_type: 'Bearer' });
expect((bad as { issues?: readonly unknown[] }).issues?.length).toBeGreaterThan(0);
expect(bad.issues?.length).toBeGreaterThan(0);
});
});

Expand Down
Loading