Skip to content
18 changes: 4 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,7 @@
"express-rate-limit": "^7.5.0",
"pkce-challenge": "^5.0.0",
"raw-body": "^3.0.0",
"zod": "^3.23.8",
"zod-to-json-schema": "^3.24.1"
"zod": "^4.1.9"
},
"devDependencies": {
"@eslint/js": "^9.8.0",
Expand Down
4 changes: 2 additions & 2 deletions src/examples/server/simpleSseServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ const getServer = () => {
'start-notification-stream',
'Starts sending periodic notifications',
{
interval: z.number().describe('Interval in milliseconds between notifications').default(1000),
count: z.number().describe('Number of notifications to send').default(10)
interval: z.number().describe('Interval in milliseconds between notifications').prefault(1000),
count: z.number().describe('Number of notifications to send').prefault(10)
},
async ({ interval, count }, extra): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
Expand Down
4 changes: 2 additions & 2 deletions src/examples/server/simpleStatelessStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ const getServer = () => {
'start-notification-stream',
'Starts sending periodic notifications for testing resumability',
{
interval: z.number().describe('Interval in milliseconds between notifications').default(100),
count: z.number().describe('Number of notifications to send (0 for 100)').default(10)
interval: z.number().describe('Interval in milliseconds between notifications').prefault(100),
count: z.number().describe('Number of notifications to send (0 for 100)').prefault(10)
},
async ({ interval, count }, extra): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
Expand Down
4 changes: 2 additions & 2 deletions src/examples/server/simpleStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,8 @@ const getServer = () => {
'start-notification-stream',
'Starts sending periodic notifications for testing resumability',
{
interval: z.number().describe('Interval in milliseconds between notifications').default(100),
count: z.number().describe('Number of notifications to send (0 for 100)').default(50)
interval: z.number().describe('Interval in milliseconds between notifications').prefault(100),
count: z.number().describe('Number of notifications to send (0 for 100)').prefault(50)
},
async ({ interval, count }, extra): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
Expand Down
4 changes: 2 additions & 2 deletions src/examples/server/sseAndStreamableHttpCompatibleServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ const getServer = () => {
'start-notification-stream',
'Starts sending periodic notifications for testing resumability',
{
interval: z.number().describe('Interval in milliseconds between notifications').default(100),
count: z.number().describe('Number of notifications to send (0 for 100)').default(50)
interval: z.number().describe('Interval in milliseconds between notifications').prefault(100),
count: z.number().describe('Number of notifications to send (0 for 100)').prefault(50)
},
async ({ interval, count }, extra): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe('Streamable HTTP Transport Session Management', () => {
'greet',
'A simple greeting tool',
{
name: z.string().describe('Name to greet').default('World')
name: z.string().describe('Name to greet').prefault('World')
},
async ({ name }) => {
return {
Expand Down
6 changes: 3 additions & 3 deletions src/integration-tests/taskResumability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('Transport resumability', () => {
'send-notification',
'Sends a single notification',
{
message: z.string().describe('Message to send').default('Test notification')
message: z.string().describe('Message to send').prefault('Test notification')
},
async ({ message }, { sendNotification }) => {
// Send notification immediately
Expand All @@ -51,8 +51,8 @@ describe('Transport resumability', () => {
'run-notifications',
'Sends multiple notifications over time',
{
count: z.number().describe('Number of notifications to send').default(10),
interval: z.number().describe('Interval between notifications in ms').default(50)
count: z.number().describe('Number of notifications to send').prefault(10),
interval: z.number().describe('Interval between notifications in ms').prefault(50)
},
async ({ count, interval }, { sendNotification }) => {
// Send notifications at specified intervals
Expand Down
6 changes: 4 additions & 2 deletions src/server/auth/handlers/authorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ const ClientAuthorizationParamsSchema = z.object({
redirect_uri: z
.string()
.optional()
.refine(value => value === undefined || URL.canParse(value), { message: 'redirect_uri must be a valid URL' })
.refine(value => value === undefined || URL.canParse(value), {
error: 'redirect_uri must be a valid URL'
})
});

// Parameters that must be validated for a successful authorization request. Failure can be reported to the redirect URI.
Expand All @@ -31,7 +33,7 @@ const RequestAuthorizationParamsSchema = z.object({
code_challenge_method: z.literal('S256'),
scope: z.string().optional(),
state: z.string().optional(),
resource: z.string().url().optional()
resource: z.url().optional()
});

export function authorizationHandler({ provider, rateLimit: rateLimitConfig }: AuthorizationHandlerOptions): RequestHandler {
Expand Down
4 changes: 2 additions & 2 deletions src/server/auth/handlers/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ const AuthorizationCodeGrantSchema = z.object({
code: z.string(),
code_verifier: z.string(),
redirect_uri: z.string().optional(),
resource: z.string().url().optional()
resource: z.url().optional()
});

const RefreshTokenGrantSchema = z.object({
refresh_token: z.string(),
scope: z.string().optional(),
resource: z.string().url().optional()
resource: z.url().optional()
});

export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHandlerOptions): RequestHandler {
Expand Down
97 changes: 43 additions & 54 deletions src/server/completable.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ZodTypeAny, ZodTypeDef, ZodType, ParseInput, ParseReturnType, RawCreateParams, ZodErrorMap, ProcessedCreateParams } from 'zod';
import { ZodTypeAny } from 'zod';

export enum McpZodTypeKind {
Completable = 'McpCompletable'
Expand All @@ -11,69 +11,58 @@ export type CompleteCallback<T extends ZodTypeAny = ZodTypeAny> = (
}
) => T['_input'][] | Promise<T['_input'][]>;

export interface CompletableDef<T extends ZodTypeAny = ZodTypeAny> extends ZodTypeDef {
export interface CompletableDef<T extends ZodTypeAny = ZodTypeAny> {
type: T;
complete: CompleteCallback<T>;
typeName: McpZodTypeKind.Completable;
}

export class Completable<T extends ZodTypeAny> extends ZodType<T['_output'], CompletableDef<T>, T['_input']> {
_parse(input: ParseInput): ParseReturnType<this['_output']> {
const { ctx } = this._processInputParams(input);
const data = ctx.data;
return this._def.type._parse({
data,
path: ctx.path,
parent: ctx
});
}

unwrap() {
return this._def.type;
}

static create = <T extends ZodTypeAny>(
type: T,
params: RawCreateParams & {
complete: CompleteCallback<T>;
}
): Completable<T> => {
return new Completable({
type,
typeName: McpZodTypeKind.Completable,
complete: params.complete,
...processCreateParams(params)
});
};
}
/**
* A Zod schema that has been wrapped with completion capabilities.
*/
export type CompletableSchema<T extends ZodTypeAny> = T & { _def: T['_def'] & CompletableDef<T> };

/**
* Wraps a Zod type to provide autocompletion capabilities. Useful for, e.g., prompt arguments in MCP.
*
* Uses an immutable wrapper approach that creates a new schema object with completion metadata
* while preserving all validation behavior of the underlying schema.
*/
export function completable<T extends ZodTypeAny>(schema: T, complete: CompleteCallback<T>): Completable<T> {
return Completable.create(schema, { ...schema._def, complete });
}

// Not sure why this isn't exported from Zod:
// https://github.com/colinhacks/zod/blob/f7ad26147ba291cb3fb257545972a8e00e767470/src/types.ts#L130
function processCreateParams(params: RawCreateParams): ProcessedCreateParams {
if (!params) return {};
const { errorMap, invalid_type_error, required_error, description } = params;
if (errorMap && (invalid_type_error || required_error)) {
throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`);
}
if (errorMap) return { errorMap: errorMap, description };
const customMap: ZodErrorMap = (iss, ctx) => {
const { message } = params;
export function completable<T extends ZodTypeAny>(schema: T, complete: CompleteCallback<T>): CompletableSchema<T> {
// Create new schema object inheriting from original
const wrapped = Object.create(Object.getPrototypeOf(schema));

if (iss.code === 'invalid_enum_value') {
return { message: message ?? ctx.defaultError };
// Copy all properties including getters/setters (except _def and _zod which we'll redefine)
Object.getOwnPropertyNames(schema).forEach(key => {
if (key !== '_def' && key !== '_zod') {
const descriptor = Object.getOwnPropertyDescriptor(schema, key);
if (descriptor) {
Object.defineProperty(wrapped, key, descriptor);
}
}
if (typeof ctx.data === 'undefined') {
return { message: message ?? required_error ?? ctx.defaultError };
}
if (iss.code !== 'invalid_type') return { message: ctx.defaultError };
return { message: message ?? invalid_type_error ?? ctx.defaultError };
});

// Create new def with added completion metadata
const newDef = {
...schema._def,
typeName: McpZodTypeKind.Completable,
type: schema,
complete
};
return { errorMap: customMap, description };

// Set _def as read-only property (matching Zod's design)
Object.defineProperty(wrapped, '_def', {
value: newDef,
writable: false,
enumerable: false,
configurable: false
});

// Update _zod to maintain _def === _zod.def invariant
wrapped._zod = {
...schema._zod,
def: newDef
};

return wrapped as CompletableSchema<T>;
}
28 changes: 12 additions & 16 deletions src/server/mcp.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Server, ServerOptions } from './index.js';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { z, ZodRawShape, ZodObject, ZodString, AnyZodObject, ZodTypeAny, ZodType, ZodTypeDef, ZodOptional } from 'zod';
import { z, ZodRawShape, ZodObject, ZodString, ZodType, ZodOptional } from 'zod';
import {
Implementation,
Tool,
Expand Down Expand Up @@ -33,7 +32,7 @@ import {
ToolAnnotations,
LoggingMessageNotification
} from '../types.js';
import { Completable, CompletableDef } from './completable.js';
import { CompletableDef, McpZodTypeKind } from './completable.js';
import { UriTemplate, Variables } from '../shared/uriTemplate.js';
import { RequestHandlerExtra } from '../shared/protocol.js';
import { Transport } from '../shared/transport.js';
Expand Down Expand Up @@ -103,18 +102,14 @@ export class McpServer {
title: tool.title,
description: tool.description,
inputSchema: tool.inputSchema
? (zodToJsonSchema(tool.inputSchema, {
strictUnions: true
}) as Tool['inputSchema'])
? (z.toJSONSchema(tool.inputSchema) as Tool['inputSchema'])
: EMPTY_OBJECT_JSON_SCHEMA,
annotations: tool.annotations,
_meta: tool._meta
};

if (tool.outputSchema) {
toolDefinition.outputSchema = zodToJsonSchema(tool.outputSchema, {
strictUnions: true
}) as Tool['outputSchema'];
toolDefinition.outputSchema = z.toJSONSchema(tool.outputSchema) as Tool['outputSchema'];
}

return toolDefinition;
Expand Down Expand Up @@ -243,11 +238,12 @@ export class McpServer {
}

const field = prompt.argsSchema.shape[request.params.argument.name];
if (!(field instanceof Completable)) {
const defLike = (field as unknown as { _def?: { typeName?: unknown } })._def;
if (!defLike || defLike.typeName !== McpZodTypeKind.Completable) {
return EMPTY_COMPLETION_RESULT;
}

const def: CompletableDef<ZodString> = field._def;
const def: CompletableDef<ZodString> = (field as unknown as { _def: CompletableDef<ZodString> })._def;
const suggestions = await def.complete(request.params.argument.value, request.params.context);
return createCompletionResult(suggestions);
}
Expand Down Expand Up @@ -1013,16 +1009,16 @@ export class ResourceTemplate {
*/
export type ToolCallback<Args extends undefined | ZodRawShape = undefined> = Args extends ZodRawShape
? (
args: z.objectOutputType<Args, ZodTypeAny>,
args: z.infer<ZodObject<Args>>,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
) => CallToolResult | Promise<CallToolResult>
: (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResult | Promise<CallToolResult>;

export type RegisteredTool = {
title?: string;
description?: string;
inputSchema?: AnyZodObject;
outputSchema?: AnyZodObject;
inputSchema?: ZodObject<ZodRawShape>;
outputSchema?: ZodObject<ZodRawShape>;
annotations?: ToolAnnotations;
_meta?: Record<string, unknown>;
callback: ToolCallback<undefined | ZodRawShape>;
Expand Down Expand Up @@ -1138,12 +1134,12 @@ export type RegisteredResourceTemplate = {
};

type PromptArgsRawShape = {
[k: string]: ZodType<string, ZodTypeDef, string> | ZodOptional<ZodType<string, ZodTypeDef, string>>;
[k: string]: ZodString | ZodOptional<ZodString>;
};

export type PromptCallback<Args extends undefined | PromptArgsRawShape = undefined> = Args extends PromptArgsRawShape
? (
args: z.objectOutputType<Args, ZodTypeAny>,
args: z.infer<ZodObject<Args>>,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>
) => GetPromptResult | Promise<GetPromptResult>
: (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => GetPromptResult | Promise<GetPromptResult>;
Expand Down
Loading