From 3df49c38dc7483074653ed5ceb0805b9f46459f6 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 25 Nov 2025 02:39:33 +0000 Subject: [PATCH] fix iframe resizing (#19) * reshuffle build / start scripts * fix resize logic * prettier * extract RESOURCE_URI_META_KEY constant * Update message-transport.ts * Update package.json --- build.esbuild.ts | 30 --------------------- examples/simple-host/package.json | 18 +++++-------- examples/simple-server/package.json | 4 +-- examples/simple-server/server.ts | 7 ++--- examples/simple-server/src/ui-raw.ts | 18 ++++--------- examples/simple-server/src/ui-vanilla.ts | 7 ----- package.json | 9 ++++--- specification/draft/apps.mdx | 2 +- src/app-bridge.ts | 7 ----- src/app.ts | 34 +++++++++--------------- src/message-transport.ts | 2 -- 11 files changed, 38 insertions(+), 100 deletions(-) delete mode 100644 build.esbuild.ts diff --git a/build.esbuild.ts b/build.esbuild.ts deleted file mode 100644 index 559c7fc..0000000 --- a/build.esbuild.ts +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env tsx -import { execSync } from "child_process"; -import * as esbuild from "esbuild"; - -// Run TypeScript compiler for type declarations -execSync("tsc", { stdio: "inherit" }); - -const isDevelopment = process.env.NODE_ENV === "development"; - -// Build all JavaScript/TypeScript files -async function buildJs( - entrypoint: string, - opts: { outdir?: string; target?: "browser" | "node" } = {}, -) { - return esbuild.build({ - entryPoints: [entrypoint], - outdir: opts.outdir ?? "dist", - platform: opts.target === "node" ? "node" : "browser", - format: "esm", - bundle: true, - minify: !isDevelopment, - sourcemap: isDevelopment ? "inline" : false, - }); -} - -await Promise.all([ - buildJs("src/app.ts", { outdir: "dist/src" }), - buildJs("src/app-bridge.ts", { outdir: "dist/src" }), - buildJs("src/react/index.tsx", { outdir: "dist/src/react" }), -]); diff --git a/examples/simple-host/package.json b/examples/simple-host/package.json index 0de72ce..03686fe 100644 --- a/examples/simple-host/package.json +++ b/examples/simple-host/package.json @@ -4,34 +4,30 @@ "version": "1.0.0", "type": "module", "scripts": { - "start:server": "tsx server.ts", - "start:mcp-server": "cd ../simple-server && npm install && npm run start", - "build": "concurrently 'INPUT=example-host-vanilla.html vite build' 'INPUT=example-host-react.html vite build' 'INPUT=sandbox.html vite build'", - "server": "bun server.ts", - "start": "NODE_ENV=development npm run build && concurrently 'npm run start:server' 'npm run start:mcp-server'" + "start": "NODE_ENV=development npm run build && concurrently 'npm run start:server'", + "start:server": "bun server.ts", + "build": "concurrently 'INPUT=example-host-vanilla.html vite build' 'INPUT=example-host-react.html vite build' 'INPUT=sandbox.html vite build'" }, "dependencies": { "@modelcontextprotocol/ext-apps": "../..", "@modelcontextprotocol/sdk": "^1.22.0", - "react-dom": "^19.2.0", "react": "^19.2.0", + "react-dom": "^19.2.0", "zod": "^3.25.0" }, "devDependencies": { "@types/express": "^5.0.0", "@types/node": "^22.0.0", - "@types/react-dom": "^19.2.2", "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", "@vitejs/plugin-react": "^4.3.4", + "bun": "^1.3.2", "concurrently": "^9.2.1", "cors": "^2.8.5", - "esbuild": "~0.19.10", "express": "^5.1.0", "prettier": "^3.6.2", - "tsx": "^4.20.6", - "typescript": "^5.9.3", - "vite-plugin-singlefile": "^2.3.0", "vite": "^6.0.0", + "vite-plugin-singlefile": "^2.3.0", "vitest": "^3.2.4" } } diff --git a/examples/simple-server/package.json b/examples/simple-server/package.json index 21a1edd..52eb470 100644 --- a/examples/simple-server/package.json +++ b/examples/simple-server/package.json @@ -4,9 +4,9 @@ "private": true, "type": "module", "scripts": { + "start": "NODE_ENV=development npm run build && npm run server", "build": "concurrently 'INPUT=ui-raw.html vite build' 'INPUT=ui-vanilla.html vite build' 'INPUT=ui-react.html vite build'", - "server": "bun server.ts", - "start": "npm run build && npm run server" + "server": "bun server.ts" }, "dependencies": { "@modelcontextprotocol/ext-apps": "../..", diff --git a/examples/simple-server/server.ts b/examples/simple-server/server.ts index 0e4c7a5..b329d6b 100644 --- a/examples/simple-server/server.ts +++ b/examples/simple-server/server.ts @@ -14,6 +14,7 @@ import cors from "cors"; import path from "node:path"; import fs from "node:fs/promises"; import { fileURLToPath } from "node:url"; +import { RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -81,7 +82,7 @@ const getServer = async () => { message: z.string().describe("Message to display"), }, _meta: { - "ui/resourceUri": rawResource.uri, + [RESOURCE_URI_META_KEY]: rawResource.uri, }, }, async ({ message }): Promise => ({ @@ -112,7 +113,7 @@ const getServer = async () => { message: z.string().describe("Message to display"), }, _meta: { - "ui/resourceUri": vanillaResource.uri, + [RESOURCE_URI_META_KEY]: vanillaResource.uri, }, }, async ({ message }): Promise => ({ @@ -143,7 +144,7 @@ const getServer = async () => { message: z.string().describe("Message to display"), }, _meta: { - "ui/resourceUri": reactResource.uri, + [RESOURCE_URI_META_KEY]: reactResource.uri, }, }, async ({ message }): Promise => ({ diff --git a/examples/simple-server/src/ui-raw.ts b/examples/simple-server/src/ui-raw.ts index 1000571..1cf2b2e 100644 --- a/examples/simple-server/src/ui-raw.ts +++ b/examples/simple-server/src/ui-raw.ts @@ -131,19 +131,11 @@ window.addEventListener("load", async () => { }); new ResizeObserver(() => { - const { body, documentElement: html } = document; - - const bodyStyle = getComputedStyle(body); - const htmlStyle = getComputedStyle(html); - - const width = body.scrollWidth; - const height = - body.scrollHeight + - (parseFloat(bodyStyle.borderTop) || 0) + - (parseFloat(bodyStyle.borderBottom) || 0) + - (parseFloat(htmlStyle.borderTop) || 0) + - (parseFloat(htmlStyle.borderBottom) || 0); - + const rect = ( + document.body.parentElement ?? document.body + ).getBoundingClientRect(); + const width = Math.ceil(rect.width); + const height = Math.ceil(rect.height); app.sendNotification({ method: "ui/notifications/size-change", params: { width, height }, diff --git a/examples/simple-server/src/ui-vanilla.ts b/examples/simple-server/src/ui-vanilla.ts index 1407127..f536a45 100644 --- a/examples/simple-server/src/ui-vanilla.ts +++ b/examples/simple-server/src/ui-vanilla.ts @@ -48,13 +48,6 @@ window.addEventListener("load", async () => { appendText(`Host context changed: ${JSON.stringify(params)}`); }; - document.body.addEventListener("resize", () => { - app.sendSizeChange({ - width: document.body.scrollWidth, - height: document.body.scrollHeight, - }); - }); - root.appendChild( Object.assign(document.createElement("button"), { textContent: "Get Weather (Tool)", diff --git a/package.json b/package.json index 39647cd..ef32670 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,9 @@ "dist" ], "scripts": { - "start": "cd examples/simple-host && npm i && npm start", + "start:example-host": "cd examples/simple-host && npm --registry=https://registry.npmjs.org/ i && npm start", + "start:example-mcp-server": "cd examples//simple-server && npm --registry=https://registry.npmjs.org/ install && npm start", + "start": "NODE_ENV=development npm run build && concurrently 'npm run start:example-host' 'npm run start:example-mcp-server'", "build": "bun build.bun.ts", "prepare": "npm run build", "prettier:base-cmd": "prettier -u --ignore-path ./.gitignore --ignore-path ./.prettierignore", @@ -36,15 +38,16 @@ "@types/react-dom": "^19.2.2", "concurrently": "^9.2.1", "cors": "^2.8.5", + "esbuild": "^0.25.12", "express": "^5.1.0", - "prettier": "^3.6.2" + "prettier": "^3.6.2", + "typescript": "^5.9.3" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.22.0", "bun": "^1.3.2", "react": "^19.2.0", "react-dom": "^19.2.0", - "typescript": "5.7.2", "zod": "^3.25" } } diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 24d63aa..b70e27e 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -316,7 +316,7 @@ function sendRequest(method: string, params: any) { if (event.data?.id === id) { window.removeEventListener('message', listener); if (event.data?.result) { - resolve(true); + resolve(event.data?.result); } else if (event.data?.error) { reject(new Error(event.data.error)); } diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 0053c3b..ea4c5e8 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -185,13 +185,6 @@ export class AppBridge extends Protocol { }; } - sendSizeChange(params: McpUiSizeChangeNotification["params"]) { - return this.notification({ - method: "ui/notifications/size-change", - params, - }); - } - sendToolInput(params: McpUiToolInputNotification["params"]) { return this.notification({ method: "ui/notifications/tool-input", diff --git a/src/app.ts b/src/app.ts index 230af27..fb5ef6f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -41,6 +41,8 @@ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; export { PostMessageTransport } from "./message-transport.js"; export * from "./types"; +export const RESOURCE_URI_META_KEY = "ui/resourceUri"; + type AppOptions = ProtocolOptions & { autoResize?: boolean; }; @@ -166,30 +168,20 @@ export class App extends Protocol { } setupSizeChangeNotifications() { + let scheduled = false; const sendBodySizeChange = () => { - let rafId: number | null = null; - - // Debounce using requestAnimationFrame to avoid duplicate messages - // when both documentElement and body fire resize events - if (rafId !== null) { - cancelAnimationFrame(rafId); + if (scheduled) { + return; } - rafId = requestAnimationFrame(() => { - const { body, documentElement: html } = document; - - const bodyStyle = getComputedStyle(body); - const htmlStyle = getComputedStyle(html); - - const width = body.scrollWidth; - const height = - body.scrollHeight + - (parseFloat(bodyStyle.borderTop) || 0) + - (parseFloat(bodyStyle.borderBottom) || 0) + - (parseFloat(htmlStyle.borderTop) || 0) + - (parseFloat(htmlStyle.borderBottom) || 0); - + scheduled = true; + requestAnimationFrame(() => { + scheduled = false; + const rect = ( + document.body.parentElement ?? document.body + ).getBoundingClientRect(); + const width = Math.ceil(rect.width); + const height = Math.ceil(rect.height); this.sendSizeChange({ width, height }); - rafId = null; }); }; diff --git a/src/message-transport.ts b/src/message-transport.ts index 90b8e21..4210d4d 100644 --- a/src/message-transport.ts +++ b/src/message-transport.ts @@ -3,12 +3,10 @@ import { JSONRPCMessageSchema, MessageExtraInfo, } from "@modelcontextprotocol/sdk/types.js"; -import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; import { Transport, TransportSendOptions, } from "@modelcontextprotocol/sdk/shared/transport.js"; -import { z } from "zod"; export class PostMessageTransport implements Transport { private messageListener: (