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>
158 lines
7 KiB
TypeScript
158 lines
7 KiB
TypeScript
import type { ClientCapabilities } from "@modelcontextprotocol/client";
|
|
import { RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/app-bridge";
|
|
import { TASKS_EXTENSION_KEY } from "./modernTaskSchemas.js";
|
|
|
|
/**
|
|
* Extension identifier for SEP-2350 enterprise-managed authorization. Advertised
|
|
* when the connection routes through the enterprise IdP (EMA), which is a
|
|
* property of the auth mode rather than a free debugging toggle — so it is a
|
|
* conditional built-in in {@link buildClientExtensions}, not a registry entry.
|
|
*/
|
|
export const EMA_EXTENSION_KEY =
|
|
"io.modelcontextprotocol/enterprise-managed-authorization";
|
|
|
|
/**
|
|
* Extension identifier for the MCP Apps UI extension (SEP-ext-apps). Mirrors
|
|
* `EXTENSION_ID` from `@modelcontextprotocol/ext-apps`. Hardcoded rather than
|
|
* imported: as of ext-apps 2.0.0 that constant is still exported only from the
|
|
* package's `/server` subpath, which would pull server-only code (and the
|
|
* optional `@modelcontextprotocol/server` peer) into the browser bundle. The
|
|
* node integration test `extensions-mimetype.test.ts` pins the two together.
|
|
* The Inspector always renders MCP Apps, so this is advertised by default
|
|
* (#1740).
|
|
*/
|
|
export const UI_EXTENSION_KEY = "io.modelcontextprotocol/ui";
|
|
|
|
/**
|
|
* The MCP Apps UI resource MIME type the Inspector renders. A server checks for
|
|
* it in the client's advertised `io.modelcontextprotocol/ui` `mimeTypes` to
|
|
* decide whether to serve an App. Re-exported from ext-apps' `/app-bridge`
|
|
* subpath — the one the Inspector already imports everywhere — rather than
|
|
* restated: the extensionless re-export that once kept this a hardcoded copy
|
|
* was fixed upstream (ext-apps#705) and shipped in 2.0.0 (#1745).
|
|
*/
|
|
export const MCP_APP_MIME_TYPE = RESOURCE_MIME_TYPE;
|
|
|
|
/**
|
|
* The value the client stamps for each advertised extension. The wire shape is
|
|
* `{ [extensionId]: object }` (per `ClientCapabilities.extensions`); an empty
|
|
* object is the standard "declared, no sub-options" advertisement.
|
|
*/
|
|
export type ExtensionAdvertisement = NonNullable<
|
|
ClientCapabilities["extensions"]
|
|
>[string];
|
|
|
|
/**
|
|
* A single Inspector-advertisable extension. The registry of these is the shared
|
|
* source of truth for both the capability builder here and the Server Settings
|
|
* toggle UI (#1739), so the two never drift on which extensions exist or what
|
|
* they are called.
|
|
*/
|
|
export interface AdvertisableExtension {
|
|
/** Extension identifier stamped into `capabilities.extensions`. */
|
|
key: string;
|
|
/** Human-readable label for the Server Settings toggle. */
|
|
label: string;
|
|
/**
|
|
* Whether the Inspector advertises this extension when the user has expressed
|
|
* no explicit preference (the toggle's default position).
|
|
*/
|
|
defaultAdvertised: boolean;
|
|
/**
|
|
* The object value stamped into `capabilities.extensions[key]` when
|
|
* advertised. Defaults to `{}` (declared, no sub-options); an extension that
|
|
* carries settings — e.g. the UI extension's `mimeTypes` — sets its own shape.
|
|
*/
|
|
advertisement?: ExtensionAdvertisement;
|
|
}
|
|
|
|
/**
|
|
* Catalog of extensions the Inspector can advertise and the user can toggle.
|
|
* EMA is deliberately absent — it is driven by the auth mode (see
|
|
* {@link EMA_EXTENSION_KEY}), not a standalone toggle. The `io.modelcontextprotocol/ui`
|
|
* Apps extension is added here in Phase 3 (#1740).
|
|
*/
|
|
export const ADVERTISABLE_EXTENSIONS: readonly AdvertisableExtension[] = [
|
|
{
|
|
key: TASKS_EXTENSION_KEY,
|
|
label: "Tasks (io.modelcontextprotocol/tasks)",
|
|
// The modern Tasks extension (SEP-2663). Advertised by default so the SDK
|
|
// stamps it into every modern request envelope — the per-request
|
|
// declaration a server requires before it may return a `CreateTaskResult`
|
|
// (server-directed task creation). Harmless on legacy (extensions ignored).
|
|
defaultAdvertised: true,
|
|
},
|
|
{
|
|
key: UI_EXTENSION_KEY,
|
|
label: "MCP Apps UI (io.modelcontextprotocol/ui)",
|
|
// The MCP Apps UI extension. The Inspector always renders App tools, so it
|
|
// advertises this by default with the App resource MIME type it supports —
|
|
// a conforming server checks the `mimeTypes` before serving a UI resource.
|
|
defaultAdvertised: true,
|
|
advertisement: { mimeTypes: [MCP_APP_MIME_TYPE] },
|
|
},
|
|
];
|
|
|
|
export interface BuildClientExtensionsInput {
|
|
/** True when the connection routes through the enterprise IdP (EMA). */
|
|
enterpriseManaged: boolean;
|
|
/**
|
|
* Per-extension advertise overrides keyed by extension id, from
|
|
* {@link InspectorClientOptions.advertisedExtensions}. A key present here wins
|
|
* over the registry's `defaultAdvertised`; an absent key falls back to it.
|
|
*/
|
|
advertised?: Record<string, boolean>;
|
|
/**
|
|
* True when this client can render an MCP App and resolve an
|
|
* `elicitation/create` request through its bridge (#1854). Adds the nested
|
|
* `elicitation` setting to the UI extension's advertisement, which is half of
|
|
* the negotiation a server checks before attaching an App to an elicitation.
|
|
*
|
|
* Deliberately an input rather than a registry default: the shared
|
|
* `InspectorClient` knowing the MCP Apps MIME type says nothing about whether
|
|
* the *client* has a sandbox renderer, so CLI and TUI must never advertise it.
|
|
* Ignored when the UI extension itself is not advertised — a nested setting on
|
|
* an extension we did not declare would be meaningless.
|
|
*/
|
|
appElicitation?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Assemble the `capabilities.extensions` map advertised at construction — the
|
|
* single source of truth that replaces the previously ad-hoc, per-extension
|
|
* spreads. Registry entries resolve to advertised/not via the user override with
|
|
* a registry-default fallback; EMA is layered on top as an auth-mode-driven
|
|
* built-in.
|
|
*
|
|
* With the Tasks entry defaulting to advertised, the map is non-empty for a
|
|
* default config, so `capabilities.extensions` is always attached.
|
|
*/
|
|
export function buildClientExtensions(
|
|
input: BuildClientExtensionsInput,
|
|
): Record<string, ExtensionAdvertisement> {
|
|
const map: Record<string, ExtensionAdvertisement> = {};
|
|
for (const ext of ADVERTISABLE_EXTENSIONS) {
|
|
const advertised = input.advertised?.[ext.key] ?? ext.defaultAdvertised;
|
|
if (advertised) {
|
|
// Clone the registry advertisement so the returned map never aliases the
|
|
// shared `ADVERTISABLE_EXTENSIONS` entry — a later in-place mutation of a
|
|
// stamped value (e.g. `extensions[ui].mimeTypes`) can't corrupt the
|
|
// registry for subsequent connections. `{}` entries are fresh literals.
|
|
map[ext.key] = ext.advertisement
|
|
? structuredClone(ext.advertisement)
|
|
: {};
|
|
}
|
|
}
|
|
// Nested app-rendered-elicitation opt-in (#1854), layered onto the UI
|
|
// extension's own advertisement rather than added as a second extension.
|
|
// Guarded on the UI entry actually being present so turning the Apps
|
|
// extension off in Server Settings also turns this off.
|
|
const uiAdvertisement = map[UI_EXTENSION_KEY];
|
|
if (input.appElicitation && uiAdvertisement) {
|
|
map[UI_EXTENSION_KEY] = { ...uiAdvertisement, elicitation: {} };
|
|
}
|
|
if (input.enterpriseManaged) {
|
|
map[EMA_EXTENSION_KEY] = {};
|
|
}
|
|
return map;
|
|
}
|