Skip to content

Conversation

@mattzcarey
Copy link
Contributor

@mattzcarey mattzcarey commented Feb 3, 2026

Summary

Replace the Zod-specific AnySchema type with StandardJSONSchemaV1 from the Standard Schema spec for user-provided tool and prompt schemas. This enables any schema library that implements the spec (Zod v4, Valibot, ArkType, etc.) to be used for tool/prompt schemas.

Motivation and Context

Currently, the MCP SDK is tightly coupled to Zod for user-provided schemas. This PR makes the SDK library-agnostic by using the Standard Schema spec interfaces:

  • StandardJSONSchemaV1: Primary interface - uses ~standard.jsonSchema.input/output() for JSON Schema conversion (wire protocol)
  • StandardSchemaV1: Optional interface - uses ~standard.validate() for native validation when available

Libraries that implement these interfaces:

  • Zod v4 - implements both (native validation + JSON Schema)
  • Valibot - via @valibot/to-json-schema (native validation + JSON Schema)
  • ArkType - implements both (native validation + JSON Schema)

Changes

Core (packages/core)

  • New file: src/util/standardSchema.ts - Standard Schema interfaces and utilities
    • Type guards: isStandardJSONSchema(), isStandardSchema(), isStandardSchemaWithJSON()
    • JSON Schema conversion: standardSchemaToJsonSchema()
    • Validation: validateStandardSchema() (uses native validation if available, otherwise JSON Schema)
    • Prompt argument extraction: promptArgumentsFromStandardSchema()
  • Updated src/util/schema.ts - Internal Zod utilities for protocol handling (unchanged API)
  • Updated src/shared/protocol.ts - Use schema utility aliases instead of direct Zod imports

Server (packages/server)

  • Updated src/server/mcp.ts
    • RegisteredTool.inputSchema/outputSchema now accept StandardJSONSchemaV1
    • RegisteredPrompt.argsSchema now accepts StandardJSONSchemaV1
    • Tool/prompt callbacks use StandardJSONSchemaV1.InferOutput<T> for type inference
    • Validation uses validateStandardSchema() for native validation when available

Client (packages/client)

  • Updated src/client/client.ts - Use parseSchema utility instead of direct z.safeParse

Examples

  • New: examples/server/src/arktypeExample.ts - ArkType schema example
  • New: examples/server/src/valibotExample.ts - Valibot schema example

Tests

  • New: test/integration/test/standardSchema.test.ts - Comprehensive integration tests
    • ArkType tool registration and validation
    • Valibot tool registration and validation
    • Mixed schema libraries in same server
    • Error message quality verification
    • Type inference verification

How Has This Been Tested?

  1. All 377 existing tests pass - No regressions
  2. New integration tests for ArkType and Valibot schemas
  3. Manual testing of example servers with invalid input

Error Message Quality

All libraries produce clear, user-friendly error messages:

Library Example Error
ArkType age must be a number (was a string)
Valibot Invalid type: Expected string but received 123
Zod Invalid input: expected string, received number

Breaking Changes

  • Tool/prompt inputSchema, outputSchema, and argsSchema now expect StandardJSONSchemaV1 instead of Zod's AnySchema
  • Zod v4 schemas still work unchanged - Zod v4 implements StandardJSONSchemaV1
  • The completable() function for prompt argument completion remains Zod-specific

API Examples

Zod (existing, unchanged)

import * as z from 'zod/v4';

server.registerTool('greet', {
  inputSchema: z.object({ name: z.string() })
}, async ({ name }) => {  // name: string (type inference works)
  return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
});

ArkType (new)

import { type } from 'arktype';

server.registerTool('greet', {
  inputSchema: type({ name: 'string' })
}, async ({ name }) => {
  return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
});

Valibot (new)

import * as v from 'valibot';
import { toStandardJsonSchema } from '@valibot/to-json-schema';

server.registerTool('greet', {
  inputSchema: toStandardJsonSchema(v.object({ name: v.string() }))
}, async ({ name }) => {
  return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
});

Types of changes

  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

@mattzcarey mattzcarey requested a review from a team as a code owner February 3, 2026 20:17
@mattzcarey mattzcarey marked this pull request as draft February 3, 2026 20:17
@changeset-bot
Copy link

changeset-bot bot commented Feb 3, 2026

⚠️ No Changeset found

Latest commit: cb1e1ef

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 3, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@1473

@modelcontextprotocol/server

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server@1473

@modelcontextprotocol/express

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/express@1473

@modelcontextprotocol/hono

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/hono@1473

@modelcontextprotocol/node

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/node@1473

commit: cb1e1ef

@mattzcarey mattzcarey changed the title WIP: support standard json schema for user supplied schemas Support standard json schema for user supplied schemas Feb 3, 2026
@mattzcarey mattzcarey marked this pull request as ready for review February 3, 2026 20:36
@mattzcarey mattzcarey changed the title Support standard json schema for user supplied schemas feat: support Standard JSON Schema (StandardJSONSchemaV1) for tool/prompt schemas Feb 3, 2026
@mattzcarey mattzcarey force-pushed the support-standard-json-schema branch from 35f4667 to edc3098 Compare February 3, 2026 20:55
- Replace z.safeParse with parseSchema utility in client.ts and protocol.ts
- Replace z.core.$ZodType with AnySchema alias
- Replace z.core.$ZodObject with AnyObjectSchema alias
- Replace z.output<T> with SchemaOutput<T> alias
- Remove direct zod import from protocol.ts (now uses schema utilities)
- Add arktypeExample.ts demonstrating ArkType schema validation
- Add valibotExample.ts demonstrating Valibot schema validation
- Fix type guards to accept function-based schemas (ArkType uses functions)
- Fix validation result handling to check issues array first (Valibot compatibility)
- Add arktype, valibot, and @valibot/to-json-schema dependencies

Both examples demonstrate:
- Tool registration with inputSchema and outputSchema
- Native validation via ~standard.validate()
- JSON Schema conversion via ~standard.jsonSchema.input()
Tests cover:
- ArkType tool registration (input/output schemas)
- Valibot tool registration (with descriptions)
- Tool validation with valid/invalid input for both libraries
- Mixed schema libraries in the same server (Zod + ArkType + Valibot)
- Prompt completions with Zod completable
- Error message quality from all three libraries
- Type inference verification

All libraries produce clear, user-friendly error messages:
- ArkType: "age must be a number (was a string)"
- Valibot: "Invalid type: Expected string but received 123"
- Zod: "Invalid input: expected string, received number"
- Add eslint-disable for namespace rule in standardSchema.ts (matches Standard Schema spec design)
- Remove unused imports in standardSchema.test.ts
- Fix switch case braces in example files
- Apply prettier formatting
- Add AnySchema import to experimental/tasks/server.ts
- Add parseSchema import to server/server.ts
@mattzcarey mattzcarey force-pushed the support-standard-json-schema branch from edc3098 to 2983603 Compare February 3, 2026 21:08
- Simplify ArkType and Valibot examples to minimal single-tool demos
- Remove verbose docstrings and section dividers from standardSchema.ts

/**
* The base Standard interface for typed schemas.
* @see https://standardschema.dev
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: copy/paste from https://github.com/standard-schema/standard-schema as recommended - this file basically is the spec.

"@modelcontextprotocol/hono": "workspace:^",
"@modelcontextprotocol/node": "workspace:^",
"@modelcontextprotocol/server": "workspace:^",
"@valibot/to-json-schema": "^1.5.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

example only dep

@@ -0,0 +1,179 @@
/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

- Add arktype, valibot, @valibot/to-json-schema to pnpm catalog
- Replace z.safeParse with parseSchema in server.ts
- Replace z.output/z.core.$ZodType with SchemaOutput/AnySchema aliases
- Remove unused zod imports
Update comments in extractCompleters to explicitly document that
the completable feature only supports Zod schemas due to reliance
on Zod-specific .shape property introspection.
Comment on lines 1240 to 1244
/**
* @internal Extracts completable callbacks from a Zod schema at registration time.
* NOTE: This only works with Zod schemas. ArkType and Valibot are not supported.
*/
function extractCompleters(schema: StandardJSONSchemaV1 | undefined): Map<string, CompleteCallback<StandardJSONSchemaV1>> | undefined {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't it weird to annotate StandardJSONSchemaV1 but it actually requires a zod type as input?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, cant be helped

if (!schema) return undefined;

const shape = getZodSchemaShape(schema);
if (!shape) return undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the guard for passing in a non-zod schema in here

…le lookup

Revert the completable handling back to the original approach where
completable fields are looked up at request time rather than pre-extracted
at registration time.

This removes:
- extractCompleters() function and completers Map optimization
- CompleteCallback type import (unused)
- completers field from RegisteredPrompt

The original approach directly inspects the schema at completion request
time using getSchemaShape/isCompletable/getCompleter.
): PromptHandler {
if (argsSchema) {
const typedCallback = callback as (args: SchemaOutput<AnySchema>, ctx: ServerContext) => GetPromptResult | Promise<GetPromptResult>;
const typedCallback = callback as (args: unknown, ctx: ServerContext) => GetPromptResult | Promise<GetPromptResult>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the change to unknown?

@felixweinberger
Copy link
Contributor

One thought for a potential follow-up: now that the SDK is moving to schema-library-agnostic types, the completable() API could be simplified to decouple it from Zod entirely.

Instead of embedding completion callbacks inside schema objects (which requires Zod-specific introspection to extract), completers could be passed explicitly in the prompt config:

server.registerPrompt('greeting', {
  argsSchema: type({ name: 'string', language: 'string' }),  // any library
  completers: {
    name: (value) => ['Alice', 'Bob'].filter(n => n.startsWith(value)),
    language: (value) => ['en', 'fr'].filter(l => l.startsWith(value))
  }
}, callback);

The type simplifies too, since completion values are always strings over the wire:

type CompleteCallback = (
  value: string,
  context?: { arguments?: Record<string, string> }
) => string[] | Promise<string[]>;

This PR already does most of the internal plumbing — RegisteredPrompt stores completers?: Map<string, CompleteCallback>. The remaining step would be accepting them in the config directly, which would let us deprecate completable(), extractCompleters, and the Zod-specific helpers (getZodSchemaShape, unwrapZodOptionalSchema, etc.).

Not suggesting this as a blocker for this PR — just flagging as a natural follow-up that would complete the schema-agnostic story.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants