Skip to content

Commit 2f58643

Browse files
Add exemple implementation
1 parent 637f498 commit 2f58643

4 files changed

Lines changed: 85 additions & 1594 deletions

File tree

examples/basic-host/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
},
2020
"devDependencies": {
2121
"@types/express": "^5.0.0",
22-
"@types/node": "22.10.0",
22+
"@types/node": "24.13.2",
2323
"@types/react": "^19.2.2",
2424
"@types/react-dom": "^19.2.2",
2525
"@vitejs/plugin-react": "^4.3.4",

examples/basic-host/src/implementation.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ interface UiResourceData {
7272
html: string;
7373
csp?: McpUiResourceCsp;
7474
permissions?: McpUiResourcePermissions;
75+
linkTrustedDomains?: string[];
7576
}
7677

7778
export interface ToolCallInfo {
@@ -151,8 +152,9 @@ async function getUiResource(serverInfo: ServerInfo, uri: string): Promise<UiRes
151152
const uiMeta = contentMeta?.ui ?? listingMeta?.ui;
152153
const csp = uiMeta?.csp;
153154
const permissions = uiMeta?.permissions;
155+
const linkTrustedDomains = uiMeta?.linkTrustedDomains;
154156

155-
return { html, csp, permissions };
157+
return { html, csp, permissions, linkTrustedDomains };
156158
}
157159

158160

@@ -271,6 +273,7 @@ export interface AppBridgeCallbacks {
271273
export interface AppBridgeOptions {
272274
containerDimensions?: { maxHeight?: number; width?: number } | { height: number; width?: number };
273275
displayMode?: "inline" | "fullscreen";
276+
linkTrustedDomains?: string[];
274277
}
275278

276279
export function newAppBridge(
@@ -340,8 +343,24 @@ export function newAppBridge(
340343

341344
appBridge.onopenlink = async (params, _extra) => {
342345
log.info("Open link request:", params);
343-
window.open(params.url, "_blank", "noopener,noreferrer");
344-
return {};
346+
347+
const HOST_OPENLINK_DENYLIST = [new URL("https://malicious.com")];
348+
if (HOST_OPENLINK_DENYLIST.some(({ origin }) => origin === new URL(params.url).origin)) {
349+
log.info("Blocked link by host denylist:", params.url);
350+
return { isError: true };
351+
}
352+
353+
const isTrustedByApp = options?.linkTrustedDomains?.some((trustedDomain) =>
354+
new URLPattern(trustedDomain).test(params.url)
355+
);
356+
const shouldOpen = isTrustedByApp || window.confirm(`Open external link?\n${params.url}`);
357+
if (shouldOpen) {
358+
window.open(params.url, "_blank", "noopener,noreferrer");
359+
return {};
360+
}
361+
362+
log.info("User declined to open link:", params.url);
363+
return { isError: true };
345364
};
346365

347366
appBridge.onloggingmessage = (params) => {

examples/basic-host/src/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ function AppIFramePanel({ toolCallInfo, isDestroying, onTeardownComplete }: AppI
434434

435435
// First get CSP and permissions from resource, then load sandbox
436436
// CSP is set via HTTP headers (tamper-proof), permissions via iframe allow attribute
437-
toolCallInfo.appResourcePromise.then(({ csp, permissions }) => {
437+
toolCallInfo.appResourcePromise.then(({ csp, permissions, linkTrustedDomains }) => {
438438
loadSandboxProxy(iframe, csp, permissions).then((firstTime) => {
439439
// The `firstTime` check guards against React Strict Mode's double
440440
// invocation (mount → unmount → remount simulation in development).
@@ -449,6 +449,7 @@ function AppIFramePanel({ toolCallInfo, isDestroying, onTeardownComplete }: AppI
449449
// Provide container dimensions - maxHeight for flexible sizing
450450
containerDimensions: { maxHeight: 6000 },
451451
displayMode: "inline",
452+
linkTrustedDomains,
452453
});
453454
appBridgeRef.current = appBridge;
454455
initializeApp(iframe, appBridge, toolCallInfo);

0 commit comments

Comments
 (0)