Skip to content

Commit f46f2ac

Browse files
fix: rewrite Completable using immutable wrapper for Zod v4
The previous class-based approach that extended ZodType no longer works in Zod v4 due to architectural changes. This implements an immutable wrapper approach that creates a new schema object with completion metadata while preserving all validation behavior. - Creates new schema via Object.create() + property copying - Adds completion metadata to new _def object (not mutating shared state) - Maintains _def === _zod.def invariant - Preserves all schema properties including getters - Follows Zod's immutability pattern Also updates mcp.ts to check _def.typeName instead of instanceof. All 755 tests pass.
1 parent 9563135 commit f46f2ac

File tree

2 files changed

+46
-58
lines changed

2 files changed

+46
-58
lines changed

src/server/completable.ts

Lines changed: 42 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ZodTypeAny, ZodTypeDef, ZodType, ParseInput, ParseReturnType, RawCreateParams, ZodErrorMap, ProcessedCreateParams } from 'zod';
1+
import { ZodTypeAny } from 'zod';
22

33
export enum McpZodTypeKind {
44
Completable = 'McpCompletable'
@@ -11,69 +11,56 @@ export type CompleteCallback<T extends ZodTypeAny = ZodTypeAny> = (
1111
}
1212
) => T['_input'][] | Promise<T['_input'][]>;
1313

14-
export interface CompletableDef<T extends ZodTypeAny = ZodTypeAny> extends ZodTypeDef {
14+
export interface CompletableDef<T extends ZodTypeAny = ZodTypeAny> {
1515
type: T;
1616
complete: CompleteCallback<T>;
1717
typeName: McpZodTypeKind.Completable;
1818
}
1919

20-
export class Completable<T extends ZodTypeAny> extends ZodType<T['_output'], CompletableDef<T>, T['_input']> {
21-
_parse(input: ParseInput): ParseReturnType<this['_output']> {
22-
const { ctx } = this._processInputParams(input);
23-
const data = ctx.data;
24-
return this._def.type._parse({
25-
data,
26-
path: ctx.path,
27-
parent: ctx
28-
});
29-
}
30-
31-
unwrap() {
32-
return this._def.type;
33-
}
34-
35-
static create = <T extends ZodTypeAny>(
36-
type: T,
37-
params: RawCreateParams & {
38-
complete: CompleteCallback<T>;
39-
}
40-
): Completable<T> => {
41-
return new Completable({
42-
type,
43-
typeName: McpZodTypeKind.Completable,
44-
complete: params.complete,
45-
...processCreateParams(params)
46-
});
47-
};
48-
}
49-
5020
/**
5121
* Wraps a Zod type to provide autocompletion capabilities. Useful for, e.g., prompt arguments in MCP.
22+
*
23+
* Uses an immutable wrapper approach that creates a new schema object with completion metadata
24+
* while preserving all validation behavior of the underlying schema.
5225
*/
53-
export function completable<T extends ZodTypeAny>(schema: T, complete: CompleteCallback<T>): Completable<T> {
54-
return Completable.create(schema, { ...schema._def, complete });
55-
}
26+
export function completable<T extends ZodTypeAny>(
27+
schema: T,
28+
complete: CompleteCallback<T>
29+
): T & { _def: T['_def'] & CompletableDef<T> } {
30+
// Create new schema object inheriting from original
31+
const wrapped = Object.create(Object.getPrototypeOf(schema));
5632

57-
// Not sure why this isn't exported from Zod:
58-
// https://github.com/colinhacks/zod/blob/f7ad26147ba291cb3fb257545972a8e00e767470/src/types.ts#L130
59-
function processCreateParams(params: RawCreateParams): ProcessedCreateParams {
60-
if (!params) return {};
61-
const { errorMap, invalid_type_error, required_error, description } = params;
62-
if (errorMap && (invalid_type_error || required_error)) {
63-
throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`);
64-
}
65-
if (errorMap) return { errorMap: errorMap, description };
66-
const customMap: ZodErrorMap = (iss, ctx) => {
67-
const { message } = params;
68-
69-
if (iss.code === 'invalid_enum_value') {
70-
return { message: message ?? ctx.defaultError };
33+
// Copy all properties including getters/setters (except _def and _zod which we'll redefine)
34+
Object.getOwnPropertyNames(schema).forEach(key => {
35+
if (key !== '_def' && key !== '_zod') {
36+
const descriptor = Object.getOwnPropertyDescriptor(schema, key);
37+
if (descriptor) {
38+
Object.defineProperty(wrapped, key, descriptor);
39+
}
7140
}
72-
if (typeof ctx.data === 'undefined') {
73-
return { message: message ?? required_error ?? ctx.defaultError };
74-
}
75-
if (iss.code !== 'invalid_type') return { message: ctx.defaultError };
76-
return { message: message ?? invalid_type_error ?? ctx.defaultError };
41+
});
42+
43+
// Create new def with added completion metadata
44+
const newDef = {
45+
...schema._def,
46+
typeName: McpZodTypeKind.Completable,
47+
type: schema,
48+
complete
7749
};
78-
return { errorMap: customMap, description };
50+
51+
// Set _def as read-only property (matching Zod's design)
52+
Object.defineProperty(wrapped, '_def', {
53+
value: newDef,
54+
writable: false,
55+
enumerable: false,
56+
configurable: false
57+
});
58+
59+
// Update _zod to maintain _def === _zod.def invariant
60+
wrapped._zod = {
61+
...schema._zod,
62+
def: newDef
63+
};
64+
65+
return wrapped as T & { _def: T['_def'] & CompletableDef<T> };
7966
}

src/server/mcp.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
ToolAnnotations,
3333
LoggingMessageNotification
3434
} from '../types.js';
35-
import { Completable, CompletableDef } from './completable.js';
35+
import { CompletableDef, McpZodTypeKind } from './completable.js';
3636
import { UriTemplate, Variables } from '../shared/uriTemplate.js';
3737
import { RequestHandlerExtra } from '../shared/protocol.js';
3838
import { Transport } from '../shared/transport.js';
@@ -238,11 +238,12 @@ export class McpServer {
238238
}
239239

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

245-
const def: CompletableDef<ZodString> = field._def;
246+
const def: CompletableDef<ZodString> = (field as unknown as { _def: CompletableDef<ZodString> })._def;
246247
const suggestions = await def.complete(request.params.argument.value, request.params.context);
247248
return createCompletionResult(suggestions);
248249
}

0 commit comments

Comments
 (0)