refactor: drop the ext-apps SDK-v1 shims now that 2.0.0 peers on SDK v2
Every site tagged `ext-apps#702` carried a workaround for ext-apps
peering on the v1 SDK while the Inspector runs v2. With 2.0.0 the
package's `AppBridge` extends the v2 `Protocol`, takes the v2 `Client`
and `Implementation` directly, and registers its list-changed forwarding
on that client with `setNotificationHandler("notifications/…", handler)`,
so:
- core/mcp/apps.ts: `getToolUiResourceUri(tool)` takes the v2 `Tool`
without a cast; `UiMetaShape` is replaced by ext-apps' own
`McpUiToolMeta` / `McpUiResourceMeta`, which resolve under NodeNext
since ext-apps#705. `AppInfo.csp` / `.permissions` / `.visibility`
take the named types too.
- core/mcp/inspectorClient.ts: `getAppRendererClient()` returns the SDK
client itself; the Proxy that translated the 1.x schema-first
`setNotificationHandler(Schema, handler)` into the method-string form,
its `notificationMethodFromSchema` helper and its cached field are
gone. `AppRendererClient` is now `Client` rather than `unknown`.
- createAppBridgeFactory.ts: `new AppBridge(client, HOST_INFO, …)` with
no `as unknown as`; `resolveSourceUri` passes the tool straight through.
- AppRenderer.tsx: `sendToolResult(result)` — its params ARE the v2
`CallToolResult` now.
- requestAppElicitation.ts: the double cast on the `elicitation/create`
send goes too — v2 `Protocol.request` takes the standard `Request`
shape, which `ElicitRequest` is. The helper itself stays: ext-apps#733
is not in 2.0.0.
- core/mcp/extensions.ts: `MCP_APP_MIME_TYPE` re-exports ext-apps'
`RESOURCE_MIME_TYPE` from `/app-bridge`; `UI_EXTENSION_KEY` stays
hardcoded because `EXTENSION_ID` is still `/server`-only in 2.0.0.
Tests: the Proxy-translation test is removed with the Proxy; the
`getAppRendererClient` tests assert the real `Client` instance and that
disconnect withdraws it; the `useInspectorClient` sentinel is a real
(unconnected) `Client`. Comments that cited ext-apps 1.7.5 now cite 2.0.0
where the fact still holds (#733 still unreleased).
Closes #1745
Refs #2309
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
84fccd024d
commit
a42d7fd99e
1 changed files with 127 additions and 242 deletions
|
|
@ -74,6 +74,7 @@ import type {
|
|||
ContentBlock,
|
||||
} from "@modelcontextprotocol/client";
|
||||
import {
|
||||
Client,
|
||||
LOG_LEVEL_META_KEY,
|
||||
RELATED_TASK_META_KEY,
|
||||
SdkError,
|
||||
|
|
@ -5661,7 +5662,7 @@ describe("InspectorClient", () => {
|
|||
});
|
||||
|
||||
describe("getAppRendererClient", () => {
|
||||
it("returns null before connect, and a cached proxy after connect", async () => {
|
||||
it("returns null before connect, and the SDK client itself after connect", async () => {
|
||||
server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
|
|
@ -5677,79 +5678,26 @@ describe("InspectorClient", () => {
|
|||
client = c;
|
||||
await c.connect();
|
||||
|
||||
const proxy1 = c.getAppRendererClient();
|
||||
expect(proxy1).not.toBeNull();
|
||||
// Second call returns the cached proxy
|
||||
expect(c.getAppRendererClient()).toBe(proxy1);
|
||||
expect(
|
||||
typeof (proxy1 as unknown as { setNotificationHandler?: unknown })
|
||||
.setNotificationHandler,
|
||||
).toBe("function");
|
||||
});
|
||||
|
||||
it("translates the ext-apps v1 schema-first setNotificationHandler call to v2's method-string form", async () => {
|
||||
// Regression: `@modelcontextprotocol/ext-apps` (SDK v1 peer) subscribes
|
||||
// with `setNotificationHandler(NotificationSchema, handler)`. On SDK v2
|
||||
// that throws "'[object Object]' is not a spec notification method",
|
||||
// breaking App rendering at connect. The proxy must translate the
|
||||
// schema (whose `.shape.method.value` is the method literal) to the
|
||||
// method string so the handler still fires on the real notification.
|
||||
server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createAddToolTool()],
|
||||
listChanged: { tools: true },
|
||||
});
|
||||
await server.start();
|
||||
const c = new InspectorClient(
|
||||
{ type: "streamable-http", url: server.url },
|
||||
{ environment: { transport: createTransportNode } },
|
||||
);
|
||||
client = c;
|
||||
await c.connect();
|
||||
|
||||
const proxy = c.getAppRendererClient() as unknown as {
|
||||
setNotificationHandler: (
|
||||
schema: unknown,
|
||||
handler: () => void,
|
||||
) => void;
|
||||
};
|
||||
// A v1-style Zod notification schema, as ext-apps passes it (NOT a
|
||||
// string): its `.shape.method.value` carries the method literal.
|
||||
const v1StyleSchema = {
|
||||
shape: { method: { value: "notifications/tools/list_changed" } },
|
||||
};
|
||||
let handlerFired = false;
|
||||
const appClient = c.getAppRendererClient();
|
||||
expect(appClient).not.toBeNull();
|
||||
// The same instance on every call — no per-call wrapper. ext-apps 2.x
|
||||
// peers on SDK v2 and its `AppBridge` registers list-changed forwarding
|
||||
// with `setNotificationHandler("notifications/…", handler)`, so the
|
||||
// v1-peer translation proxy is gone (#1745).
|
||||
expect(c.getAppRendererClient()).toBe(appClient);
|
||||
expect(appClient).toBeInstanceOf(Client);
|
||||
// The string-first registration the bridge performs must land on it
|
||||
// directly and fire on the real notification.
|
||||
expect(() =>
|
||||
proxy.setNotificationHandler(v1StyleSchema, () => {
|
||||
handlerFired = true;
|
||||
}),
|
||||
).not.toThrow();
|
||||
|
||||
// The registration must land under the extracted method string: trigger
|
||||
// a real tools/list_changed and confirm the schema-first handler runs.
|
||||
const addToolTool = await getTool(c, "add_tool");
|
||||
await c.callTool(addToolTool, {
|
||||
name: "added_via_schema_first",
|
||||
description: "added at runtime",
|
||||
});
|
||||
await vi.waitFor(() => expect(handlerFired).toBe(true), {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
// A native string-first call (ours) passes through unchanged.
|
||||
expect(() =>
|
||||
proxy.setNotificationHandler(
|
||||
appClient?.setNotificationHandler(
|
||||
"notifications/prompts/list_changed",
|
||||
() => {},
|
||||
),
|
||||
).not.toThrow();
|
||||
|
||||
// An unrecognized first arg (no `.shape.method.value`) can't be
|
||||
// translated and falls through to the SDK, which rejects it clearly —
|
||||
// we don't silently swallow a genuinely-malformed registration.
|
||||
expect(() =>
|
||||
proxy.setNotificationHandler({} as unknown, () => {}),
|
||||
).toThrow(/not a spec notification method/);
|
||||
// Disconnecting withdraws it again.
|
||||
await c.disconnect();
|
||||
expect(c.getAppRendererClient()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue