Skip to content
Open
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
7 changes: 5 additions & 2 deletions examples/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,17 @@
"dependencies": {
"@hono/node-server": "catalog:runtimeServerOnly",
"@modelcontextprotocol/examples-shared": "workspace:^",
"@modelcontextprotocol/node": "workspace:^",
"@modelcontextprotocol/server": "workspace:^",
"@modelcontextprotocol/express": "workspace:^",
"@modelcontextprotocol/hono": "workspace:^",
"@modelcontextprotocol/node": "workspace:^",
"@modelcontextprotocol/server": "workspace:^",
"@valibot/to-json-schema": "catalog:devTools",
"arktype": "catalog:devTools",
"better-auth": "^1.4.17",
"cors": "catalog:runtimeServerOnly",
"express": "catalog:runtimeServerOnly",
"hono": "catalog:runtimeServerOnly",
"valibot": "catalog:devTools",
"zod": "catalog:runtimeShared"
},
"devDependencies": {
Expand Down
28 changes: 28 additions & 0 deletions examples/server/src/arktypeExample.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env node
/**
* Minimal MCP server using ArkType for schema validation.
* ArkType implements the Standard Schema spec with built-in JSON Schema conversion.
*/

import { McpServer, StdioServerTransport } from '@modelcontextprotocol/server';
import { type } from 'arktype';

const server = new McpServer({
name: 'arktype-example',
version: '1.0.0'
});

// Register a tool with ArkType schema
server.registerTool(
'greet',
{
description: 'Generate a greeting',
inputSchema: type({ name: 'string' })
},
async ({ name }) => ({
content: [{ type: 'text', text: `Hello, ${name}!` }]
})
);

const transport = new StdioServerTransport();
await server.connect(transport);
30 changes: 30 additions & 0 deletions examples/server/src/valibotExample.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env node
/**
* Minimal MCP server using Valibot for schema validation.
* Use toStandardJsonSchema() from @valibot/to-json-schema to create
* StandardJSONSchemaV1-compliant schemas.
*/

import { McpServer, StdioServerTransport } from '@modelcontextprotocol/server';
import { toStandardJsonSchema } from '@valibot/to-json-schema';
import * as v from 'valibot';

const server = new McpServer({
name: 'valibot-example',
version: '1.0.0'
});

// Register a tool with Valibot schema
server.registerTool(
'greet',
{
description: 'Generate a greeting',
inputSchema: toStandardJsonSchema(v.object({ name: v.string() }))
},
async ({ name }) => ({
content: [{ type: 'text', text: `Hello, ${name}!` }]
})
);

const transport = new StdioServerTransport();
await server.connect(transport);
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from './shared/uriTemplate.js';
export * from './types/types.js';
export * from './util/inMemory.js';
export * from './util/schema.js';
export * from './util/standardSchema.js';

// experimental exports
export * from './experimental/index.js';
Expand Down
11 changes: 6 additions & 5 deletions packages/core/src/util/schema.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
/**
* Internal Zod schema utilities for protocol handling.
* These are used internally by the SDK for protocol message validation.
*/

import * as z from 'zod/v4';

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

/**
* A Zod schema for objects specifically (not unions).
* Use this when you need to constrain to ZodObject schemas.
* A Zod schema for objects specifically.
*/
export type AnyObjectSchema = z.core.$ZodObject;

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

/**
* Checks if a schema is optional (accepts undefined).
* Uses the public .type property which works in both zod/v4 and zod/v4/mini.
*/
export function isOptionalSchema(schema: AnySchema): boolean {
const candidate = schema as { type?: string };
Expand All @@ -83,7 +85,6 @@ export function isOptionalSchema(schema: AnySchema): boolean {
/**
* Unwraps an optional schema to get the inner schema.
* If the schema is not optional, returns it unchanged.
* Uses the public .def.innerType property which works in both zod/v4 and zod/v4/mini.
*/
export function unwrapOptionalSchema(schema: AnySchema): AnySchema {
if (!isOptionalSchema(schema)) {
Expand Down
179 changes: 179 additions & 0 deletions packages/core/src/util/standardSchema.ts
Original file line number Diff line number Diff line change
@@ -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.

* Standard Schema utilities for user-provided schemas.
* Supports Zod v4, Valibot, ArkType, and other Standard Schema implementations.
* @see https://standardschema.dev
*/

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

import type { JsonSchemaType, jsonSchemaValidator } from '../validation/types.js';

// Standard Schema interfaces (from https://standardschema.dev)

export interface StandardTypedV1<Input = unknown, Output = Input> {
readonly '~standard': StandardTypedV1.Props<Input, Output>;
}

export namespace StandardTypedV1 {
export interface Props<Input = unknown, Output = Input> {
readonly version: 1;
readonly vendor: string;
readonly types?: Types<Input, Output> | undefined;
}

export interface Types<Input = unknown, Output = Input> {
readonly input: Input;
readonly output: Output;
}

export type InferInput<Schema extends StandardTypedV1> = NonNullable<Schema['~standard']['types']>['input'];
export type InferOutput<Schema extends StandardTypedV1> = NonNullable<Schema['~standard']['types']>['output'];
}

export interface StandardSchemaV1<Input = unknown, Output = Input> {
readonly '~standard': StandardSchemaV1.Props<Input, Output>;
}

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

export interface Options {
readonly libraryOptions?: Record<string, unknown> | undefined;
}

export type Result<Output> = SuccessResult<Output> | FailureResult;

export interface SuccessResult<Output> {
readonly value: Output;
readonly issues?: undefined;
}

export interface FailureResult {
readonly issues: ReadonlyArray<Issue>;
}

export interface Issue {
readonly message: string;
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
}

export interface PathSegment {
readonly key: PropertyKey;
}

export type InferInput<Schema extends StandardTypedV1> = StandardTypedV1.InferInput<Schema>;
export type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
}

export interface StandardJSONSchemaV1<Input = unknown, Output = Input> {
readonly '~standard': StandardJSONSchemaV1.Props<Input, Output>;
}

export namespace StandardJSONSchemaV1 {
export interface Props<Input = unknown, Output = Input> extends StandardTypedV1.Props<Input, Output> {
readonly jsonSchema: Converter;
}

export interface Converter {
readonly input: (options: Options) => Record<string, unknown>;
readonly output: (options: Options) => Record<string, unknown>;
}

export type Target = 'draft-2020-12' | 'draft-07' | 'openapi-3.0' | (object & string);

export interface Options {
readonly target: Target;
readonly libraryOptions?: Record<string, unknown> | undefined;
}

export type InferInput<Schema extends StandardTypedV1> = StandardTypedV1.InferInput<Schema>;
export type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
}

/** Combined interface for schemas with both validation and JSON Schema conversion (e.g., Zod v4). */
export interface StandardSchemaWithJSON<Input = unknown, Output = Input> {
readonly '~standard': StandardSchemaV1.Props<Input, Output> & StandardJSONSchemaV1.Props<Input, Output>;
}

// Type guards

export function isStandardJSONSchema(schema: unknown): schema is StandardJSONSchemaV1 {
if (schema == null) return false;
const schemaType = typeof schema;
if (schemaType !== 'object' && schemaType !== 'function') return false;
if (!('~standard' in (schema as object))) return false;
const std = (schema as StandardJSONSchemaV1)['~standard'];
return typeof std?.jsonSchema?.input === 'function' && typeof std?.jsonSchema?.output === 'function';
}

export function isStandardSchema(schema: unknown): schema is StandardSchemaV1 {
if (schema == null) return false;
const schemaType = typeof schema;
if (schemaType !== 'object' && schemaType !== 'function') return false;
if (!('~standard' in (schema as object))) return false;
const std = (schema as StandardSchemaV1)['~standard'];
return typeof std?.validate === 'function';
}

export function isStandardSchemaWithJSON(schema: unknown): schema is StandardSchemaWithJSON {
return isStandardJSONSchema(schema) && isStandardSchema(schema);
}

// JSON Schema conversion

export function standardSchemaToJsonSchema(schema: StandardJSONSchemaV1, io: 'input' | 'output' = 'input'): Record<string, unknown> {
return schema['~standard'].jsonSchema[io]({ target: 'draft-2020-12' });
}

// Validation

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

export async function validateStandardSchema<T extends StandardJSONSchemaV1>(
schema: T,
data: unknown,
jsonSchemaValidatorInstance?: jsonSchemaValidator
): Promise<StandardSchemaValidationResult<StandardJSONSchemaV1.InferOutput<T>>> {
// Use native validation if available
if (isStandardSchema(schema)) {
const result = await schema['~standard'].validate(data);
if (result.issues && result.issues.length > 0) {
const errorMessage = result.issues.map((i: StandardSchemaV1.Issue) => i.message).join(', ');
return { success: false, error: errorMessage };
}
return { success: true, data: (result as StandardSchemaV1.SuccessResult<unknown>).value as StandardJSONSchemaV1.InferOutput<T> };
}

// Fall back to JSON Schema validation
if (jsonSchemaValidatorInstance) {
const jsonSchema = standardSchemaToJsonSchema(schema, 'input');
const validator = jsonSchemaValidatorInstance.getValidator<StandardJSONSchemaV1.InferOutput<T>>(jsonSchema as JsonSchemaType);
const validationResult = validator(data);

if (validationResult.valid) {
return { success: true, data: validationResult.data };
}
return { success: false, error: validationResult.errorMessage ?? 'Validation failed' };
}

// No validation - trust the data
return { success: true, data: data as StandardJSONSchemaV1.InferOutput<T> };
}

// Prompt argument extraction

export function promptArgumentsFromStandardSchema(
schema: StandardJSONSchemaV1
): Array<{ name: string; description?: string; required: boolean }> {
const jsonSchema = standardSchemaToJsonSchema(schema, 'input');
const properties = (jsonSchema.properties as Record<string, { description?: string }>) || {};
const required = (jsonSchema.required as string[]) || [];

return Object.entries(properties).map(([name, prop]) => ({
name,
description: prop?.description,
required: required.includes(name)
}));
}
17 changes: 8 additions & 9 deletions packages/server/src/experimental/tasks/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
*/

import type {
AnySchema,
CallToolResult,
CreateTaskResult,
CreateTaskServerContext,
GetTaskResult,
Result,
StandardJSONSchemaV1,
TaskServerContext
} from '@modelcontextprotocol/core';

Expand All @@ -23,18 +23,17 @@ import type { BaseToolCallback } from '../../server/mcp.js';
* Handler for creating a task.
* @experimental
*/
export type CreateTaskRequestHandler<ResultT extends Result, Args extends AnySchema | undefined = undefined> = BaseToolCallback<
ResultT,
CreateTaskServerContext,
Args
>;
export type CreateTaskRequestHandler<
SendResultT extends Result,
Args extends StandardJSONSchemaV1 | undefined = undefined
> = BaseToolCallback<SendResultT, CreateTaskServerContext, Args>;

/**
* Handler for task operations (get, getResult).
* @experimental
*/
export type TaskRequestHandler<ResultT extends Result, Args extends AnySchema | undefined = undefined> = BaseToolCallback<
ResultT,
export type TaskRequestHandler<SendResultT extends Result, Args extends StandardJSONSchemaV1 | undefined = undefined> = BaseToolCallback<
SendResultT,
TaskServerContext,
Args
>;
Expand All @@ -43,7 +42,7 @@ export type TaskRequestHandler<ResultT extends Result, Args extends AnySchema |
* Interface for task-based tool handlers.
* @experimental
*/
export interface ToolTaskHandler<Args extends AnySchema | undefined = undefined> {
export interface ToolTaskHandler<Args extends StandardJSONSchemaV1 | undefined = undefined> {
createTask: CreateTaskRequestHandler<CreateTaskResult, Args>;
getTask: TaskRequestHandler<GetTaskResult, Args>;
getTaskResult: TaskRequestHandler<CallToolResult, Args>;
Expand Down
Loading
Loading