ext-apps/src/legacy-handlers.ts
Olivier Chafik 728b0bf38e Keep the 1.x handler registration forms as deprecated overloads
Consumers on 1.x wrote setRequestHandler(Schema, handler) and
setNotificationHandler(Schema, handler) with a whole-message Zod schema.
SDK 2.0 keys handlers by method name, so App and AppBridge now accept
that form as a third, deprecated overload: a small helper detects the
schema, extracts the method literal, forwards to the 2.x
(method, { params }, handler) form, and hands the legacy handler the
reassembled { method, params } message plus a 1.x-shaped extra
(signal, requestId, sessionId, _meta). A one-time console warning
points at the new form. The double-registration guard applies to both
forms.

The AppRequest, AppNotification and AppResult unions that 1.x fed to
the SDK's Protocol generics come back as deprecated type aliases, since
they leaked through the root entry and cost nothing to keep.

Both go away in 3.0.
2026-09-08 14:24:25 +01:00

94 lines
3 KiB
TypeScript

import type { BaseContext, RequestId } from "@modelcontextprotocol/client";
import type { ZodLiteral, ZodObject, ZodType } from "zod/v4";
/**
* A whole-message Zod schema (`method` literal plus `params`), as accepted by
* the 1.x `setRequestHandler(Schema, handler)` forms.
*/
export type LegacyMethodSchema = ZodObject<{
method: ZodLiteral<string>;
params: ZodType;
}>;
/**
* The `extra` argument 1.x request handlers received.
*
* @deprecated Use the 2.x `BaseContext` (`extra.mcpReq.signal`, `extra.mcpReq.id`).
*/
export type LegacyRequestHandlerExtra = {
signal: AbortSignal;
requestId: RequestId;
sessionId?: string;
_meta?: BaseContext["mcpReq"]["_meta"];
sendRequest: BaseContext["mcpReq"]["send"];
sendNotification: BaseContext["mcpReq"]["notify"];
authInfo?: NonNullable<BaseContext["http"]>["authInfo"];
};
/** @deprecated Use `setRequestHandler("method", { params, result }, (params, ctx) => …)`. */
export type LegacyRequestHandler<S extends LegacyMethodSchema> = (
request: S["_output"],
extra: LegacyRequestHandlerExtra,
) => unknown;
/** @deprecated Use `setNotificationHandler("method", { params }, (params) => …)`. */
export type LegacyNotificationHandler<S extends LegacyMethodSchema> = (
notification: S["_output"],
) => void | Promise<void>;
/**
* Arguments for the 2.x three-argument `setRequestHandler` /
* `setNotificationHandler` form.
*/
type ModernArgs = [
method: string,
schemas: { params: ZodType },
handler: (params: unknown, ctx: BaseContext) => unknown,
];
const warned = new Set<string>();
/**
* Translate a 1.x `(Schema, handler)` registration into the 2.x
* `(method, { params }, handler)` form, or return `undefined` when the call
* is already in the 2.x form. The 1.x handler receives the reassembled
* `{ method, params }` message and, for requests, a 1.x-shaped `extra`.
*/
export function toModernArgs(
kind: "request" | "notification",
args: unknown[],
): ModernArgs | undefined {
const [first, handler] = args;
const shape = (first as Partial<LegacyMethodSchema> | undefined)?.shape;
if (typeof shape?.method?.value !== "string") return undefined;
const method = shape.method.value;
const setter =
kind === "request" ? "setRequestHandler" : "setNotificationHandler";
if (!warned.has(setter)) {
warned.add(setter);
console.warn(
`[ext-apps] ${setter}(Schema, handler) is deprecated; use ` +
`${setter}("${method}", { params: Schema.shape.params }, handler).`,
);
}
const call = handler as (...a: unknown[]) => unknown;
return [
method,
{ params: shape.params },
kind === "request"
? (params, ctx) => call({ method, params }, legacyExtra(ctx))
: (params) => call({ method, params }),
];
}
function legacyExtra(ctx: BaseContext): LegacyRequestHandlerExtra {
return {
signal: ctx.mcpReq.signal,
requestId: ctx.mcpReq.id,
sessionId: ctx.sessionId,
_meta: ctx.mcpReq._meta,
sendRequest: ctx.mcpReq.send,
sendNotification: ctx.mcpReq.notify,
authInfo: ctx.http?.authInfo,
};
}