Skip to content

Commit d7ba146

Browse files
committed
chore: merge main into release-0.3.0
Merge latest changes from main including: - Vue, Svelte, Preact, and Solid basic server examples (#141) - safeAreaInsets support (#202) - E2E test fixes (#206) - npm publishing for examples (#184) - ui.resourceUri optional (#210) - Method names as consts (#192) - toolInfo.id optional (#216) - PostMessageTransport security fixes (#207, #208) - Server-utils.ts refactoring
2 parents afecfa2 + 7eb98ad commit d7ba146

125 files changed

Lines changed: 6043 additions & 2163 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/npm-publish.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,47 @@ jobs:
8383
- run: npm publish --provenance --access public ${{ steps.npm-tag.outputs.tag }}
8484
env:
8585
NODE_AUTH_TOKEN: ${{ secrets.NPM_SECRET }}
86+
87+
publish-examples:
88+
runs-on: ubuntu-latest
89+
if: github.event_name == 'release'
90+
environment: Release
91+
needs: [publish]
92+
93+
permissions:
94+
contents: read
95+
id-token: write
96+
97+
strategy:
98+
fail-fast: false
99+
matrix:
100+
example:
101+
- basic-server-react
102+
- basic-server-vanillajs
103+
- budget-allocator-server
104+
- cohort-heatmap-server
105+
- customer-segmentation-server
106+
- scenario-modeler-server
107+
- system-monitor-server
108+
- threejs-server
109+
- wiki-explorer-server
110+
111+
steps:
112+
- uses: actions/checkout@v4
113+
- uses: oven-sh/setup-bun@v2
114+
with:
115+
bun-version: latest
116+
- uses: actions/setup-node@v4
117+
with:
118+
node-version: "22"
119+
cache: npm
120+
registry-url: "https://registry.npmjs.org"
121+
- run: npm ci
122+
123+
- name: Build example
124+
run: npm run build --workspace examples/${{ matrix.example }}
125+
126+
- name: Publish example
127+
run: npm publish --workspace examples/${{ matrix.example }} --provenance --access public
128+
env:
129+
NODE_AUTH_TOKEN: ${{ secrets.NPM_SECRET }}

.github/workflows/publish.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,16 @@ jobs:
2020
cache: npm
2121
- run: npm ci
2222
- run: npm run build
23-
- run: npx pkg-pr-new publish
23+
- run: npm run examples:build
24+
- run: |
25+
npx pkg-pr-new publish \
26+
. \
27+
./examples/basic-server-react \
28+
./examples/basic-server-vanillajs \
29+
./examples/budget-allocator-server \
30+
./examples/cohort-heatmap-server \
31+
./examples/customer-segmentation-server \
32+
./examples/scenario-modeler-server \
33+
./examples/system-monitor-server \
34+
./examples/threejs-server \
35+
./examples/wiki-explorer-server

.prettierignore

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
examples/basic-host/**/*.ts
22
examples/basic-host/**/*.tsx
3-
examples/basic-server-react/**/*.ts
4-
examples/basic-server-react/**/*.tsx
5-
examples/basic-server-vanillajs/**/*.ts
6-
examples/basic-server-vanillajs/**/*.tsx
3+
examples/basic-server-*/**/*.ts
4+
examples/basic-server-*/**/*.tsx

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,13 @@ Or edit your `package.json` manually:
4747

4848
Start with these foundational examples to learn the SDK:
4949

50-
- [`examples/basic-server-vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) — Example MCP server with tools that return UI Apps (vanilla JS)
51-
- [`examples/basic-server-react`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) — Example MCP server with tools that return UI Apps (React)
52-
- [`examples/basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) — Bare-bones example of hosting MCP Apps
50+
- [`examples/basic-server-vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) — MCP server + MCP App using vanilla JS
51+
- [`examples/basic-server-react`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) — MCP server + MCP App using [React](https://github.com/facebook/react)
52+
- [`examples/basic-server-vue`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vue) — MCP server + MCP App using [Vue](https://github.com/vuejs/vue)
53+
- [`examples/basic-server-svelte`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-svelte) — MCP server + MCP App using [Svelte](https://github.com/sveltejs/svelte)
54+
- [`examples/basic-server-preact`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-preact) — MCP server + MCP App using [Preact](https://github.com/preactjs/preact)
55+
- [`examples/basic-server-solid`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-solid) — MCP server + MCP App using [Solid](https://github.com/solidjs/solid)
56+
- [`examples/basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) — MCP host application supporting MCP Apps
5357

5458
The [`examples/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples) directory contains additional demo apps showcasing real-world use cases.
5559

examples/basic-host/src/index.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,27 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
350350
async function connectToAllServers(): Promise<ServerInfo[]> {
351351
const serverUrlsResponse = await fetch("/api/servers");
352352
const serverUrls = (await serverUrlsResponse.json()) as string[];
353-
return Promise.all(serverUrls.map((url) => connectToServer(new URL(url))));
353+
354+
// Use allSettled to be resilient to individual server failures
355+
const results = await Promise.allSettled(
356+
serverUrls.map((url) => connectToServer(new URL(url)))
357+
);
358+
359+
const servers: ServerInfo[] = [];
360+
for (let i = 0; i < results.length; i++) {
361+
const result = results[i];
362+
if (result.status === "fulfilled") {
363+
servers.push(result.value);
364+
} else {
365+
console.warn(`[HOST] Failed to connect to ${serverUrls[i]}:`, result.reason);
366+
}
367+
}
368+
369+
if (servers.length === 0 && serverUrls.length > 0) {
370+
throw new Error(`Failed to connect to any servers (${serverUrls.length} attempted)`);
371+
}
372+
373+
return servers;
354374
}
355375

356376
createRoot(document.getElementById("root")!).render(

examples/basic-host/src/sandbox.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ if (!document.referrer.match(ALLOWED_REFERRER_PATTERN)) {
1616
);
1717
}
1818

19+
// Extract the expected host origin from the referrer for origin validation.
20+
// This is the origin we expect all parent messages to come from.
21+
const EXPECTED_HOST_ORIGIN = new URL(document.referrer).origin;
22+
23+
const OWN_ORIGIN = new URL(window.location.href).origin;
24+
1925
// Security self-test: verify iframe isolation is working correctly.
2026
// This MUST throw a SecurityError -- if `window.top` is accessible, the sandbox
2127
// configuration is dangerously broken and untrusted content could escape.
@@ -79,8 +85,18 @@ function buildCspMetaTag(csp?: { connectDomains?: string[]; resourceDomains?: st
7985

8086
window.addEventListener("message", async (event) => {
8187
if (event.source === window.parent) {
82-
// NOTE: In production you'll also want to validate `event.origin` against
83-
// your Host domain.
88+
// Validate that messages from parent come from the expected host origin.
89+
// This prevents malicious pages from sending messages to this sandbox.
90+
if (event.origin !== EXPECTED_HOST_ORIGIN) {
91+
console.error(
92+
"[Sandbox] Rejecting message from unexpected origin:",
93+
event.origin,
94+
"expected:",
95+
EXPECTED_HOST_ORIGIN
96+
);
97+
return;
98+
}
99+
84100
if (event.data && event.data.method === RESOURCE_READY_NOTIFICATION) {
85101
const { html, sandbox, csp } = event.data.params;
86102
if (typeof sandbox === "string") {
@@ -112,14 +128,25 @@ window.addEventListener("message", async (event) => {
112128
}
113129
}
114130
} else if (event.source === inner.contentWindow) {
131+
if (event.origin !== OWN_ORIGIN) {
132+
console.error(
133+
"[Sandbox] Rejecting message from inner iframe with unexpected origin:",
134+
event.origin,
135+
"expected:",
136+
OWN_ORIGIN
137+
);
138+
return;
139+
}
115140
// Relay messages from inner frame to parent window.
116-
window.parent.postMessage(event.data, "*");
141+
// Use specific origin instead of "*" to prevent message interception.
142+
window.parent.postMessage(event.data, EXPECTED_HOST_ORIGIN);
117143
}
118144
});
119145

120146
// Notify the Host that the Sandbox is ready to receive Guest UI HTML.
147+
// Use specific origin instead of "*" to ensure only the expected host receives this.
121148
window.parent.postMessage({
122149
jsonrpc: "2.0",
123150
method: PROXY_READY_NOTIFICATION,
124151
params: {},
125-
}, "*");
152+
}, EXPECTED_HOST_ORIGIN);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Example: Basic Server (Preact)
2+
3+
An MCP App example with a Preact UI.
4+
5+
> [!TIP]
6+
> Looking for a vanilla JavaScript example? See [`basic-server-vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs)!
7+
8+
## Overview
9+
10+
- Tool registration with a linked UI resource
11+
- Preact UI using the [`App`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html) class
12+
- App communication APIs: [`callServerTool`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#callservertool), [`sendMessage`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendmessage), [`sendLog`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendlog), [`openLink`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#openlink)
13+
14+
## Key Files
15+
16+
- [`server.ts`](server.ts) - MCP server with tool and resource registration
17+
- [`mcp-app.html`](mcp-app.html) / [`src/mcp-app.tsx`](src/mcp-app.tsx) - Preact UI using `App` class
18+
19+
## Getting Started
20+
21+
```bash
22+
npm install
23+
npm run dev
24+
```
25+
26+
## How It Works
27+
28+
1. The server registers a `get-time` tool with metadata linking it to a UI HTML resource (`ui://get-time/mcp-app.html`).
29+
2. When the tool is invoked, the Host renders the UI from the resource.
30+
3. The UI uses the MCP App SDK API to communicate with the host and call server tools.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta name="color-scheme" content="light dark">
7+
<title>Get Time App</title>
8+
<link rel="stylesheet" href="/src/global.css">
9+
</head>
10+
<body>
11+
<div id="root"></div>
12+
<script type="module" src="/src/mcp-app.tsx"></script>
13+
</body>
14+
</html>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "@modelcontextprotocol/server-basic-preact",
3+
"version": "0.1.0",
4+
"type": "module",
5+
"description": "Basic MCP App Server example using Preact",
6+
"repository": {
7+
"type": "git",
8+
"url": "https://github.com/modelcontextprotocol/ext-apps",
9+
"directory": "examples/basic-server-preact"
10+
},
11+
"license": "MIT",
12+
"main": "server.ts",
13+
"files": [
14+
"server.ts",
15+
"server-utils.ts",
16+
"dist"
17+
],
18+
"scripts": {
19+
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build",
20+
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
21+
"serve": "bun server.ts",
22+
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
23+
"dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'",
24+
"prepublishOnly": "npm run build"
25+
},
26+
"dependencies": {
27+
"@modelcontextprotocol/ext-apps": "^0.2.2",
28+
"@modelcontextprotocol/sdk": "^1.24.0",
29+
"preact": "^10.0.0",
30+
"zod": "^4.1.13"
31+
},
32+
"devDependencies": {
33+
"@preact/preset-vite": "^2.0.0",
34+
"@types/cors": "^2.8.19",
35+
"@types/express": "^5.0.0",
36+
"@types/node": "^22.0.0",
37+
"concurrently": "^9.2.1",
38+
"cors": "^2.8.5",
39+
"cross-env": "^10.1.0",
40+
"express": "^5.1.0",
41+
"typescript": "^5.9.3",
42+
"vite": "^6.0.0",
43+
"vite-plugin-singlefile": "^2.3.0"
44+
}
45+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Shared utilities for running MCP servers with Streamable HTTP transport.
3+
*/
4+
5+
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
6+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
8+
import cors from "cors";
9+
import type { Request, Response } from "express";
10+
11+
export interface ServerOptions {
12+
port: number;
13+
name?: string;
14+
}
15+
16+
/**
17+
* Starts an MCP server with Streamable HTTP transport in stateless mode.
18+
*
19+
* @param createServer - Factory function that creates a new McpServer instance per request.
20+
* @param options - Server configuration options.
21+
*/
22+
export async function startServer(
23+
createServer: () => McpServer,
24+
options: ServerOptions,
25+
): Promise<void> {
26+
const { port, name = "MCP Server" } = options;
27+
28+
const app = createMcpExpressApp({ host: "0.0.0.0" });
29+
app.use(cors());
30+
31+
app.all("/mcp", async (req: Request, res: Response) => {
32+
const server = createServer();
33+
const transport = new StreamableHTTPServerTransport({
34+
sessionIdGenerator: undefined,
35+
});
36+
37+
res.on("close", () => {
38+
transport.close().catch(() => {});
39+
server.close().catch(() => {});
40+
});
41+
42+
try {
43+
await server.connect(transport);
44+
await transport.handleRequest(req, res, req.body);
45+
} catch (error) {
46+
console.error("MCP error:", error);
47+
if (!res.headersSent) {
48+
res.status(500).json({
49+
jsonrpc: "2.0",
50+
error: { code: -32603, message: "Internal server error" },
51+
id: null,
52+
});
53+
}
54+
}
55+
});
56+
57+
const httpServer = app.listen(port, () => {
58+
console.log(`${name} listening on http://localhost:${port}/mcp`);
59+
});
60+
61+
const shutdown = () => {
62+
console.log("\nShutting down...");
63+
httpServer.close(() => process.exit(0));
64+
};
65+
66+
process.on("SIGINT", shutdown);
67+
process.on("SIGTERM", shutdown);
68+
}

0 commit comments

Comments
 (0)