refactor: migrate App to SDK v2 client

This commit is contained in:
Andrew Khadder 2026-07-14 15:02:25 -07:00
commit 59e6ffc6d8
No known key found for this signature in database
3 changed files with 435 additions and 172 deletions

View file

@ -7,10 +7,7 @@ import {
ListPromptsResultSchema,
ListResourcesResultSchema,
ListResourceTemplatesResultSchema,
PromptListChangedNotificationSchema,
ReadResourceResultSchema,
ResourceListChangedNotificationSchema,
ToolListChangedNotificationSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod/v4";
@ -2313,9 +2310,12 @@ describe("App <-> AppBridge integration", () => {
it("sendToolListChanged sends notification to app", async () => {
const receivedNotifications: unknown[] = [];
app.setNotificationHandler(ToolListChangedNotificationSchema, (n) => {
receivedNotifications.push(n.params);
});
app.setNotificationHandler(
"notifications/tools/list_changed",
(notification) => {
receivedNotifications.push(notification.params);
},
);
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
@ -2328,9 +2328,12 @@ describe("App <-> AppBridge integration", () => {
it("sendResourceListChanged sends notification to app", async () => {
const receivedNotifications: unknown[] = [];
app.setNotificationHandler(ResourceListChangedNotificationSchema, (n) => {
receivedNotifications.push(n.params);
});
app.setNotificationHandler(
"notifications/resources/list_changed",
(notification) => {
receivedNotifications.push(notification.params);
},
);
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
@ -2343,9 +2346,12 @@ describe("App <-> AppBridge integration", () => {
it("sendPromptListChanged sends notification to app", async () => {
const receivedNotifications: unknown[] = [];
app.setNotificationHandler(PromptListChangedNotificationSchema, (n) => {
receivedNotifications.push(n.params);
});
app.setNotificationHandler(
"notifications/prompts/list_changed",
(notification) => {
receivedNotifications.push(notification.params);
},
);
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
@ -2771,20 +2777,6 @@ describe("isToolVisibilityAppOnly", () => {
);
}).toThrow(/already registered/);
});
it("direct setNotificationHandler throws for event-mapped methods", () => {
const app2 = new App(testAppInfo, {}, { autoResize: false });
app2.addEventListener("toolinput", () => {});
expect(() => {
app2.setNotificationHandler(
// @ts-expect-error — exercising throw path with raw schema
{
shape: { method: { value: "ui/notifications/tool-input" } },
},
() => {},
);
}).toThrow(/already registered/);
});
});
});

300
src/app.test.ts Normal file
View file

@ -0,0 +1,300 @@
import { afterEach, describe, expect, it } from "bun:test";
import {
InMemoryTransport,
Server,
type JSONRPCMessage,
} from "@modelcontextprotocol/server";
import { z } from "zod/v4";
import { App } from "./app";
import {
LATEST_PROTOCOL_VERSION,
McpUiInitializeRequestSchema,
McpUiInitializeResultSchema,
McpUiInitializedNotificationSchema,
} from "./types";
const INNER_MCP_PROTOCOL_VERSION = "2025-11-25";
type ConnectedPair = {
app: App;
server: Server;
sentByApp: JSONRPCMessage[];
appsInitialized: Promise<void>;
};
async function connectPair(
app = new App(
{ name: "view-app", version: "1.0.0" },
{ tools: { listChanged: true } },
{ autoResize: false },
),
): Promise<ConnectedPair> {
const server = new Server(
{ name: "inner-mcp-server", version: "0.0.0" },
{
capabilities: { resources: {}, tools: {} },
supportedProtocolVersions: [INNER_MCP_PROTOCOL_VERSION],
},
);
const [appTransport, serverTransport] = InMemoryTransport.createLinkedPair();
const sentByApp: JSONRPCMessage[] = [];
const send = appTransport.send.bind(appTransport);
appTransport.send = async (message, options) => {
sentByApp.push(message);
await send(message, options);
};
let resolveAppsInitialized!: () => void;
const appsInitialized = new Promise<void>((resolve) => {
resolveAppsInitialized = resolve;
});
server.setRequestHandler(
"ui/initialize",
{
params: McpUiInitializeRequestSchema.shape.params,
result: McpUiInitializeResultSchema,
},
(params) => {
expect(params.protocolVersion).toBe(LATEST_PROTOCOL_VERSION);
return {
protocolVersion: LATEST_PROTOCOL_VERSION,
hostCapabilities: { openLinks: {} },
hostInfo: { name: "apps-host", version: "2.0.0" },
hostContext: { theme: "dark" as const, locale: "en-US" },
};
},
);
server.setNotificationHandler(
"ui/notifications/initialized",
{ params: McpUiInitializedNotificationSchema.shape.params },
() => resolveAppsInitialized(),
);
server.setRequestHandler("tools/call", (request) => ({
content: [{ type: "text", text: `called ${request.params.name}` }],
}));
server.setRequestHandler("resources/read", (request) => ({
contents: [{ uri: request.params.uri, text: "resource body" }],
}));
server.setRequestHandler("resources/list", () => ({
resources: [{ uri: "test://resource", name: "Test resource" }],
}));
server.setRequestHandler("sampling/createMessage", () => ({
model: "test-model",
role: "assistant",
content: { type: "text", text: "sampled" },
}));
await server.connect(serverTransport);
await app.connect(appTransport);
await appsInitialized;
return { app, server, sentByApp, appsInitialized };
}
const connected: Array<Pick<ConnectedPair, "app" | "server">> = [];
afterEach(async () => {
await Promise.all(
connected
.splice(0)
.flatMap(({ app, server }) => [app.close(), server.close()]),
);
});
describe("App base MCP SDK v2 Client migration", () => {
it("completes the legacy MCP handshake before the authoritative Apps handshake", async () => {
const pair = await connectPair();
connected.push(pair);
const lifecycleMethods = pair.sentByApp
.filter(
(message): message is JSONRPCMessage & { method: string } =>
"method" in message,
)
.map((message) => message.method)
.filter((method) =>
[
"initialize",
"notifications/initialized",
"ui/initialize",
"ui/notifications/initialized",
].includes(method),
);
expect(lifecycleMethods).toEqual([
"initialize",
"notifications/initialized",
"ui/initialize",
"ui/notifications/initialized",
]);
expect(pair.app.getNegotiatedProtocolVersion()).toBe(
INNER_MCP_PROTOCOL_VERSION,
);
});
it("uses ui/initialize as the authority for Apps host state", async () => {
const pair = await connectPair();
connected.push(pair);
expect(pair.app.getServerVersion()).toEqual({
name: "inner-mcp-server",
version: "0.0.0",
});
expect(pair.app.getHostVersion()).toEqual({
name: "apps-host",
version: "2.0.0",
});
expect(pair.app.getHostCapabilities()).toEqual({ openLinks: {} });
expect(pair.app.getHostContext()).toEqual({
theme: "dark",
locale: "en-US",
});
});
it("preserves registered App tools with the base MCP SDK v2 Client handler context", async () => {
const pair = await connectPair();
connected.push(pair);
let receivedSignal: AbortSignal | undefined;
pair.app.registerTool(
"greet",
{ description: "Greets a person" },
async (extra) => {
receivedSignal = extra.mcpReq.signal;
return { content: [{ type: "text", text: "hello" }] };
},
);
const listed = await pair.server.request({
method: "tools/list",
params: {},
});
const result = await pair.server.request({
method: "tools/call",
params: { name: "greet", arguments: {} },
});
expect(listed.tools.map((tool) => tool.name)).toEqual(["greet"]);
expect(result.content).toEqual([{ type: "text", text: "hello" }]);
expect(receivedSignal).toBeInstanceOf(AbortSignal);
});
it("uses base MCP SDK v2 method-keyed sends for standard MCP requests", async () => {
const pair = await connectPair();
connected.push(pair);
const toolResult = await pair.app.callServerTool({
name: "refresh",
arguments: {},
});
const resource = await pair.app.readServerResource({
uri: "test://resource",
});
const resources = await pair.app.listServerResources();
const sample = await pair.app.createSamplingMessage({
messages: [{ role: "user", content: { type: "text", text: "hello" } }],
maxTokens: 20,
});
expect(toolResult.content).toEqual([
{ type: "text", text: "called refresh" },
]);
expect(resource.contents[0]).toEqual({
uri: "test://resource",
text: "resource body",
});
expect(resources.resources.map(({ uri }) => uri)).toEqual([
"test://resource",
]);
expect(sample.content).toEqual({ type: "text", text: "sampled" });
});
it("preserves reconnect behavior after close", async () => {
const first = await connectPair();
await first.app.close();
await first.server.close();
const second = await connectPair(first.app);
connected.push(second);
expect(second.app.getNegotiatedProtocolVersion()).toBe(
INNER_MCP_PROTOCOL_VERSION,
);
expect(second.app.getHostVersion()).toEqual({
name: "apps-host",
version: "2.0.0",
});
});
it("preserves auto-resize setup after Apps initialization", async () => {
const app = new App(
{ name: "view-app", version: "1.0.0" },
{},
{ autoResize: true },
);
let setupCalls = 0;
app.setupSizeChangedNotifications = () => {
setupCalls += 1;
return () => {};
};
const pair = await connectPair(app);
connected.push(pair);
expect(setupCalls).toBe(1);
});
it("preserves strict late-handler timing errors", async () => {
const app = new App(
{ name: "view-app", version: "1.0.0" },
{},
{ autoResize: false, strict: true },
);
const pair = await connectPair(app);
connected.push(pair);
expect(() => {
app.ontoolinput = () => {};
}).toThrow(/handler registered after connect/);
});
it("uses base MCP SDK replacement semantics for request handlers", async () => {
const pair = await connectPair();
connected.push(pair);
let handledBy = 0;
pair.app.setRequestHandler("ping", () => {
handledBy = 1;
return {};
});
pair.app.setRequestHandler("ping", () => {
handledBy = 2;
return {};
});
await pair.server.request({ method: "ping", params: {} });
expect(handledBy).toBe(2);
});
it("uses base MCP SDK replacement semantics for non-event notifications", async () => {
const pair = await connectPair();
connected.push(pair);
const calls: number[] = [];
const params = z.object({});
pair.app.setNotificationHandler("test/notification", { params }, () => {
calls.push(1);
});
pair.app.setNotificationHandler("test/notification", { params }, () => {
calls.push(2);
});
await pair.server.notification({
method: "test/notification",
params: {},
});
expect(calls).toEqual([2]);
});
});

View file

@ -1,38 +1,27 @@
import {
Client,
type ClientContext,
type ClientOptions,
type RequestOptions,
mergeCapabilities,
Protocol,
ProtocolOptions,
} from "@modelcontextprotocol/sdk/shared/protocol.js";
import {
CallToolRequest,
CallToolRequestSchema,
CallToolResult,
CallToolResultSchema,
CreateMessageRequest,
CreateMessageResult,
CreateMessageResultSchema,
CreateMessageResultWithTools,
CreateMessageResultWithToolsSchema,
EmptyResultSchema,
Implementation,
ListResourcesRequest,
ListResourcesResult,
ListResourcesResultSchema,
ListToolsRequest,
ListToolsRequestSchema,
ListToolsResult,
LoggingMessageNotification,
PingRequestSchema,
ReadResourceRequest,
ReadResourceResult,
ReadResourceResultSchema,
Tool,
ToolAnnotations,
ToolListChangedNotification,
} from "@modelcontextprotocol/sdk/types.js";
import { AppNotification, AppRequest, AppResult } from "./types";
type CallToolRequest,
type CallToolResult,
type CreateMessageRequest,
type CreateMessageResult,
type CreateMessageResultWithTools,
type Implementation,
type ListResourcesRequest,
type ListResourcesResult,
type ListToolsRequest,
type ListToolsResult,
type LoggingMessageNotification,
type ReadResourceRequest,
type ReadResourceResult,
type Tool,
type ToolAnnotations,
type ToolListChangedNotification,
type Transport,
} from "@modelcontextprotocol/client";
import { EmptyResultSchema } from "@modelcontextprotocol/core";
import { EventDispatcher } from "./events";
export { EventDispatcher } from "./events";
import { PostMessageTransport } from "./message-transport";
@ -56,6 +45,7 @@ import {
McpUiResourceTeardownRequest,
McpUiResourceTeardownRequestSchema,
McpUiResourceTeardownResult,
McpUiResourceTeardownResultSchema,
McpUiRequestTeardownNotification,
McpUiSizeChangedNotification,
McpUiToolCancelledNotification,
@ -69,15 +59,36 @@ import {
McpUiRequestDisplayModeRequest,
McpUiRequestDisplayModeResultSchema,
} from "./types";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import {
StandardSchemaV1,
standardSchemaToJsonSchema,
validateStandardSchema,
} from "./standard-schema";
import { z, type ZodLiteral, type ZodObject } from "zod/v4";
import { z, type ZodLiteral, type ZodObject, type ZodType } from "zod/v4";
type MethodSchema = ZodObject<{ method: ZodLiteral<string> }>;
type MethodSchema = ZodObject<{
method: ZodLiteral<string>;
params: ZodType;
}>;
const INNER_MCP_PROTOCOL_VERSION = "2025-11-25";
function mergeAppCapabilities(
base: McpUiAppCapabilities,
additional: McpUiAppCapabilities,
): McpUiAppCapabilities {
const result = { ...base, ...additional };
if (additional.experimental !== undefined) {
result.experimental = {
...base.experimental,
...additional.experimental,
};
}
if (additional.tools !== undefined) {
result.tools = { ...base.tools, ...additional.tools };
}
return result;
}
export type {
StandardSchemaV1,
@ -167,7 +178,10 @@ export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
*
* @see `ProtocolOptions` from @modelcontextprotocol/sdk for inherited options
*/
export type AppOptions = ProtocolOptions & {
export type AppOptions = Omit<
ClientOptions,
"capabilities" | "supportedProtocolVersions" | "versionNegotiation"
> & {
/**
* Automatically report size changes to the host using `ResizeObserver`.
*
@ -207,9 +221,7 @@ export type AppOptions = ProtocolOptions & {
allowUnsafeEval?: boolean;
};
type RequestHandlerExtra = Parameters<
Parameters<App["setRequestHandler"]>[1]
>[1];
type RequestHandlerExtra = ClientContext;
/**
* Result of an app-registered tool callback. When `Out` is provided,
@ -343,38 +355,15 @@ export type AppEventMap = {
* await app.connect();
* ```
*/
export class App extends Protocol<AppRequest, AppNotification, AppResult> {
export class App extends Client {
private _hostCapabilities?: McpUiHostCapabilities;
private _hostInfo?: Implementation;
private _hostContext?: McpUiHostContext;
private _registeredTools: { [name: string]: RegisteredAppTool } = {};
private _initializedSent = false;
private readonly _registeredMethods = new Set<string>();
private readonly _registeredEvents = new Set<keyof AppEventMap>();
private readonly _events = new EventDispatcher<AppEventMap>();
// ProtocolWithEvents previously owned these registration guards. Keep them
// on App to preserve the existing double-registration behavior while event
// fan-out moves to protocol-independent composition. Arrow-function fields
// ensure Protocol's constructor uses its own methods before the guards init.
override setRequestHandler: Protocol<
AppRequest,
AppNotification,
AppResult
>["setRequestHandler"] = (schema, handler) => {
this._assertMethodNotRegistered(schema, "setRequestHandler");
super.setRequestHandler(schema, handler);
};
override setNotificationHandler: Protocol<
AppRequest,
AppNotification,
AppResult
>["setNotificationHandler"] = (schema, handler) => {
this._assertMethodNotRegistered(schema, "setNotificationHandler");
super.setNotificationHandler(schema, handler);
};
/**
* Warn if a host-bound method is called before {@link connect `connect`} has
* completed the `ui/initialize` → `ui/notifications/initialized` handshake.
@ -479,13 +468,17 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
if (this._registeredEvents.has(event)) return;
const schema = this.eventSchemas[event];
if (!schema) throw new Error(`Unknown event: ${String(event)}`);
this.setNotificationHandler(schema, (notification) => {
const params = (notification as { params: AppEventMap[K] }).params;
if (event === "hostcontextchanged") {
this._hostContext = { ...this._hostContext, ...params };
}
this._events.dispatch(event, params);
});
this.setNotificationHandler(
schema.shape.method.value,
{ params: schema.shape.params },
(params) => {
const eventParams = params as AppEventMap[K];
if (event === "hostcontextchanged") {
this._hostContext = { ...this._hostContext, ...eventParams };
}
this._events.dispatch(event, eventParams);
},
);
this._registeredEvents.add(event);
}
@ -508,33 +501,11 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
}
}
protected replaceRequestHandler: Protocol<
AppRequest,
AppNotification,
AppResult
>["setRequestHandler"] = (schema, handler) => {
const method = (schema as MethodSchema).shape.method.value;
this._registeredMethods.add(method);
super.setRequestHandler(schema, handler);
};
private _assertMethodNotRegistered(schema: unknown, via: string): void {
const method = (schema as MethodSchema).shape.method.value;
if (this._registeredMethods.has(method)) {
throw new Error(
`Handler for "${method}" already registered (via ${via}). ` +
`Use addEventListener() to attach multiple listeners, ` +
`or the on* setter for replace semantics.`,
);
}
this._registeredMethods.add(method);
}
/**
* Create a new MCP App instance.
*
* @param _appInfo - App identification (name and version)
* @param _capabilities - Features and capabilities this app provides
* @param _appCapabilities - Features and capabilities this app provides
* @param options - Configuration options including `autoResize` behavior
*
* @example
@ -548,16 +519,29 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
*/
constructor(
private _appInfo: Implementation,
private _capabilities: McpUiAppCapabilities = {},
private _appCapabilities: McpUiAppCapabilities = {},
private options: AppOptions = { autoResize: true },
) {
super(options);
// TEMPORARY INNER-CHANNEL NEGOTIATION:
// App and AppBridge use the public v2 Client/Server roles, so the iframe
// channel first completes the SDK's standard legacy MCP initialization.
// ui/initialize remains authoritative for Apps capabilities, identity, and
// host context, and ui/notifications/initialized remains the View-ready gate.
// Keep the inner MCP era pinned to 2025-11-25. Revisit this sequence only
// after the MCP Apps negotiation specification is resolved:
// https://github.com/modelcontextprotocol/ext-apps/issues/708
super(_appInfo, {
...options,
capabilities: {},
versionNegotiation: { mode: "legacy" },
supportedProtocolVersions: [INNER_MCP_PROTOCOL_VERSION],
});
if (!options.allowUnsafeEval) {
z.config({ jitless: true });
}
this.setRequestHandler(PingRequestSchema, (request) => {
this.setRequestHandler("ping", (request) => {
console.log("Received ping:", request.params);
return {};
});
@ -567,13 +551,16 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
this.setEventHandler("hostcontextchanged", undefined);
}
private registerCapabilities(capabilities: McpUiAppCapabilities): void {
private registerAppCapabilities(capabilities: McpUiAppCapabilities): void {
if (this.transport) {
throw new Error(
"Cannot register capabilities after transport is established",
);
}
this._capabilities = mergeCapabilities(this._capabilities, capabilities);
this._appCapabilities = mergeAppCapabilities(
this._appCapabilities,
capabilities,
);
}
registerTool<
@ -596,7 +583,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
}
const app = this;
const notify = () => {
if (app._initializedSent && app._capabilities.tools?.listChanged) {
if (app._initializedSent && app._appCapabilities.tools?.listChanged) {
void app.sendToolListChanged();
}
};
@ -668,8 +655,8 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
// to declare { tools: {} } in the constructor just to use registerTool.
// Only do this pre-connect; post-connect the capability was already
// advertised (or wasn't) and can't change.
if (!this._capabilities.tools && !this.transport) {
this.registerCapabilities({ tools: { listChanged: true } });
if (!this._appCapabilities.tools && !this.transport) {
this.registerAppCapabilities({ tools: { listChanged: true } });
}
this.ensureToolHandlersInitialized();
@ -1081,11 +1068,15 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
) {
this.warnIfRequestHandlerReplaced("onteardown", this._onteardown, callback);
this._onteardown = callback;
this.replaceRequestHandler(
McpUiResourceTeardownRequestSchema,
(request, extra) => {
this.setRequestHandler(
"ui/resource-teardown",
{
params: McpUiResourceTeardownRequestSchema.shape.params,
result: McpUiResourceTeardownResultSchema,
},
(params, extra) => {
if (!this._onteardown) throw new Error("No onteardown handler set");
return this._onteardown(request.params, extra);
return this._onteardown(params, extra);
},
);
}
@ -1135,7 +1126,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
) {
this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback);
this._oncalltool = callback;
this.replaceRequestHandler(CallToolRequestSchema, (request, extra) => {
this.setRequestHandler("tools/call", (request, extra) => {
if (!this._oncalltool) throw new Error("No oncalltool handler set");
return this._oncalltool(request.params, extra);
});
@ -1206,7 +1197,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
callback,
);
this._onlisttools = callback;
this.replaceRequestHandler(ListToolsRequestSchema, (request, extra) => {
this.setRequestHandler("tools/list", (request, extra) => {
if (!this._onlisttools) throw new Error("No onlisttools handler set");
return this._onlisttools(request.params, extra);
});
@ -1216,7 +1207,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* Verify that the host supports the capability required for the given request method.
* @internal
*/
assertCapabilityForMethod(method: AppRequest["method"]): void {
protected override assertCapabilityForMethod(method: string): void {
switch (method) {
case "sampling/createMessage":
if (!this._hostCapabilities?.sampling) {
@ -1232,11 +1223,11 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* Verify that the app declared the capability required for the given request method.
* @internal
*/
assertRequestHandlerCapability(method: AppRequest["method"]): void {
protected override assertRequestHandlerCapability(method: string): void {
switch (method) {
case "tools/call":
case "tools/list":
if (!this._capabilities.tools) {
if (!this._appCapabilities.tools) {
throw new Error(
`Client does not support tool capability (required for ${method})`,
);
@ -1254,26 +1245,10 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* Verify that the app supports the capability required for the given notification method.
* @internal
*/
assertNotificationCapability(_method: AppNotification["method"]): void {
protected override assertNotificationCapability(_method: string): void {
// TODO
}
/**
* Verify that task creation is supported for the given request method.
* @internal
*/
protected assertTaskCapability(_method: string): void {
throw new Error("Tasks are not supported in MCP Apps");
}
/**
* Verify that task handler is supported for the given method.
* @internal
*/
protected assertTaskHandlerCapability(_method: string): void {
throw new Error("Task handlers are not supported in MCP Apps");
}
/**
* Call a tool on the originating MCP server (proxied through the host).
*
@ -1322,7 +1297,6 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
}
return await this.request(
{ method: "tools/call", params },
CallToolResultSchema,
{
// Hosts may interpose long-running or user-interactive steps before the
// tool result arrives. Opting in here lets a host heartbeat keep the
@ -1379,11 +1353,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
options?: RequestOptions,
): Promise<ReadResourceResult> {
this._assertInitialized("readServerResource");
return await this.request(
{ method: "resources/read", params },
ReadResourceResultSchema,
options,
);
return await this.request({ method: "resources/read", params }, options);
}
/**
@ -1427,11 +1397,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
options?: RequestOptions,
): Promise<ListResourcesResult> {
this._assertInitialized("listServerResources");
return await this.request(
{ method: "resources/list", params },
ListResourcesResultSchema,
options,
);
return await this.request({ method: "resources/list", params }, options);
}
/**
@ -1508,12 +1474,8 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
options?: RequestOptions,
): Promise<CreateMessageResult | CreateMessageResultWithTools> {
this._assertInitialized("createSamplingMessage");
const resultSchema = params.tools
? CreateMessageResultWithToolsSchema
: CreateMessageResultSchema;
return await this.request(
{ method: "sampling/createMessage", params },
resultSchema,
options,
);
}
@ -2030,14 +1992,23 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
);
}
this._initializedSent = false;
await super.connect(transport);
// TEMPORARY INNER-CHANNEL NEGOTIATION:
// App and AppBridge use the public v2 Client/Server roles, so the iframe
// channel first completes the SDK's standard legacy MCP initialization.
// ui/initialize remains authoritative for Apps capabilities, identity, and
// host context, and ui/notifications/initialized remains the View-ready gate.
// Keep the inner MCP era pinned to 2025-11-25. Revisit this sequence only
// after the MCP Apps negotiation specification is resolved:
// https://github.com/modelcontextprotocol/ext-apps/issues/708
try {
await super.connect(transport);
const result = await this.request(
<McpUiInitializeRequest>{
method: "ui/initialize",
params: {
appCapabilities: this._capabilities,
appCapabilities: this._appCapabilities,
appInfo: this._appInfo,
protocolVersion: LATEST_PROTOCOL_VERSION,
},