Skip to content

Commit 47f4cdf

Browse files
fix(app-bridge): v1 parity — setHostContext diffing, hostContext accumulate, onclose shim, onupdatemodelcontext return type, getToolUiResourceUri error message
1 parent 570781e commit 47f4cdf

2 files changed

Lines changed: 22 additions & 7 deletions

File tree

src/app-bridge.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ const BRIDGE_EVENT_NOTIFICATION_SCHEMAS: Record<
159159
*/
160160
export function isToolVisibilityModelOnly(tool: {
161161
_meta?: Record<string, unknown>;
162+
[key: string]: unknown;
162163
}): boolean {
163164
const v = (tool._meta?.ui as McpUiToolMeta | undefined)?.visibility;
164165
return Array.isArray(v) && v.length > 0 && !v.includes("app");
@@ -169,6 +170,7 @@ export function isToolVisibilityModelOnly(tool: {
169170
*/
170171
export function isToolVisibilityAppOnly(tool: {
171172
_meta?: Record<string, unknown>;
173+
[key: string]: unknown;
172174
}): boolean {
173175
const v = (tool._meta?.ui as McpUiToolMeta | undefined)?.visibility;
174176
return Array.isArray(v) && v.length > 0 && !v.includes("model");
@@ -219,6 +221,8 @@ export class AppBridge extends EventDispatcher<AppBridgeEventMap> {
219221

220222
/** Optional error handler. Mirrors the v1 `Protocol.onerror` slot. */
221223
onerror?: (error: Error) => void;
224+
/** Called when the underlying transport closes. Mirrors v1 Protocol.onclose. */
225+
onclose?: () => void;
222226

223227
constructor(
224228
private _client: Client | null,
@@ -239,6 +243,7 @@ export class AppBridge extends EventDispatcher<AppBridgeEventMap> {
239243
},
240244
});
241245
this.server.onerror = (err) => this.onerror?.(err);
246+
this.server.onclose = () => this.onclose?.();
242247
this.ui = this.server.extension(MCP_APPS_EXTENSION_ID, _capabilities, {
243248
peerSchema: McpUiAppCapabilitiesSchema,
244249
});
@@ -447,7 +452,7 @@ export class AppBridge extends EventDispatcher<AppBridgeEventMap> {
447452
private _onupdatemodelcontext?: (
448453
params: McpUiUpdateModelContextRequest["params"],
449454
extra: RequestHandlerExtra,
450-
) => Promise<void> | void;
455+
) => Promise<void | object> | void | object;
451456
get onupdatemodelcontext() { return this._onupdatemodelcontext; }
452457
set onupdatemodelcontext(cb) {
453458
this.warnIfRequestHandlerReplaced(
@@ -552,8 +557,13 @@ export class AppBridge extends EventDispatcher<AppBridgeEventMap> {
552557
* Call this when theme, locale, displayMode, etc. change.
553558
*/
554559
setHostContext(context: Partial<McpUiHostContext>) {
560+
const changed: Partial<McpUiHostContext> = {};
561+
for (const [k, v] of Object.entries(context) as [keyof McpUiHostContext, unknown][]) {
562+
if (this._hostContext[k] !== v) (changed as Record<string, unknown>)[k] = v;
563+
}
555564
this._hostContext = { ...this._hostContext, ...context };
556-
return this.sendHostContextChanged(context);
565+
if (Object.keys(changed).length === 0) return Promise.resolve();
566+
return this.sendHostContextChanged(changed);
557567
}
558568

559569
/** Low-level: send a host-context-changed notification with the given diff. */

src/app.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,13 @@ export function getToolUiResourceUri(tool: {
100100
const meta = tool._meta;
101101
if (!meta) return undefined;
102102
const ui = meta.ui as { resourceUri?: unknown } | undefined;
103-
const candidate = ui && "resourceUri" in ui ? ui.resourceUri : meta[RESOURCE_URI_META_KEY];
104-
if (candidate === undefined) return undefined;
103+
const hasNested = ui != null && "resourceUri" in ui;
104+
const candidate = hasNested ? ui.resourceUri : meta[RESOURCE_URI_META_KEY];
105+
if (candidate === undefined && !hasNested) return undefined;
105106
if (typeof candidate !== "string" || !candidate.startsWith("ui://")) {
106107
throw new Error(
107-
"Tool _meta.ui.resourceUri must be a string starting with ui://, got: " + JSON.stringify(candidate),
108+
'Invalid UI resource URI (must be a string starting with "ui://"): ' +
109+
JSON.stringify(candidate),
108110
);
109111
}
110112
return candidate;
@@ -186,6 +188,8 @@ export class App extends EventDispatcher<AppEventMap> {
186188
* error. Mirrors the v1 `Protocol.onerror` slot.
187189
*/
188190
onerror?: (error: Error) => void;
191+
/** Called when the underlying transport closes. Mirrors v1 Protocol.onclose. */
192+
onclose?: () => void;
189193

190194
constructor(
191195
private _appInfo: Implementation,
@@ -198,6 +202,7 @@ export class App extends EventDispatcher<AppEventMap> {
198202
capabilities: { roots: undefined },
199203
});
200204
this.client.onerror = (err) => this.onerror?.(err);
205+
this.client.onclose = () => this.onclose?.();
201206
this.ui = this.client.extension(MCP_APPS_EXTENSION_ID, _capabilities, {
202207
peerSchema: McpUiHostCapabilitiesSchema,
203208
});
@@ -247,8 +252,8 @@ export class App extends EventDispatcher<AppEventMap> {
247252
event: K,
248253
params: AppEventMap[K],
249254
): void {
250-
if (event === "hostcontextchanged" && this._hostContext !== undefined) {
251-
this._hostContext = { ...this._hostContext, ...(params as McpUiHostContext) };
255+
if (event === "hostcontextchanged") {
256+
this._hostContext = { ...(this._hostContext ?? {}), ...(params as McpUiHostContext) };
252257
}
253258
}
254259

0 commit comments

Comments
 (0)