Skip to content

Commit 3871100

Browse files
refactor: drop type assertions in v2 port
events.ts: convert property-override arrow functions to method overrides with declared overload signatures. The overrides now expose only the ZodSchema form (the only one this package uses); a guard skips tracking during super() construction (Protocol's ctor registers built-in handlers before our fields initialize). Adds _methodOf() helper, removes the MethodSchema local type and the eslint-disable directives. server/index.ts: split registerAppTool into two public overloads (StandardSchemaWithJSON and ZodRawShape forms) matching registerTool's, removing the union-args casts. The impl signature still loses the overload pairing, so the forwarding call into registerTool uses one @ts-expect-error in place of three casts. Re-export LegacyToolCallback alongside ToolCallback.
1 parent d88ef47 commit 3871100

2 files changed

Lines changed: 95 additions & 45 deletions

File tree

src/events.ts

Lines changed: 67 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@ import {
55
Result,
66
type BaseContext,
77
type LegacyContextFields,
8+
type ZodLikeRequestSchema,
89
} from "@modelcontextprotocol/sdk/types.js";
910

1011
type AppContext = BaseContext & LegacyContextFields;
11-
import { ZodLiteral, ZodObject } from "zod/v4";
12-
13-
type MethodSchema = ZodObject<{ method: ZodLiteral<string> }>;
1412

1513
/**
1614
* Per-event state: a singular `on*` handler (replace semantics) plus a
@@ -78,7 +76,7 @@ export abstract class ProtocolWithEvents<
7876
* schema on first use.
7977
*/
8078
protected abstract readonly eventSchemas: {
81-
[K in keyof EventMap]: MethodSchema;
79+
[K in keyof EventMap]: ZodLikeRequestSchema;
8280
};
8381

8482
/**
@@ -202,12 +200,15 @@ export abstract class ProtocolWithEvents<
202200

203201
// ── Handler registration with double-set protection ─────────────────
204202

205-
// The two overrides below are arrow-function class fields rather than
206-
// prototype methods so that Protocol's constructor — which registers its
207-
// own ping/cancelled/progress handlers via `this.setRequestHandler`
208-
// before our fields initialize — hits the base implementation and skips
209-
// tracking. Converting these to proper methods would crash with
210-
// `_registeredMethods` undefined during super().
203+
// These overrides are prototype methods, so Protocol's constructor (which
204+
// registers built-in ping/cancelled/progress handlers via
205+
// `this.setRequestHandler` during super()) dispatches here before our own
206+
// fields have initialized. The `_registeredMethods === undefined` guard
207+
// skips tracking during that window.
208+
//
209+
// The base method has four overloads in v2; we expose only the Zod-schema
210+
// form here since that is all this package uses. Subclasses retain the
211+
// typed (request, ctx) handler signature.
211212

212213
/**
213214
* Registers a request handler. Throws if a handler for the same method
@@ -216,11 +217,24 @@ export abstract class ProtocolWithEvents<
216217
*
217218
* @throws {Error} if a handler for this method is already registered.
218219
*/
219-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
220-
override setRequestHandler = ((...args: any) => {
221-
this._assertMethodNotRegistered(args[0], "setRequestHandler");
222-
super.setRequestHandler(...(args as [MethodSchema, () => Result]));
223-
}) as Protocol<AppContext>["setRequestHandler"];
220+
override setRequestHandler<T extends ZodLikeRequestSchema>(
221+
requestSchema: T,
222+
handler: (
223+
request: ReturnType<T["parse"]>,
224+
ctx: AppContext,
225+
) => Result | Promise<Result>,
226+
): void;
227+
override setRequestHandler(
228+
schema: ZodLikeRequestSchema,
229+
handler: (request: unknown, ctx: AppContext) => Result | Promise<Result>,
230+
): void {
231+
if (this._registeredMethods === undefined) {
232+
super.setRequestHandler(schema, handler);
233+
return;
234+
}
235+
this._assertMethodNotRegistered(schema, "setRequestHandler");
236+
super.setRequestHandler(schema, handler);
237+
}
224238

225239
/**
226240
* Registers a notification handler. Throws if a handler for the same
@@ -229,11 +243,21 @@ export abstract class ProtocolWithEvents<
229243
*
230244
* @throws {Error} if a handler for this method is already registered.
231245
*/
232-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
233-
override setNotificationHandler = ((...args: any) => {
234-
this._assertMethodNotRegistered(args[0], "setNotificationHandler");
235-
super.setNotificationHandler(...(args as [MethodSchema, () => void]));
236-
}) as Protocol<AppContext>["setNotificationHandler"];
246+
override setNotificationHandler<T extends ZodLikeRequestSchema>(
247+
notificationSchema: T,
248+
handler: (notification: ReturnType<T["parse"]>) => void | Promise<void>,
249+
): void;
250+
override setNotificationHandler(
251+
schema: ZodLikeRequestSchema,
252+
handler: (notification: unknown) => void | Promise<void>,
253+
): void {
254+
if (this._registeredMethods === undefined) {
255+
super.setNotificationHandler(schema, handler);
256+
return;
257+
}
258+
this._assertMethodNotRegistered(schema, "setNotificationHandler");
259+
super.setNotificationHandler(schema, handler);
260+
}
237261

238262
/**
239263
* Warn if a request handler `on*` setter is replacing a previously-set
@@ -256,15 +280,30 @@ export abstract class ProtocolWithEvents<
256280
* Replace a request handler, bypassing double-set protection. Used by
257281
* `on*` request-handler setters that need replace semantics.
258282
*/
259-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
260-
protected replaceRequestHandler = ((...args: any) => {
261-
const method = (args[0] as MethodSchema).shape.method.value;
262-
this._registeredMethods.add(method);
263-
super.setRequestHandler(...(args as [MethodSchema, () => Result]));
264-
}) as Protocol<AppContext>["setRequestHandler"];
283+
protected replaceRequestHandler<T extends ZodLikeRequestSchema>(
284+
requestSchema: T,
285+
handler: (
286+
request: ReturnType<T["parse"]>,
287+
ctx: AppContext,
288+
) => Result | Promise<Result>,
289+
): void;
290+
protected replaceRequestHandler(
291+
schema: ZodLikeRequestSchema,
292+
handler: (request: unknown, ctx: AppContext) => Result | Promise<Result>,
293+
): void {
294+
this._registeredMethods.add(this._methodOf(schema));
295+
super.setRequestHandler(schema, handler);
296+
}
265297

266-
private _assertMethodNotRegistered(schema: unknown, via: string): void {
267-
const method = (schema as MethodSchema).shape.method.value;
298+
private _methodOf(arg: ZodLikeRequestSchema | string): string {
299+
return typeof arg === "string" ? arg : arg.shape.method.value;
300+
}
301+
302+
private _assertMethodNotRegistered(
303+
schema: ZodLikeRequestSchema | string,
304+
via: string,
305+
): void {
306+
const method = this._methodOf(schema);
268307
if (this._registeredMethods.has(method)) {
269308
throw new Error(
270309
`Handler for "${method}" already registered (via ${via}). ` +

src/server/index.ts

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import type {
4545
McpServer,
4646
RegisteredTool,
4747
ResourceMetadata,
48-
ToolCallback as SchemaToolCallback,
48+
ToolCallback,
4949
ReadResourceCallback as _ReadResourceCallback,
5050
RegisteredResource,
5151
} from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -55,13 +55,6 @@ import type {
5555
StandardSchemaWithJSON,
5656
} from "@modelcontextprotocol/sdk/server/zod-compat.js";
5757
import type { ZodRawShape } from "@modelcontextprotocol/sdk";
58-
59-
type ToolCallback<Args extends ZodRawShapeCompat | AnySchema | undefined> =
60-
Args extends ZodRawShape
61-
? LegacyToolCallback<Args>
62-
: Args extends StandardSchemaWithJSON
63-
? SchemaToolCallback<Args>
64-
: SchemaToolCallback<undefined>;
6558
import type {
6659
ClientCapabilities,
6760
ReadResourceResult,
@@ -70,7 +63,7 @@ import type {
7063

7164
// Re-exports for convenience
7265
export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE };
73-
export type { ResourceMetadata, ToolCallback };
66+
export type { ResourceMetadata, ToolCallback, LegacyToolCallback };
7467

7568
/**
7669
* Base tool configuration matching the standard MCP server tool options.
@@ -224,8 +217,8 @@ export interface McpUiAppResourceConfig extends ResourceMetadata {
224217
* @see {@link registerAppResource `registerAppResource`} to register the HTML resource referenced by the tool
225218
*/
226219
export function registerAppTool<
227-
OutputArgs extends ZodRawShapeCompat | AnySchema,
228-
InputArgs extends undefined | ZodRawShapeCompat | AnySchema = undefined,
220+
OutputArgs extends StandardSchemaWithJSON,
221+
InputArgs extends StandardSchemaWithJSON | undefined = undefined,
229222
>(
230223
server: Pick<McpServer, "registerTool">,
231224
name: string,
@@ -234,6 +227,25 @@ export function registerAppTool<
234227
outputSchema?: OutputArgs;
235228
},
236229
cb: ToolCallback<InputArgs>,
230+
): RegisteredTool;
231+
/** Raw-shape form: `inputSchema` may be a plain `{ field: z.string() }` record. */
232+
export function registerAppTool<
233+
InputArgs extends ZodRawShape,
234+
OutputArgs extends ZodRawShape | StandardSchemaWithJSON | undefined = undefined,
235+
>(
236+
server: Pick<McpServer, "registerTool">,
237+
name: string,
238+
config: McpUiAppToolConfig & {
239+
inputSchema: InputArgs;
240+
outputSchema?: OutputArgs;
241+
},
242+
cb: LegacyToolCallback<InputArgs>,
243+
): RegisteredTool;
244+
export function registerAppTool(
245+
server: Pick<McpServer, "registerTool">,
246+
name: string,
247+
config: McpUiAppToolConfig,
248+
cb: (...args: never) => ReturnType<ToolCallback<undefined>>,
237249
): RegisteredTool {
238250
// Normalize metadata for backward compatibility:
239251
// - If _meta.ui.resourceUri is set, also set the legacy flat key
@@ -251,12 +263,11 @@ export function registerAppTool<
251263
normalizedMeta = { ...meta, ui: { ...uiMeta, resourceUri: legacyUri } };
252264
}
253265

254-
return (server.registerTool as McpServer["registerTool"])(
255-
name,
256-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
257-
{ ...config, _meta: normalizedMeta } as any,
258-
cb as SchemaToolCallback<undefined>,
259-
);
266+
// The two public overloads above guarantee (config.inputSchema, cb) match one
267+
// of registerTool's overloads. The impl signature loses that pairing, so this
268+
// forward needs a single suppression rather than three casts.
269+
// @ts-expect-error -- forwarding overload-paired args through one impl signature
270+
return server.registerTool(name, { ...config, _meta: normalizedMeta }, cb);
260271
}
261272

262273
export type McpUiReadResourceResult = ReadResourceResult & {

0 commit comments

Comments
 (0)