Skip to content

Commit 93bd3b7

Browse files
committed
Merge main into ochafik/swift-sdk
2 parents 1b219cf + 2411c71 commit 93bd3b7

File tree

107 files changed

+5564
-936
lines changed

Some content is hidden

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

107 files changed

+5564
-936
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ jobs:
157157
mkdir test-project
158158
cd test-project
159159
npm init -y
160-
# Install from the PR branch
161-
npm install "git+https://github.com/${{ github.repository }}#${{ github.head_ref || github.ref_name }}"
160+
# Install from the PR branch (use head repo for fork PRs)
161+
npm install "git+https://github.com/${{ github.event.pull_request.head.repo.full_name || github.repository }}#${{ github.head_ref || github.ref_name }}"
162162
# Verify the package is usable (ESM import)
163163
node --input-type=module -e "import { App } from '@modelcontextprotocol/ext-apps'; console.log('Import successful:', typeof App)"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ To run all examples together:
5757

5858
```bash
5959
npm install
60-
npm run examples:start
60+
npm start
6161
```
6262

6363
Then open http://localhost:8080/.

build.bun.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,20 @@ function buildJs(entrypoint: string, opts: Record<string, any> = {}) {
2323
}
2424

2525
await Promise.all([
26-
buildJs("src/app.ts", { outdir: "dist/src" }),
26+
buildJs("src/app.ts", {
27+
outdir: "dist/src",
28+
external: ["@modelcontextprotocol/sdk"],
29+
}),
2730
buildJs("src/app-bridge.ts", {
2831
outdir: "dist/src",
2932
external: ["@modelcontextprotocol/sdk"],
3033
}),
3134
buildJs("src/react/index.tsx", {
3235
outdir: "dist/src/react",
33-
external: ["react", "react-dom"],
36+
external: ["react", "react-dom", "@modelcontextprotocol/sdk"],
37+
}),
38+
buildJs("src/server/index.ts", {
39+
outdir: "dist/src/server",
40+
external: ["@modelcontextprotocol/sdk"],
3441
}),
3542
]);

docs/quickstart.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ Create `server.ts`:
9797
```typescript
9898
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9999
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
100-
import { RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps";
100+
import {
101+
RESOURCE_MIME_TYPE,
102+
type McpUiToolMeta,
103+
} from "@modelcontextprotocol/ext-apps";
101104
import cors from "cors";
102105
import express from "express";
103106
import fs from "node:fs/promises";
@@ -119,7 +122,7 @@ server.registerTool(
119122
description: "Returns the current server time.",
120123
inputSchema: {},
121124
outputSchema: { time: z.string() },
122-
_meta: { [RESOURCE_URI_META_KEY]: resourceUri }, // Links tool to UI
125+
_meta: { ui: { resourceUri } as McpUiToolMeta }, // Links tool to UI
123126
},
124127
async () => {
125128
const time = new Date().toISOString();
@@ -177,7 +180,7 @@ app.listen(3001, (err) => {
177180
Then, verify your server compiles:
178181

179182
```bash
180-
npx tsc --noEmit server.ts
183+
npx tsc --noEmit
181184
```
182185

183186
No output means success. If you see errors, check for typos in `server.ts`.

examples/basic-host/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
"version": "1.0.0",
55
"type": "module",
66
"scripts": {
7-
"build": "concurrently \"cross-env INPUT=index.html vite build\" \"cross-env INPUT=sandbox.html vite build\"",
7+
"build": "tsc --noEmit && concurrently \"cross-env INPUT=index.html vite build\" \"cross-env INPUT=sandbox.html vite build\"",
88
"watch": "concurrently \"cross-env INPUT=index.html vite build --watch\" \"cross-env INPUT=sandbox.html vite build --watch\"",
99
"serve": "bun serve.ts",
1010
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
1111
"dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\""
1212
},
1313
"dependencies": {
1414
"@modelcontextprotocol/ext-apps": "../..",
15-
"@modelcontextprotocol/sdk": "^1.22.0",
15+
"@modelcontextprotocol/sdk": "^1.24.0",
1616
"react": "^19.2.0",
1717
"react-dom": "^19.2.0",
1818
"zod": "^4.1.13"

examples/basic-host/src/implementation.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport } from "@modelcontextprotocol/ext-apps/app-bridge";
1+
import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport } from "@modelcontextprotocol/ext-apps/app-bridge";
22
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
33
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
44
import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
@@ -77,7 +77,7 @@ export function callTool(
7777

7878
const toolCallInfo: ToolCallInfo = { serverInfo, tool, input, resultPromise };
7979

80-
const uiResourceUri = getUiResourceUri(tool);
80+
const uiResourceUri = getToolUiResourceUri(tool);
8181
if (uiResourceUri) {
8282
toolCallInfo.appResourcePromise = getUiResource(serverInfo, uiResourceUri);
8383
}
@@ -86,16 +86,6 @@ export function callTool(
8686
}
8787

8888

89-
function getUiResourceUri(tool: Tool): string | undefined {
90-
const uri = tool._meta?.[RESOURCE_URI_META_KEY];
91-
if (typeof uri === "string" && uri.startsWith("ui://")) {
92-
return uri;
93-
} else if (uri !== undefined) {
94-
throw new Error(`Invalid UI resource URI: ${JSON.stringify(uri)}`);
95-
}
96-
}
97-
98-
9989
async function getUiResource(serverInfo: ServerInfo, uri: string): Promise<UiResourceData> {
10090
log.info("Reading UI resource:", uri);
10191
const resource = await serverInfo.client.readResource({ uri });

examples/basic-host/src/index.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ function AppIFramePanel({ toolCallInfo, isDestroying, onTeardownComplete }: AppI
289289
}
290290

291291
log.info("Sending teardown notification to MCP App");
292-
appBridgeRef.current.sendResourceTeardown({})
292+
appBridgeRef.current.teardownResource({})
293293
.catch((err) => {
294294
log.warn("Teardown request failed (app may have already closed):", err);
295295
})
@@ -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);

examples/basic-server-react/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ An MCP App example with a React UI.
99

1010
- Tool registration with a linked UI resource
1111
- React UI using the [`useApp()`](https://modelcontextprotocol.github.io/ext-apps/api/functions/_modelcontextprotocol_ext-apps_react.useApp.html) hook
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), [`sendOpenLink`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendopenlink)
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)
1313

1414
## Key Files
1515

examples/basic-server-react/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
"private": true,
55
"type": "module",
66
"scripts": {
7-
"build": "cross-env INPUT=mcp-app.html vite build",
7+
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build",
88
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
99
"serve": "bun server.ts",
1010
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
1111
"dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'"
1212
},
1313
"dependencies": {
1414
"@modelcontextprotocol/ext-apps": "../..",
15-
"@modelcontextprotocol/sdk": "^1.22.0",
15+
"@modelcontextprotocol/sdk": "^1.24.0",
1616
"react": "^19.2.0",
1717
"react-dom": "^19.2.0",
1818
"zod": "^4.1.13"

0 commit comments

Comments
 (0)