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",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start:server": "tsx server.ts",
|
"start": "NODE_ENV=development npm run build && concurrently 'npm run start:server'",
|
||||||
"start:mcp-server": "cd ../simple-server && npm install && npm run start",
|
"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'",
|
"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'"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/ext-apps": "../..",
|
"@modelcontextprotocol/ext-apps": "../..",
|
||||||
"@modelcontextprotocol/sdk": "^1.22.0",
|
"@modelcontextprotocol/sdk": "^1.22.0",
|
||||||
"react-dom": "^19.2.0",
|
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
"zod": "^3.25.0"
|
"zod": "^3.25.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@types/react-dom": "^19.2.2",
|
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
|
"@types/react-dom": "^19.2.2",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"bun": "^1.3.2",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"esbuild": "~0.19.10",
|
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"tsx": "^4.20.6",
|
|
||||||
"typescript": "^5.9.3",
|
|
||||||
"vite-plugin-singlefile": "^2.3.0",
|
|
||||||
"vite": "^6.0.0",
|
"vite": "^6.0.0",
|
||||||
|
"vite-plugin-singlefile": "^2.3.0",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"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'",
|
"build": "concurrently 'INPUT=ui-raw.html vite build' 'INPUT=ui-vanilla.html vite build' 'INPUT=ui-react.html vite build'",
|
||||||
"server": "bun server.ts",
|
"server": "bun server.ts"
|
||||||
"start": "npm run build && npm run server"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/ext-apps": "../..",
|
"@modelcontextprotocol/ext-apps": "../..",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import cors from "cors";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
@ -81,7 +82,7 @@ const getServer = async () => {
|
||||||
message: z.string().describe("Message to display"),
|
message: z.string().describe("Message to display"),
|
||||||
},
|
},
|
||||||
_meta: {
|
_meta: {
|
||||||
"ui/resourceUri": rawResource.uri,
|
[RESOURCE_URI_META_KEY]: rawResource.uri,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ message }): Promise<CallToolResult> => ({
|
async ({ message }): Promise<CallToolResult> => ({
|
||||||
|
|
@ -112,7 +113,7 @@ const getServer = async () => {
|
||||||
message: z.string().describe("Message to display"),
|
message: z.string().describe("Message to display"),
|
||||||
},
|
},
|
||||||
_meta: {
|
_meta: {
|
||||||
"ui/resourceUri": vanillaResource.uri,
|
[RESOURCE_URI_META_KEY]: vanillaResource.uri,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ message }): Promise<CallToolResult> => ({
|
async ({ message }): Promise<CallToolResult> => ({
|
||||||
|
|
@ -143,7 +144,7 @@ const getServer = async () => {
|
||||||
message: z.string().describe("Message to display"),
|
message: z.string().describe("Message to display"),
|
||||||
},
|
},
|
||||||
_meta: {
|
_meta: {
|
||||||
"ui/resourceUri": reactResource.uri,
|
[RESOURCE_URI_META_KEY]: reactResource.uri,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ message }): Promise<CallToolResult> => ({
|
async ({ message }): Promise<CallToolResult> => ({
|
||||||
|
|
|
||||||
|
|
@ -131,19 +131,11 @@ window.addEventListener("load", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
new ResizeObserver(() => {
|
new ResizeObserver(() => {
|
||||||
const { body, documentElement: html } = document;
|
const rect = (
|
||||||
|
document.body.parentElement ?? document.body
|
||||||
const bodyStyle = getComputedStyle(body);
|
).getBoundingClientRect();
|
||||||
const htmlStyle = getComputedStyle(html);
|
const width = Math.ceil(rect.width);
|
||||||
|
const height = Math.ceil(rect.height);
|
||||||
const width = body.scrollWidth;
|
|
||||||
const height =
|
|
||||||
body.scrollHeight +
|
|
||||||
(parseFloat(bodyStyle.borderTop) || 0) +
|
|
||||||
(parseFloat(bodyStyle.borderBottom) || 0) +
|
|
||||||
(parseFloat(htmlStyle.borderTop) || 0) +
|
|
||||||
(parseFloat(htmlStyle.borderBottom) || 0);
|
|
||||||
|
|
||||||
app.sendNotification<McpUiSizeChangeNotification>({
|
app.sendNotification<McpUiSizeChangeNotification>({
|
||||||
method: "ui/notifications/size-change",
|
method: "ui/notifications/size-change",
|
||||||
params: { width, height },
|
params: { width, height },
|
||||||
|
|
|
||||||
|
|
@ -48,13 +48,6 @@ window.addEventListener("load", async () => {
|
||||||
appendText(`Host context changed: ${JSON.stringify(params)}`);
|
appendText(`Host context changed: ${JSON.stringify(params)}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
document.body.addEventListener("resize", () => {
|
|
||||||
app.sendSizeChange({
|
|
||||||
width: document.body.scrollWidth,
|
|
||||||
height: document.body.scrollHeight,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
root.appendChild(
|
root.appendChild(
|
||||||
Object.assign(document.createElement("button"), {
|
Object.assign(document.createElement("button"), {
|
||||||
textContent: "Get Weather (Tool)",
|
textContent: "Get Weather (Tool)",
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,9 @@
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"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",
|
"build": "bun build.bun.ts",
|
||||||
"prepare": "npm run build",
|
"prepare": "npm run build",
|
||||||
"prettier:base-cmd": "prettier -u --ignore-path ./.gitignore --ignore-path ./.prettierignore",
|
"prettier:base-cmd": "prettier -u --ignore-path ./.gitignore --ignore-path ./.prettierignore",
|
||||||
|
|
@ -36,15 +38,16 @@
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"esbuild": "^0.25.12",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"prettier": "^3.6.2"
|
"prettier": "^3.6.2",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.22.0",
|
"@modelcontextprotocol/sdk": "^1.22.0",
|
||||||
"bun": "^1.3.2",
|
"bun": "^1.3.2",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"typescript": "5.7.2",
|
|
||||||
"zod": "^3.25"
|
"zod": "^3.25"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -316,7 +316,7 @@ function sendRequest(method: string, params: any) {
|
||||||
if (event.data?.id === id) {
|
if (event.data?.id === id) {
|
||||||
window.removeEventListener('message', listener);
|
window.removeEventListener('message', listener);
|
||||||
if (event.data?.result) {
|
if (event.data?.result) {
|
||||||
resolve(true);
|
resolve(event.data?.result);
|
||||||
} else if (event.data?.error) {
|
} else if (event.data?.error) {
|
||||||
reject(new Error(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"]) {
|
sendToolInput(params: McpUiToolInputNotification["params"]) {
|
||||||
return this.notification(<McpUiToolInputNotification>{
|
return this.notification(<McpUiToolInputNotification>{
|
||||||
method: "ui/notifications/tool-input",
|
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 { PostMessageTransport } from "./message-transport.js";
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
|
|
||||||
|
export const RESOURCE_URI_META_KEY = "ui/resourceUri";
|
||||||
|
|
||||||
type AppOptions = ProtocolOptions & {
|
type AppOptions = ProtocolOptions & {
|
||||||
autoResize?: boolean;
|
autoResize?: boolean;
|
||||||
};
|
};
|
||||||
|
|
@ -166,30 +168,20 @@ export class App extends Protocol<Request, Notification, Result> {
|
||||||
}
|
}
|
||||||
|
|
||||||
setupSizeChangeNotifications() {
|
setupSizeChangeNotifications() {
|
||||||
|
let scheduled = false;
|
||||||
const sendBodySizeChange = () => {
|
const sendBodySizeChange = () => {
|
||||||
let rafId: number | null = null;
|
if (scheduled) {
|
||||||
|
return;
|
||||||
// Debounce using requestAnimationFrame to avoid duplicate messages
|
|
||||||
// when both documentElement and body fire resize events
|
|
||||||
if (rafId !== null) {
|
|
||||||
cancelAnimationFrame(rafId);
|
|
||||||
}
|
}
|
||||||
rafId = requestAnimationFrame(() => {
|
scheduled = true;
|
||||||
const { body, documentElement: html } = document;
|
requestAnimationFrame(() => {
|
||||||
|
scheduled = false;
|
||||||
const bodyStyle = getComputedStyle(body);
|
const rect = (
|
||||||
const htmlStyle = getComputedStyle(html);
|
document.body.parentElement ?? document.body
|
||||||
|
).getBoundingClientRect();
|
||||||
const width = body.scrollWidth;
|
const width = Math.ceil(rect.width);
|
||||||
const height =
|
const height = Math.ceil(rect.height);
|
||||||
body.scrollHeight +
|
|
||||||
(parseFloat(bodyStyle.borderTop) || 0) +
|
|
||||||
(parseFloat(bodyStyle.borderBottom) || 0) +
|
|
||||||
(parseFloat(htmlStyle.borderTop) || 0) +
|
|
||||||
(parseFloat(htmlStyle.borderBottom) || 0);
|
|
||||||
|
|
||||||
this.sendSizeChange({ width, height });
|
this.sendSizeChange({ width, height });
|
||||||
rafId = null;
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,10 @@ import {
|
||||||
JSONRPCMessageSchema,
|
JSONRPCMessageSchema,
|
||||||
MessageExtraInfo,
|
MessageExtraInfo,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
|
|
||||||
import {
|
import {
|
||||||
Transport,
|
Transport,
|
||||||
TransportSendOptions,
|
TransportSendOptions,
|
||||||
} from "@modelcontextprotocol/sdk/shared/transport.js";
|
} from "@modelcontextprotocol/sdk/shared/transport.js";
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export class PostMessageTransport implements Transport {
|
export class PostMessageTransport implements Transport {
|
||||||
private messageListener: (
|
private messageListener: (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue