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:
parent
0e62ceca92
commit
3df49c38dc
11 changed files with 38 additions and 100 deletions
|
|
@ -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" }),
|
||||
]);
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "../..",
|
||||
|
|
|
|||
|
|
@ -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> => ({
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
34
src/app.ts
34
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<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;
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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: (
|
||||
|
|
|
|||
Loading…
Reference in a new issue