Skip to content
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export default {
"/node_modules/(?!eventsource)/"
],
testPathIgnorePatterns: ["/node_modules/", "/dist/"],
modulePathIgnorePatterns: ["<rootDir>/dist"],
};
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 @@ -72,8 +72,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.0.17"
},
"devDependencies": {
"@eslint/js": "^9.8.0",
Expand Down
2 changes: 1 addition & 1 deletion src/client/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/* eslint-disable no-constant-binary-expression */
/* eslint-disable @typescript-eslint/no-unused-expressions */
import { Client } from "./index.js";
import { z } from "zod";
import { z } from "zod/v4";
import {
RequestSchema,
NotificationSchema,
Expand Down
2 changes: 1 addition & 1 deletion src/examples/server/jsonResponseStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import express, { Request, Response } from 'express';
import { randomUUID } from 'node:crypto';
import { McpServer } from '../../server/mcp.js';
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
import { z } from 'zod';
import { z } from 'zod/v4';
import { CallToolResult, isInitializeRequest } from '../../types.js';
import cors from 'cors';

Expand Down
2 changes: 1 addition & 1 deletion src/examples/server/mcpServerOutputSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { McpServer } from "../../server/mcp.js";
import { StdioServerTransport } from "../../server/stdio.js";
import { z } from "zod";
import { z } from "zod/v4";

const server = new McpServer(
{
Expand Down
6 changes: 3 additions & 3 deletions src/examples/server/simpleSseServer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import express, { Request, Response } from 'express';
import { McpServer } from '../../server/mcp.js';
import { SSEServerTransport } from '../../server/sse.js';
import { z } from 'zod';
import { z } from 'zod/v4';
import { CallToolResult } from '../../types.js';

/**
Expand All @@ -25,8 +25,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),

Choose a reason for hiding this comment

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

why is prefault preferred in this case?
as far as I understand from https://zod.dev/v4/changelog#default-updates it's about expected input type (in their example it's a string) and output type (in their example the string goes through transform to output a number)

looks like here default will work as well since input type is same as output type - number

Copy link
Author

Choose a reason for hiding this comment

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

yeah we can use default! let me get it updated. the util I used defaults to prefault to preserve default functionality so I left it as is.

count: z.number().describe('Number of notifications to send').prefault(10),
},
async ({ interval, count }, { sendNotification }): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
Expand Down
6 changes: 3 additions & 3 deletions src/examples/server/simpleStatelessStreamableHttp.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import express, { Request, Response } from 'express';
import { McpServer } from '../../server/mcp.js';
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
import { z } from 'zod';
import { z } from 'zod/v4';
import { CallToolResult, GetPromptResult, ReadResourceResult } from '../../types.js';
import cors from 'cors';

Expand Down Expand Up @@ -39,8 +39,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 }, { sendNotification }): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
Expand Down
6 changes: 3 additions & 3 deletions src/examples/server/simpleStreamableHttp.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import express, { Request, Response } from 'express';
import { randomUUID } from 'node:crypto';
import { z } from 'zod';
import { z } from 'zod/v4';
import { McpServer } from '../../server/mcp.js';
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from '../../server/auth/router.js';
Expand Down Expand Up @@ -270,8 +270,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 }, { sendNotification }): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
Expand Down
6 changes: 3 additions & 3 deletions src/examples/server/sseAndStreamableHttpCompatibleServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
import { McpServer } from '../../server/mcp.js';
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
import { SSEServerTransport } from '../../server/sse.js';
import { z } from 'zod';
import { z } from 'zod/v4';
import { CallToolResult, isInitializeRequest } from '../../types.js';
import { InMemoryEventStore } from '../shared/inMemoryEventStore.js';
import cors from 'cors';
Expand All @@ -30,8 +30,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 }, { sendNotification }): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
Expand Down
2 changes: 1 addition & 1 deletion src/examples/server/toolWithSampleServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { McpServer } from "../../server/mcp.js";
import { StdioServerTransport } from "../../server/stdio.js";
import { z } from "zod";
import { z } from "zod/v4";

const mcpServer = new McpServer({
name: "tools-with-sample-server",
Expand Down
4 changes: 2 additions & 2 deletions src/integration-tests/stateManagementStreamableHttp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { StreamableHTTPClientTransport } from '../client/streamableHttp.js';
import { McpServer } from '../server/mcp.js';
import { StreamableHTTPServerTransport } from '../server/streamableHttp.js';
import { CallToolResultSchema, ListToolsResultSchema, ListResourcesResultSchema, ListPromptsResultSchema, LATEST_PROTOCOL_VERSION } from '../types.js';
import { z } from 'zod';
import { z } from 'zod/v4';

describe('Streamable HTTP Transport Session Management', () => {
// Function to set up the server with optional session management
Expand Down Expand Up @@ -55,7 +55,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
8 changes: 4 additions & 4 deletions src/integration-tests/taskResumability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { StreamableHTTPClientTransport } from '../client/streamableHttp.js';
import { McpServer } from '../server/mcp.js';
import { StreamableHTTPServerTransport } from '../server/streamableHttp.js';
import { CallToolResultSchema, LoggingMessageNotificationSchema } from '../types.js';
import { z } from 'zod';
import { z } from 'zod/v4';
import { InMemoryEventStore } from '../examples/shared/inMemoryEventStore.js';


Expand All @@ -33,7 +33,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 @@ -56,8 +56,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
8 changes: 5 additions & 3 deletions src/server/auth/handlers/authorize.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RequestHandler } from "express";
import { z } from "zod";
import { z } from "zod/v4";
import express from "express";
import { OAuthServerProvider } from "../provider.js";
import { rateLimit, Options as RateLimitOptions } from "express-rate-limit";
Expand All @@ -25,7 +25,9 @@ export type AuthorizationHandlerOptions = {
// Parameters that must be validated in order to issue redirects.
const ClientAuthorizationParamsSchema = z.object({
client_id: z.string(),
redirect_uri: z.string().optional().refine((value) => value === undefined || URL.canParse(value), { message: "redirect_uri must be a valid URL" }),
redirect_uri: z.string().optional().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 @@ -35,7 +37,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
6 changes: 3 additions & 3 deletions src/server/auth/handlers/token.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from "zod";
import { z } from "zod/v4";
import express, { RequestHandler } from "express";
import { OAuthServerProvider } from "../provider.js";
import cors from "cors";
Expand Down 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
2 changes: 1 addition & 1 deletion src/server/auth/middleware/clientAuth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from "zod";
import { z } from "zod/v4";
import { RequestHandler } from "express";
import { OAuthRegisteredClientsStore } from "../clients.js";
import { OAuthClientInformationFull } from "../../../shared/auth.js";
Expand Down
2 changes: 1 addition & 1 deletion src/server/completable.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from "zod";
import { z } from "zod/v4";
import { completable } from "./completable.js";

describe("completable", () => {
Expand Down
95 changes: 21 additions & 74 deletions src/server/completable.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
import {
ZodTypeAny,
ZodTypeDef,
ZodType,
ParseInput,
ParseReturnType,
RawCreateParams,
ZodErrorMap,
ProcessedCreateParams,
} from "zod";
import { ZodTypeAny } from "zod/v4";

export enum McpZodTypeKind {
Completable = "McpCompletable",
Expand All @@ -17,82 +8,38 @@ export type CompleteCallback<T extends ZodTypeAny = ZodTypeAny> = (
value: T["_input"],
context?: {
arguments?: Record<string, string>;
},
}
) => 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),
});
};
}

/**
* Wraps a Zod type to provide autocompletion capabilities. Useful for, e.g., prompt arguments in MCP.
*/
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.`,
);
complete: CompleteCallback<T>
): T & {
_def: (T extends { _def: infer D } ? D : unknown) & CompletableDef<T>;
} {
const target = schema as unknown as { _def?: Record<string, unknown> };
const originalDef = (target._def ?? {}) as Record<string, unknown>;
// Only mutate the existing _def object to respect read-only property semantics
if (
(originalDef as { typeName?: unknown }).typeName !==
McpZodTypeKind.Completable
) {
(originalDef as { typeName?: McpZodTypeKind; type?: ZodTypeAny }).typeName =
McpZodTypeKind.Completable;
(originalDef as { typeName?: McpZodTypeKind; type?: ZodTypeAny }).type =
schema;
}
if (errorMap) return { errorMap: errorMap, description };
const customMap: ZodErrorMap = (iss, ctx) => {
const { message } = params;

if (iss.code === "invalid_enum_value") {
return { message: message ?? ctx.defaultError };
}
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 };
(originalDef as { complete?: CompleteCallback<T> }).complete = complete;
return schema as unknown as T & {
_def: (T extends { _def: infer D } ? D : unknown) & CompletableDef<T>;
};
return { errorMap: customMap, description };
}
Loading