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
This commit is contained in:
Olivier Chafik 2025-11-25 02:39:33 +00:00 • committed by GitHub
commit 3df49c38dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 38 additions and 100 deletions

View file

@ -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" }),
]);

View file

@ -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"
}
}

View file

@ -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": "../..",

View file

@ -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<CallToolResult> => ({
@ -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<CallToolResult> => ({
@ -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<CallToolResult> => ({

View file

@ -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<McpUiSizeChangeNotification>({
method: "ui/notifications/size-change",
params: { width, height },

View file

@ -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)",

View file

@ -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"
}
}

View file

@ -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));
}

View file

@ -185,13 +185,6 @@ export class AppBridge extends Protocol<Request, Notification, Result> {
};
}
sendSizeChange(params: McpUiSizeChangeNotification["params"]) {
return this.notification(<McpUiSizeChangeNotification>{
method: "ui/notifications/size-change",
params,
});
}
sendToolInput(params: McpUiToolInputNotification["params"]) {
return this.notification(<McpUiToolInputNotification>{
method: "ui/notifications/tool-input",

View file

@ -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<Request, Notification, Result> {
}
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;
});
};

View file

@ -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: (