Fix MCP App load regression: translate ext-apps v1 setNotificationHandler API to v2

An MCP App (e.g. NightRider's drum-pad) failed to load with
"'[object Object]' is not a spec notification method; pass schemas as the
second argument to setNotificationHandler()". `@modelcontextprotocol/ext-apps`
still peers on SDK v1 and, during the AppBridge connect handshake, subscribes
to tools/resources/prompts list_changed with the v1 schema-first API
`setNotificationHandler(NotificationSchema, handler)`. SDK v2 requires a method
STRING and throws on a schema object — the migration's compat shim covered the
types but not this runtime API difference.

Fix in the app-renderer client Proxy (`getAppRendererClient`): translate a
schema-first `setNotificationHandler` call to v2's method-string form by reading
the method literal off the schema's `shape.method.value`; native string-first
calls pass through, and an unrecognized first arg falls through to the SDK's
clear error rather than being swallowed. Only `setNotificationHandler` needed
this — AppBridge's `request(req, schema, …)` calls work as-is (the v1 SDK
schemas expose `~standard`, so v2 accepts them). Remove when ext-apps#702 ships
a v2 peer.

Added an integration test covering the schema-first translation end-to-end
(handler fires on a real tools/list_changed), plus the string-passthrough and
unrecognized-schema branches. Coverage ≥90 holds; typecheck + App tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-07-15 12:44:20 -04:00
commit 8a7a38f800

View file

@ -186,6 +186,24 @@ const TOOL_CALL_CANCELLED_REASON = "Tool call cancelled by user";
*/
const DEFAULT_TASK_POLL_INTERVAL_MS = 500;
/**
* Extract the method literal from an MCP notification Zod schema (e.g.
* `ToolListChangedNotificationSchema`), or `undefined` if the shape isn't
* recognized. Used by the App-renderer client proxy to translate the SDK-v1
* schema-first `setNotificationHandler` API — which `@modelcontextprotocol/ext-apps`
* still uses — into SDK v2's method-string form. Reads the `method` literal off
* the notification schema's `shape` (the shape both the v1 SDK and v2 core
* schemas expose).
*/
function notificationMethodFromSchema(schema: unknown): string | undefined {
if (schema !== null && typeof schema === "object") {
const literal = (schema as { shape?: { method?: { value?: unknown } } })
.shape?.method?.value;
if (typeof literal === "string") return literal;
}
return undefined;
}
/**
* The descriptor for a single tools/call, threaded through the retry loop and
* each attempt. Bundled into one object so `callToolWithRetries`/`attemptToolCall`
@ -1441,9 +1459,25 @@ export class InspectorClient extends InspectorClientEventTarget {
get(proxyTarget, prop, receiver) {
const value = Reflect.get(proxyTarget, prop, receiver);
if (prop === "setNotificationHandler" && typeof value === "function") {
return (...args: Parameters<Client["setNotificationHandler"]>) => {
// Add behavior here (e.g. wrap handler, log, filter)
return value.apply(target, args);
return (schemaOrMethod: unknown, ...rest: unknown[]) => {
// `@modelcontextprotocol/ext-apps` still peers on SDK v1 and
// subscribes to list-changed notifications with the v1 schema-first
// API `setNotificationHandler(NotificationSchema, handler)`. SDK v2
// requires a method STRING as the first argument and throws
// "'[object Object]' is not a spec notification method" on a schema —
// which broke App rendering during the initial connect handshake.
// Translate a schema-first call to the method-string form; native
// string-first calls (ours) pass through untouched. Remove when
// ext-apps#702 ships a v2 peer.
const method =
typeof schemaOrMethod === "string"
? schemaOrMethod
: (notificationMethodFromSchema(schemaOrMethod) ??
schemaOrMethod);
return (value as (...a: unknown[]) => unknown).apply(target, [
method,
...rest,
]);
};
}
return value;