fix: proper CSP handling in basic-host sandboxing (HTTP headers, worker-src, document.write) (#234)
* fix: Move CSP to HTTP headers + add worker-src, frameDomains, baseUriDomains, permissions Security improvements: - CSP is now set via HTTP headers in serve.ts instead of meta tags (meta tag CSP can be tampered with by same-origin content) - CSP passed as query param to sandbox.html for header-based enforcement New CSP/permissions features (borrowed from PR #158): - frameDomains: control frame-src directive for nested iframes - baseUriDomains: control base-uri directive - permissions: camera, microphone, geolocation via iframe allow attribute WebGL fix: - Use document.write() instead of srcdoc for inner iframe content (srcdoc creates opaque origin that breaks WebGL canvas updates) - Add worker-src directive with blob: support (critical for WebGL apps like CesiumJS/Three.js that use workers for tile decoding, terrain processing, image processing) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: Remove permissions support (defer to PR #158) Keep this PR focused on CSP security fixes only. Permissions (camera, microphone, geolocation) will be handled in #158. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * cleanup McpUiResourceCsp type usage / duplicate defs * fix: guard against CSP injection in domain parameters Validate CSP domain entries to reject characters that could: - Break out of CSP directives (semicolons, newlines) - Inject CSP keywords like 'unsafe-eval' (quotes) - Inject multiple sources in one entry (spaces) This prevents injection attacks where malicious domains could override the security policy. --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
df49e88b2d
commit
8b09f4278c
11 changed files with 572 additions and 155 deletions
|
|
@ -3,8 +3,8 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="color-scheme" content="light dark">
|
<meta name="color-scheme" content="light dark">
|
||||||
<!-- CSP is set by serve.ts HTTP header - no meta tag needed here
|
<!-- CSP is set via HTTP headers by serve.ts (based on ?csp= query param).
|
||||||
The inner iframe's CSP is dynamically injected based on resource metadata -->
|
The inner iframe inherits this CSP since we use document.write(). -->
|
||||||
<title>MCP-UI Proxy</title>
|
<title>MCP-UI Proxy</title>
|
||||||
<style>
|
<style>
|
||||||
html,
|
html,
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,19 @@
|
||||||
/**
|
/**
|
||||||
* HTTP servers for the MCP UI example:
|
* HTTP servers for the MCP UI example:
|
||||||
* - Host server (port 8080): serves host HTML files (React and Vanilla examples)
|
* - Host server (port 8080): serves host HTML files (React and Vanilla examples)
|
||||||
* - Sandbox server (port 8081): serves sandbox.html with permissive CSP
|
* - Sandbox server (port 8081): serves sandbox.html with CSP headers
|
||||||
*
|
*
|
||||||
* Running on separate ports ensures proper origin isolation for security.
|
* Running on separate ports ensures proper origin isolation for security.
|
||||||
|
*
|
||||||
|
* Security: CSP is set via HTTP headers based on ?csp= query param.
|
||||||
|
* This ensures content cannot tamper with CSP (unlike meta tags).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { dirname, join } from "path";
|
import { dirname, join } from "path";
|
||||||
|
import type { McpUiResourceCsp } from "@modelcontextprotocol/ext-apps";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
@ -50,26 +54,74 @@ hostApp.get("/", (_req, res) => {
|
||||||
const sandboxApp = express();
|
const sandboxApp = express();
|
||||||
sandboxApp.use(cors());
|
sandboxApp.use(cors());
|
||||||
|
|
||||||
// Permissive CSP for sandbox content
|
// Validate CSP domain entries to prevent injection attacks.
|
||||||
sandboxApp.use((_req, res, next) => {
|
// Rejects entries containing characters that could:
|
||||||
const csp = [
|
// - `;` or newlines: break out to new CSP directive
|
||||||
"default-src 'self'",
|
// - quotes: inject CSP keywords like 'unsafe-eval'
|
||||||
"img-src * data: blob: 'unsafe-inline'",
|
// - space: inject multiple sources in one entry
|
||||||
"style-src * blob: data: 'unsafe-inline'",
|
function sanitizeCspDomains(domains?: string[]): string[] {
|
||||||
"script-src * blob: data: 'unsafe-inline' 'unsafe-eval'",
|
if (!domains) return [];
|
||||||
"connect-src *",
|
return domains.filter((d) => typeof d === "string" && !/[;\r\n'" ]/.test(d));
|
||||||
"font-src * blob: data:",
|
}
|
||||||
"media-src * blob: data:",
|
|
||||||
"frame-src * blob: data:",
|
function buildCspHeader(csp?: McpUiResourceCsp): string {
|
||||||
].join("; ");
|
const resourceDomains = sanitizeCspDomains(csp?.resourceDomains).join(" ");
|
||||||
res.setHeader("Content-Security-Policy", csp);
|
const connectDomains = sanitizeCspDomains(csp?.connectDomains).join(" ");
|
||||||
|
const frameDomains = sanitizeCspDomains(csp?.frameDomains).join(" ") || null;
|
||||||
|
const baseUriDomains =
|
||||||
|
sanitizeCspDomains(csp?.baseUriDomains).join(" ") || null;
|
||||||
|
|
||||||
|
const directives = [
|
||||||
|
// Default: allow same-origin + inline styles/scripts (needed for bundled apps)
|
||||||
|
"default-src 'self' 'unsafe-inline'",
|
||||||
|
// Scripts: same-origin + inline + eval (some libs need eval) + blob (workers) + specified domains
|
||||||
|
`script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data: ${resourceDomains}`.trim(),
|
||||||
|
// Styles: same-origin + inline + specified domains
|
||||||
|
`style-src 'self' 'unsafe-inline' blob: data: ${resourceDomains}`.trim(),
|
||||||
|
// Images: same-origin + data/blob URIs + specified domains
|
||||||
|
`img-src 'self' data: blob: ${resourceDomains}`.trim(),
|
||||||
|
// Fonts: same-origin + data/blob URIs + specified domains
|
||||||
|
`font-src 'self' data: blob: ${resourceDomains}`.trim(),
|
||||||
|
// Network requests: same-origin + specified API/tile domains
|
||||||
|
`connect-src 'self' ${connectDomains}`.trim(),
|
||||||
|
// Workers: same-origin + blob (dynamic workers) + specified domains
|
||||||
|
// This is critical for WebGL apps (CesiumJS, Three.js) that use workers for:
|
||||||
|
// - Tile decoding and terrain processing
|
||||||
|
// - Image processing and texture loading
|
||||||
|
// - Physics and geometry calculations
|
||||||
|
`worker-src 'self' blob: ${resourceDomains}`.trim(),
|
||||||
|
// Nested iframes: use frameDomains if provided, otherwise block all
|
||||||
|
frameDomains ? `frame-src ${frameDomains}` : "frame-src 'none'",
|
||||||
|
// Plugins: always blocked (defense in depth)
|
||||||
|
"object-src 'none'",
|
||||||
|
// Base URI: use baseUriDomains if provided, otherwise block all
|
||||||
|
baseUriDomains ? `base-uri ${baseUriDomains}` : "base-uri 'none'",
|
||||||
|
];
|
||||||
|
|
||||||
|
return directives.join("; ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve sandbox.html with CSP from query params
|
||||||
|
sandboxApp.get(["/", "/sandbox.html"], (req, res) => {
|
||||||
|
// Parse CSP config from query param: ?csp=<url-encoded-json>
|
||||||
|
let cspConfig: McpUiResourceCsp | undefined;
|
||||||
|
if (typeof req.query.csp === "string") {
|
||||||
|
try {
|
||||||
|
cspConfig = JSON.parse(req.query.csp);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[Sandbox] Invalid CSP query param:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set CSP via HTTP header - tamper-proof unlike meta tags
|
||||||
|
const cspHeader = buildCspHeader(cspConfig);
|
||||||
|
res.setHeader("Content-Security-Policy", cspHeader);
|
||||||
|
|
||||||
|
// Prevent caching to ensure fresh CSP on each load
|
||||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||||
res.setHeader("Pragma", "no-cache");
|
res.setHeader("Pragma", "no-cache");
|
||||||
res.setHeader("Expires", "0");
|
res.setHeader("Expires", "0");
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
sandboxApp.get(["/", "/sandbox.html"], (_req, res) => {
|
|
||||||
res.sendFile(join(DIRECTORY, "sandbox.html"));
|
res.sendFile(join(DIRECTORY, "sandbox.html"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport } from "@modelcontextprotocol/ext-apps/app-bridge";
|
import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport, type McpUiResourceCsp } from "@modelcontextprotocol/ext-apps/app-bridge";
|
||||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||||
import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
|
import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
||||||
|
|
||||||
const SANDBOX_PROXY_URL = new URL("http://localhost:8081/sandbox.html");
|
const SANDBOX_PROXY_BASE_URL = "http://localhost:8081/sandbox.html";
|
||||||
const IMPLEMENTATION = { name: "MCP Apps Host", version: "1.0.0" };
|
const IMPLEMENTATION = { name: "MCP Apps Host", version: "1.0.0" };
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -42,10 +42,7 @@ export async function connectToServer(serverUrl: URL): Promise<ServerInfo> {
|
||||||
|
|
||||||
interface UiResourceData {
|
interface UiResourceData {
|
||||||
html: string;
|
html: string;
|
||||||
csp?: {
|
csp?: McpUiResourceCsp;
|
||||||
connectDomains?: string[];
|
|
||||||
resourceDomains?: string[];
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolCallInfo {
|
export interface ToolCallInfo {
|
||||||
|
|
@ -120,7 +117,10 @@ async function getUiResource(serverInfo: ServerInfo, uri: string): Promise<UiRes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function loadSandboxProxy(iframe: HTMLIFrameElement): Promise<boolean> {
|
export function loadSandboxProxy(
|
||||||
|
iframe: HTMLIFrameElement,
|
||||||
|
csp?: McpUiResourceCsp,
|
||||||
|
): Promise<boolean> {
|
||||||
// Prevent reload
|
// Prevent reload
|
||||||
if (iframe.src) return Promise.resolve(false);
|
if (iframe.src) return Promise.resolve(false);
|
||||||
|
|
||||||
|
|
@ -140,8 +140,14 @@ export function loadSandboxProxy(iframe: HTMLIFrameElement): Promise<boolean> {
|
||||||
window.addEventListener("message", listener);
|
window.addEventListener("message", listener);
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info("Loading sandbox proxy...");
|
// Build sandbox URL with CSP query param for HTTP header-based CSP
|
||||||
iframe.src = SANDBOX_PROXY_URL.href;
|
const sandboxUrl = new URL(SANDBOX_PROXY_BASE_URL);
|
||||||
|
if (csp) {
|
||||||
|
sandboxUrl.searchParams.set("csp", JSON.stringify(csp));
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Loading sandbox proxy...", csp ? `(CSP: ${JSON.stringify(csp)})` : "");
|
||||||
|
iframe.src = sandboxUrl.href;
|
||||||
|
|
||||||
return readyPromise;
|
return readyPromise;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -263,16 +263,21 @@ function AppIFramePanel({ toolCallInfo, isDestroying, onTeardownComplete }: AppI
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const iframe = iframeRef.current!;
|
const iframe = iframeRef.current!;
|
||||||
loadSandboxProxy(iframe).then((firstTime) => {
|
|
||||||
// The `firstTime` check guards against React Strict Mode's double
|
// First get CSP from resource, then load sandbox with CSP in query param
|
||||||
// invocation (mount → unmount → remount simulation in development).
|
// This ensures CSP is set via HTTP headers (tamper-proof)
|
||||||
// Outside of Strict Mode, this `useEffect` runs only once per
|
toolCallInfo.appResourcePromise.then(({ csp }) => {
|
||||||
// `toolCallInfo`.
|
loadSandboxProxy(iframe, csp).then((firstTime) => {
|
||||||
if (firstTime) {
|
// The `firstTime` check guards against React Strict Mode's double
|
||||||
const appBridge = newAppBridge(toolCallInfo.serverInfo, iframe);
|
// invocation (mount → unmount → remount simulation in development).
|
||||||
appBridgeRef.current = appBridge;
|
// Outside of Strict Mode, this `useEffect` runs only once per
|
||||||
initializeApp(iframe, appBridge, toolCallInfo);
|
// `toolCallInfo`.
|
||||||
}
|
if (firstTime) {
|
||||||
|
const appBridge = newAppBridge(toolCallInfo.serverInfo, iframe);
|
||||||
|
appBridgeRef.current = appBridge;
|
||||||
|
initializeApp(iframe, appBridge, toolCallInfo);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}, [toolCallInfo]);
|
}, [toolCallInfo]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,26 +62,9 @@ const PROXY_READY_NOTIFICATION: McpUiSandboxProxyReadyNotification["method"] =
|
||||||
// Special case: The "ui/notifications/sandbox-proxy-ready" message is
|
// Special case: The "ui/notifications/sandbox-proxy-ready" message is
|
||||||
// intercepted here (not relayed) because the Sandbox uses it to configure and
|
// intercepted here (not relayed) because the Sandbox uses it to configure and
|
||||||
// load the inner iframe with the Guest UI HTML content.
|
// load the inner iframe with the Guest UI HTML content.
|
||||||
// Build CSP meta tag from domains
|
//
|
||||||
function buildCspMetaTag(csp?: { connectDomains?: string[]; resourceDomains?: string[] }): string {
|
// Security: CSP is enforced via HTTP headers on sandbox.html (set by serve.ts
|
||||||
const resourceDomains = csp?.resourceDomains?.join(" ") ?? "";
|
// based on ?csp= query param). This is tamper-proof unlike meta tags.
|
||||||
const connectDomains = csp?.connectDomains?.join(" ") ?? "";
|
|
||||||
|
|
||||||
// Base CSP directives
|
|
||||||
const directives = [
|
|
||||||
"default-src 'self'",
|
|
||||||
`script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data: ${resourceDomains}`.trim(),
|
|
||||||
`style-src 'self' 'unsafe-inline' blob: data: ${resourceDomains}`.trim(),
|
|
||||||
`img-src 'self' data: blob: ${resourceDomains}`.trim(),
|
|
||||||
`font-src 'self' data: blob: ${resourceDomains}`.trim(),
|
|
||||||
`connect-src 'self' ${connectDomains}`.trim(),
|
|
||||||
"frame-src 'none'",
|
|
||||||
"object-src 'none'",
|
|
||||||
"base-uri 'self'",
|
|
||||||
];
|
|
||||||
|
|
||||||
return `<meta http-equiv="Content-Security-Policy" content="${directives.join("; ")}">`;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("message", async (event) => {
|
window.addEventListener("message", async (event) => {
|
||||||
if (event.source === window.parent) {
|
if (event.source === window.parent) {
|
||||||
|
|
@ -98,29 +81,26 @@ window.addEventListener("message", async (event) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.data && event.data.method === RESOURCE_READY_NOTIFICATION) {
|
if (event.data && event.data.method === RESOURCE_READY_NOTIFICATION) {
|
||||||
const { html, sandbox, csp } = event.data.params;
|
const { html, sandbox } = event.data.params;
|
||||||
if (typeof sandbox === "string") {
|
if (typeof sandbox === "string") {
|
||||||
inner.setAttribute("sandbox", sandbox);
|
inner.setAttribute("sandbox", sandbox);
|
||||||
}
|
}
|
||||||
if (typeof html === "string") {
|
if (typeof html === "string") {
|
||||||
// Inject CSP meta tag at the start of <head> if CSP is provided
|
// Use document.write instead of srcdoc for WebGL compatibility.
|
||||||
console.log("[Sandbox] Received CSP:", csp);
|
// srcdoc creates an opaque origin which prevents WebGL canvas updates
|
||||||
let modifiedHtml = html;
|
// from being displayed properly. document.write preserves the sandbox
|
||||||
if (csp) {
|
// origin, allowing WebGL to work correctly.
|
||||||
const cspMetaTag = buildCspMetaTag(csp);
|
// CSP is enforced via HTTP headers on this page (sandbox.html).
|
||||||
console.log("[Sandbox] Injecting CSP meta tag:", cspMetaTag);
|
const doc = inner.contentDocument || inner.contentWindow?.document;
|
||||||
// Insert after <head> tag if present, otherwise prepend
|
if (doc) {
|
||||||
if (modifiedHtml.includes("<head>")) {
|
doc.open();
|
||||||
modifiedHtml = modifiedHtml.replace("<head>", `<head>\n${cspMetaTag}`);
|
doc.write(html);
|
||||||
} else if (modifiedHtml.includes("<head ")) {
|
doc.close();
|
||||||
modifiedHtml = modifiedHtml.replace(/<head[^>]*>/, `$&\n${cspMetaTag}`);
|
|
||||||
} else {
|
|
||||||
modifiedHtml = cspMetaTag + modifiedHtml;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.log("[Sandbox] No CSP provided, using default");
|
// Fallback to srcdoc if document is not accessible
|
||||||
|
console.warn("[Sandbox] document.write not available, falling back to srcdoc");
|
||||||
|
inner.srcdoc = html;
|
||||||
}
|
}
|
||||||
inner.srcdoc = modifiedHtml;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (inner && inner.contentWindow) {
|
if (inner && inner.contentWindow) {
|
||||||
|
|
|
||||||
325
examples/basic-host/src/tamper-detection.ts
Normal file
325
examples/basic-host/src/tamper-detection.ts
Normal file
|
|
@ -0,0 +1,325 @@
|
||||||
|
/**
|
||||||
|
* Tamper Detection Module for MCP-UI Sandbox
|
||||||
|
*
|
||||||
|
* Generates a script that monitors security-sensitive JavaScript APIs
|
||||||
|
* and emits notifications when guest code accesses them.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type TamperRuleTarget =
|
||||||
|
| "window"
|
||||||
|
| "Element.prototype"
|
||||||
|
| "Document.prototype"
|
||||||
|
| "Node.prototype"
|
||||||
|
| "HTMLElement.prototype"
|
||||||
|
| "EventTarget.prototype"
|
||||||
|
| "History.prototype"
|
||||||
|
| "Location.prototype";
|
||||||
|
|
||||||
|
export type TamperRuleType = "getter" | "method" | "both";
|
||||||
|
export type TamperSeverity = "low" | "medium" | "high" | "critical";
|
||||||
|
|
||||||
|
export interface TamperRule {
|
||||||
|
target: TamperRuleTarget;
|
||||||
|
property: string;
|
||||||
|
type: TamperRuleType;
|
||||||
|
severity: TamperSeverity;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TAMPER_DETECTED_NOTIFICATION =
|
||||||
|
"ui/notifications/tamper-detected" as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default rules for security-sensitive APIs.
|
||||||
|
* - critical: Access is blocked (returns undefined / throws)
|
||||||
|
* - high/medium/low: Access is allowed but an alert is emitted
|
||||||
|
*/
|
||||||
|
export const DEFAULT_TAMPER_RULES: TamperRule[] = [
|
||||||
|
// === CRITICAL (blocked) ===
|
||||||
|
// Escape attempts
|
||||||
|
{ target: "window", property: "top", type: "getter", severity: "critical" },
|
||||||
|
{
|
||||||
|
target: "window",
|
||||||
|
property: "parent",
|
||||||
|
type: "getter",
|
||||||
|
severity: "critical",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: "window",
|
||||||
|
property: "opener",
|
||||||
|
type: "getter",
|
||||||
|
severity: "critical",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: "window",
|
||||||
|
property: "frameElement",
|
||||||
|
type: "getter",
|
||||||
|
severity: "critical",
|
||||||
|
},
|
||||||
|
// Code execution
|
||||||
|
{ target: "window", property: "eval", type: "method", severity: "critical" },
|
||||||
|
|
||||||
|
// === HIGH (allowed with alert) ===
|
||||||
|
{ target: "window", property: "Function", type: "getter", severity: "high" },
|
||||||
|
// Network/data exfiltration
|
||||||
|
{ target: "window", property: "fetch", type: "method", severity: "high" },
|
||||||
|
{
|
||||||
|
target: "window",
|
||||||
|
property: "XMLHttpRequest",
|
||||||
|
type: "getter",
|
||||||
|
severity: "high",
|
||||||
|
},
|
||||||
|
{ target: "window", property: "WebSocket", type: "getter", severity: "high" },
|
||||||
|
{
|
||||||
|
target: "window",
|
||||||
|
property: "EventSource",
|
||||||
|
type: "getter",
|
||||||
|
severity: "high",
|
||||||
|
},
|
||||||
|
// DOM injection (high-risk methods)
|
||||||
|
{
|
||||||
|
target: "Document.prototype",
|
||||||
|
property: "write",
|
||||||
|
type: "method",
|
||||||
|
severity: "high",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: "Document.prototype",
|
||||||
|
property: "writeln",
|
||||||
|
type: "method",
|
||||||
|
severity: "high",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: "Element.prototype",
|
||||||
|
property: "insertAdjacentHTML",
|
||||||
|
type: "method",
|
||||||
|
severity: "high",
|
||||||
|
},
|
||||||
|
|
||||||
|
// === MEDIUM (allowed with alert) ===
|
||||||
|
// Storage access
|
||||||
|
{
|
||||||
|
target: "window",
|
||||||
|
property: "localStorage",
|
||||||
|
type: "getter",
|
||||||
|
severity: "medium",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: "window",
|
||||||
|
property: "sessionStorage",
|
||||||
|
type: "getter",
|
||||||
|
severity: "medium",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: "window",
|
||||||
|
property: "indexedDB",
|
||||||
|
type: "getter",
|
||||||
|
severity: "medium",
|
||||||
|
},
|
||||||
|
{ target: "window", property: "caches", type: "getter", severity: "medium" },
|
||||||
|
// DOM injection (property-based)
|
||||||
|
{
|
||||||
|
target: "Element.prototype",
|
||||||
|
property: "innerHTML",
|
||||||
|
type: "both",
|
||||||
|
severity: "medium",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: "Element.prototype",
|
||||||
|
property: "outerHTML",
|
||||||
|
type: "both",
|
||||||
|
severity: "medium",
|
||||||
|
},
|
||||||
|
// DOM traversal
|
||||||
|
{
|
||||||
|
target: "Node.prototype",
|
||||||
|
property: "parentNode",
|
||||||
|
type: "getter",
|
||||||
|
severity: "medium",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: "Node.prototype",
|
||||||
|
property: "parentElement",
|
||||||
|
type: "getter",
|
||||||
|
severity: "medium",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: "Element.prototype",
|
||||||
|
property: "closest",
|
||||||
|
type: "method",
|
||||||
|
severity: "medium",
|
||||||
|
},
|
||||||
|
// History manipulation
|
||||||
|
{ target: "window", property: "history", type: "getter", severity: "medium" },
|
||||||
|
{
|
||||||
|
target: "window",
|
||||||
|
property: "location",
|
||||||
|
type: "getter",
|
||||||
|
severity: "medium",
|
||||||
|
},
|
||||||
|
|
||||||
|
// === LOW (allowed with alert) ===
|
||||||
|
// Message passing
|
||||||
|
{
|
||||||
|
target: "window",
|
||||||
|
property: "postMessage",
|
||||||
|
type: "method",
|
||||||
|
severity: "low",
|
||||||
|
},
|
||||||
|
// DOM queries
|
||||||
|
{
|
||||||
|
target: "Document.prototype",
|
||||||
|
property: "querySelector",
|
||||||
|
type: "method",
|
||||||
|
severity: "low",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: "Document.prototype",
|
||||||
|
property: "querySelectorAll",
|
||||||
|
type: "method",
|
||||||
|
severity: "low",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: "Document.prototype",
|
||||||
|
property: "getElementById",
|
||||||
|
type: "method",
|
||||||
|
severity: "low",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the tamper detection script as a string.
|
||||||
|
* This script should be injected into guest HTML before any other scripts run.
|
||||||
|
*/
|
||||||
|
export function generateTamperDetectionScript(
|
||||||
|
rules: TamperRule[] = DEFAULT_TAMPER_RULES,
|
||||||
|
): string {
|
||||||
|
const rulesJson = JSON.stringify(rules);
|
||||||
|
|
||||||
|
return `(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var NOTIFICATION_METHOD = '${TAMPER_DETECTED_NOTIFICATION}';
|
||||||
|
var rules = ${rulesJson};
|
||||||
|
|
||||||
|
function emitTamperAlert(rule, accessType, stackTrace) {
|
||||||
|
try {
|
||||||
|
window.parent.postMessage({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
method: NOTIFICATION_METHOD,
|
||||||
|
params: {
|
||||||
|
property: rule.target + '.' + rule.property,
|
||||||
|
accessType: accessType,
|
||||||
|
severity: rule.severity,
|
||||||
|
blocked: rule.severity === 'critical',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
stack: stackTrace
|
||||||
|
}
|
||||||
|
}, '*');
|
||||||
|
} catch (e) {
|
||||||
|
// Silently fail if postMessage is unavailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldBlock(rule) {
|
||||||
|
return rule.severity === 'critical';
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapProperty(target, prop, rule) {
|
||||||
|
var descriptor = Object.getOwnPropertyDescriptor(target, prop);
|
||||||
|
if (!descriptor) return false;
|
||||||
|
|
||||||
|
var newDescriptor = {
|
||||||
|
configurable: false,
|
||||||
|
enumerable: descriptor.enumerable
|
||||||
|
};
|
||||||
|
|
||||||
|
var wrapped = false;
|
||||||
|
|
||||||
|
if (rule.type === 'getter' || rule.type === 'both') {
|
||||||
|
var originalGetter = descriptor.get || (function() { return descriptor.value; });
|
||||||
|
newDescriptor.get = function() {
|
||||||
|
emitTamperAlert(rule, 'read', new Error().stack);
|
||||||
|
if (shouldBlock(rule)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return originalGetter.call(this);
|
||||||
|
};
|
||||||
|
wrapped = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.type === 'method' && typeof descriptor.value === 'function') {
|
||||||
|
var originalMethod = descriptor.value;
|
||||||
|
newDescriptor.value = function() {
|
||||||
|
emitTamperAlert(rule, 'call', new Error().stack);
|
||||||
|
if (shouldBlock(rule)) {
|
||||||
|
throw new Error('Access to ' + rule.target + '.' + rule.property + ' is blocked');
|
||||||
|
}
|
||||||
|
return originalMethod.apply(this, arguments);
|
||||||
|
};
|
||||||
|
wrapped = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.type === 'both' && descriptor.set) {
|
||||||
|
var originalSetter = descriptor.set;
|
||||||
|
newDescriptor.set = function(value) {
|
||||||
|
emitTamperAlert(rule, 'write', new Error().stack);
|
||||||
|
if (shouldBlock(rule)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return originalSetter.call(this, value);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wrapped) {
|
||||||
|
try {
|
||||||
|
Object.defineProperty(target, prop, newDescriptor);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTargetObject(targetName) {
|
||||||
|
switch (targetName) {
|
||||||
|
case 'window': return window;
|
||||||
|
case 'Element.prototype': return Element.prototype;
|
||||||
|
case 'Document.prototype': return Document.prototype;
|
||||||
|
case 'Node.prototype': return Node.prototype;
|
||||||
|
case 'HTMLElement.prototype': return HTMLElement.prototype;
|
||||||
|
case 'EventTarget.prototype': return EventTarget.prototype;
|
||||||
|
case 'History.prototype': return History.prototype;
|
||||||
|
case 'Location.prototype': return Location.prototype;
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install monitoring
|
||||||
|
for (var i = 0; i < rules.length; i++) {
|
||||||
|
var rule = rules[i];
|
||||||
|
var target = getTargetObject(rule.target);
|
||||||
|
if (target) {
|
||||||
|
wrapProperty(target, rule.property, rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects a script into HTML content, ensuring it runs before any other scripts.
|
||||||
|
*/
|
||||||
|
export function injectScriptIntoHtml(html: string, script: string): string {
|
||||||
|
const scriptTag = `<script>${script}</script>`;
|
||||||
|
|
||||||
|
// Try <head> first
|
||||||
|
if (html.includes("<head>")) {
|
||||||
|
return html.replace("<head>", `<head>${scriptTag}`);
|
||||||
|
}
|
||||||
|
// Try <body>
|
||||||
|
if (html.includes("<body>")) {
|
||||||
|
return html.replace("<body>", `<body>${scriptTag}`);
|
||||||
|
}
|
||||||
|
// Fallback: prepend
|
||||||
|
return scriptTag + html;
|
||||||
|
}
|
||||||
|
|
@ -109,16 +109,9 @@ interface UIResource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UIResourceMeta {
|
interface McpUiResourceCsp {
|
||||||
/**
|
|
||||||
* Content Security Policy configuration
|
|
||||||
*
|
|
||||||
* Servers declare which external origins their UI needs to access.
|
|
||||||
* Hosts use this to enforce appropriate CSP headers.
|
|
||||||
*/
|
|
||||||
csp?: {
|
|
||||||
/**
|
/**
|
||||||
* Origins for network requests
|
* Origins for network requests (fetch/XHR/WebSocket)
|
||||||
*
|
*
|
||||||
* - Empty or omitted = no external connections (secure default)
|
* - Empty or omitted = no external connections (secure default)
|
||||||
* - Maps to CSP `connect-src` directive
|
* - Maps to CSP `connect-src` directive
|
||||||
|
|
@ -138,7 +131,24 @@ interface UIResourceMeta {
|
||||||
* ["https://cdn.jsdelivr.net", "https://*.cloudflare.com"]
|
* ["https://cdn.jsdelivr.net", "https://*.cloudflare.com"]
|
||||||
*/
|
*/
|
||||||
resourceDomains?: string[],
|
resourceDomains?: string[],
|
||||||
},
|
/**
|
||||||
|
* Origins for nested iframes (frame-src directive).
|
||||||
|
*/
|
||||||
|
frameDomains?: string[],
|
||||||
|
/**
|
||||||
|
* Allowed base URIs for the document (base-uri directive).
|
||||||
|
*/
|
||||||
|
baseUriDomains?: string[],
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UIResourceMeta {
|
||||||
|
/**
|
||||||
|
* Content Security Policy configuration
|
||||||
|
*
|
||||||
|
* Servers declare which external origins their UI needs to access.
|
||||||
|
* Hosts use this to enforce appropriate CSP headers.
|
||||||
|
*/
|
||||||
|
csp?: McpUiResourceCsp,
|
||||||
/**
|
/**
|
||||||
* Dedicated origin for widget
|
* Dedicated origin for widget
|
||||||
*
|
*
|
||||||
|
|
@ -178,8 +188,10 @@ The resource content is returned via `resources/read`:
|
||||||
_meta?: {
|
_meta?: {
|
||||||
ui?: {
|
ui?: {
|
||||||
csp?: {
|
csp?: {
|
||||||
connectDomains?: string[]; // Origins for fetch/XHR/WebSocket
|
connectDomains?: string[]; // Origins for network requests (fetch/XHR/WebSocket).
|
||||||
resourceDomains?: string[]; // Origins for images, scripts, etc
|
resourceDomains?: string[]; // Origins for static resources (scripts, images, styles, fonts).
|
||||||
|
frameDomains?: string[]; // Origins for nested iframes (frame-src directive).
|
||||||
|
baseUriDomains?: string[]; // Allowed base URIs for the document (base-uri directive).
|
||||||
};
|
};
|
||||||
domain?: string;
|
domain?: string;
|
||||||
prefersBorder?: boolean;
|
prefersBorder?: boolean;
|
||||||
|
|
|
||||||
42
src/generated/schema.json
generated
42
src/generated/schema.json
generated
|
|
@ -3475,6 +3475,20 @@
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"frameDomains": {
|
||||||
|
"description": "Origins for nested iframes (frame-src directive).",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"baseUriDomains": {
|
||||||
|
"description": "Allowed base URIs for the document (base-uri directive).",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|
@ -3500,6 +3514,20 @@
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"frameDomains": {
|
||||||
|
"description": "Origins for nested iframes (frame-src directive).",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"baseUriDomains": {
|
||||||
|
"description": "Allowed base URIs for the document (base-uri directive).",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|
@ -3593,6 +3621,20 @@
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"frameDomains": {
|
||||||
|
"description": "Origins for nested iframes (frame-src directive).",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"baseUriDomains": {
|
||||||
|
"description": "Allowed base URIs for the document (base-uri directive).",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|
|
||||||
28
src/generated/schema.test.ts
generated
28
src/generated/schema.test.ts
generated
|
|
@ -43,8 +43,8 @@ export type McpUiSandboxProxyReadyNotificationSchemaInferredType = z.infer<
|
||||||
typeof generated.McpUiSandboxProxyReadyNotificationSchema
|
typeof generated.McpUiSandboxProxyReadyNotificationSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type McpUiSandboxResourceReadyNotificationSchemaInferredType = z.infer<
|
export type McpUiResourceCspSchemaInferredType = z.infer<
|
||||||
typeof generated.McpUiSandboxResourceReadyNotificationSchema
|
typeof generated.McpUiResourceCspSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type McpUiSizeChangedNotificationSchemaInferredType = z.infer<
|
export type McpUiSizeChangedNotificationSchemaInferredType = z.infer<
|
||||||
|
|
@ -91,10 +91,6 @@ export type McpUiInitializedNotificationSchemaInferredType = z.infer<
|
||||||
typeof generated.McpUiInitializedNotificationSchema
|
typeof generated.McpUiInitializedNotificationSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type McpUiResourceCspSchemaInferredType = z.infer<
|
|
||||||
typeof generated.McpUiResourceCspSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type McpUiResourceMetaSchemaInferredType = z.infer<
|
export type McpUiResourceMetaSchemaInferredType = z.infer<
|
||||||
typeof generated.McpUiResourceMetaSchema
|
typeof generated.McpUiResourceMetaSchema
|
||||||
>;
|
>;
|
||||||
|
|
@ -119,6 +115,10 @@ export type McpUiMessageRequestSchemaInferredType = z.infer<
|
||||||
typeof generated.McpUiMessageRequestSchema
|
typeof generated.McpUiMessageRequestSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type McpUiSandboxResourceReadyNotificationSchemaInferredType = z.infer<
|
||||||
|
typeof generated.McpUiSandboxResourceReadyNotificationSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export type McpUiToolResultNotificationSchemaInferredType = z.infer<
|
export type McpUiToolResultNotificationSchemaInferredType = z.infer<
|
||||||
typeof generated.McpUiToolResultNotificationSchema
|
typeof generated.McpUiToolResultNotificationSchema
|
||||||
>;
|
>;
|
||||||
|
|
@ -171,12 +171,8 @@ expectType<spec.McpUiSandboxProxyReadyNotification>(
|
||||||
expectType<McpUiSandboxProxyReadyNotificationSchemaInferredType>(
|
expectType<McpUiSandboxProxyReadyNotificationSchemaInferredType>(
|
||||||
{} as spec.McpUiSandboxProxyReadyNotification,
|
{} as spec.McpUiSandboxProxyReadyNotification,
|
||||||
);
|
);
|
||||||
expectType<spec.McpUiSandboxResourceReadyNotification>(
|
expectType<spec.McpUiResourceCsp>({} as McpUiResourceCspSchemaInferredType);
|
||||||
{} as McpUiSandboxResourceReadyNotificationSchemaInferredType,
|
expectType<McpUiResourceCspSchemaInferredType>({} as spec.McpUiResourceCsp);
|
||||||
);
|
|
||||||
expectType<McpUiSandboxResourceReadyNotificationSchemaInferredType>(
|
|
||||||
{} as spec.McpUiSandboxResourceReadyNotification,
|
|
||||||
);
|
|
||||||
expectType<spec.McpUiSizeChangedNotification>(
|
expectType<spec.McpUiSizeChangedNotification>(
|
||||||
{} as McpUiSizeChangedNotificationSchemaInferredType,
|
{} as McpUiSizeChangedNotificationSchemaInferredType,
|
||||||
);
|
);
|
||||||
|
|
@ -235,8 +231,6 @@ expectType<spec.McpUiInitializedNotification>(
|
||||||
expectType<McpUiInitializedNotificationSchemaInferredType>(
|
expectType<McpUiInitializedNotificationSchemaInferredType>(
|
||||||
{} as spec.McpUiInitializedNotification,
|
{} as spec.McpUiInitializedNotification,
|
||||||
);
|
);
|
||||||
expectType<spec.McpUiResourceCsp>({} as McpUiResourceCspSchemaInferredType);
|
|
||||||
expectType<McpUiResourceCspSchemaInferredType>({} as spec.McpUiResourceCsp);
|
|
||||||
expectType<spec.McpUiResourceMeta>({} as McpUiResourceMetaSchemaInferredType);
|
expectType<spec.McpUiResourceMeta>({} as McpUiResourceMetaSchemaInferredType);
|
||||||
expectType<McpUiResourceMetaSchemaInferredType>({} as spec.McpUiResourceMeta);
|
expectType<McpUiResourceMetaSchemaInferredType>({} as spec.McpUiResourceMeta);
|
||||||
expectType<spec.McpUiRequestDisplayModeRequest>(
|
expectType<spec.McpUiRequestDisplayModeRequest>(
|
||||||
|
|
@ -265,6 +259,12 @@ expectType<spec.McpUiMessageRequest>(
|
||||||
expectType<McpUiMessageRequestSchemaInferredType>(
|
expectType<McpUiMessageRequestSchemaInferredType>(
|
||||||
{} as spec.McpUiMessageRequest,
|
{} as spec.McpUiMessageRequest,
|
||||||
);
|
);
|
||||||
|
expectType<spec.McpUiSandboxResourceReadyNotification>(
|
||||||
|
{} as McpUiSandboxResourceReadyNotificationSchemaInferredType,
|
||||||
|
);
|
||||||
|
expectType<McpUiSandboxResourceReadyNotificationSchemaInferredType>(
|
||||||
|
{} as spec.McpUiSandboxResourceReadyNotification,
|
||||||
|
);
|
||||||
expectType<spec.McpUiToolResultNotification>(
|
expectType<spec.McpUiToolResultNotification>(
|
||||||
{} as McpUiToolResultNotificationSchemaInferredType,
|
{} as McpUiToolResultNotificationSchemaInferredType,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
92
src/generated/schema.ts
generated
92
src/generated/schema.ts
generated
|
|
@ -186,39 +186,29 @@ export const McpUiSandboxProxyReadyNotificationSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Notification containing HTML resource for the sandbox proxy to load.
|
* @description Content Security Policy configuration for UI resources.
|
||||||
* @internal
|
|
||||||
* @see https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx#sandbox-proxy
|
|
||||||
*/
|
*/
|
||||||
export const McpUiSandboxResourceReadyNotificationSchema = z.object({
|
export const McpUiResourceCspSchema = z.object({
|
||||||
method: z.literal("ui/notifications/sandbox-resource-ready"),
|
/** @description Origins for network requests (fetch/XHR/WebSocket). */
|
||||||
params: z.object({
|
connectDomains: z
|
||||||
/** @description HTML content to load into the inner iframe. */
|
.array(z.string())
|
||||||
html: z.string().describe("HTML content to load into the inner iframe."),
|
.optional()
|
||||||
/** @description Optional override for the inner iframe's sandbox attribute. */
|
.describe("Origins for network requests (fetch/XHR/WebSocket)."),
|
||||||
sandbox: z
|
/** @description Origins for static resources (scripts, images, styles, fonts). */
|
||||||
.string()
|
resourceDomains: z
|
||||||
.optional()
|
.array(z.string())
|
||||||
.describe("Optional override for the inner iframe's sandbox attribute."),
|
.optional()
|
||||||
/** @description CSP configuration from resource metadata. */
|
.describe("Origins for static resources (scripts, images, styles, fonts)."),
|
||||||
csp: z
|
/** @description Origins for nested iframes (frame-src directive). */
|
||||||
.object({
|
frameDomains: z
|
||||||
/** @description Origins for network requests (fetch/XHR/WebSocket). */
|
.array(z.string())
|
||||||
connectDomains: z
|
.optional()
|
||||||
.array(z.string())
|
.describe("Origins for nested iframes (frame-src directive)."),
|
||||||
.optional()
|
/** @description Allowed base URIs for the document (base-uri directive). */
|
||||||
.describe("Origins for network requests (fetch/XHR/WebSocket)."),
|
baseUriDomains: z
|
||||||
/** @description Origins for static resources (scripts, images, styles, fonts). */
|
.array(z.string())
|
||||||
resourceDomains: z
|
.optional()
|
||||||
.array(z.string())
|
.describe("Allowed base URIs for the document (base-uri directive)."),
|
||||||
.optional()
|
|
||||||
.describe(
|
|
||||||
"Origins for static resources (scripts, images, styles, fonts).",
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.describe("CSP configuration from resource metadata."),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -407,22 +397,6 @@ export const McpUiInitializedNotificationSchema = z.object({
|
||||||
params: z.object({}).optional(),
|
params: z.object({}).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Content Security Policy configuration for UI resources.
|
|
||||||
*/
|
|
||||||
export const McpUiResourceCspSchema = z.object({
|
|
||||||
/** @description Origins for network requests (fetch/XHR/WebSocket). */
|
|
||||||
connectDomains: z
|
|
||||||
.array(z.string())
|
|
||||||
.optional()
|
|
||||||
.describe("Origins for network requests (fetch/XHR/WebSocket)."),
|
|
||||||
/** @description Origins for static resources (scripts, images, styles, fonts). */
|
|
||||||
resourceDomains: z
|
|
||||||
.array(z.string())
|
|
||||||
.optional()
|
|
||||||
.describe("Origins for static resources (scripts, images, styles, fonts)."),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description UI Resource metadata for security and rendering configuration.
|
* @description UI Resource metadata for security and rendering configuration.
|
||||||
*/
|
*/
|
||||||
|
|
@ -521,6 +495,28 @@ export const McpUiMessageRequestSchema = z.object({
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Notification containing HTML resource for the sandbox proxy to load.
|
||||||
|
* @internal
|
||||||
|
* @see https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx#sandbox-proxy
|
||||||
|
*/
|
||||||
|
export const McpUiSandboxResourceReadyNotificationSchema = z.object({
|
||||||
|
method: z.literal("ui/notifications/sandbox-resource-ready"),
|
||||||
|
params: z.object({
|
||||||
|
/** @description HTML content to load into the inner iframe. */
|
||||||
|
html: z.string().describe("HTML content to load into the inner iframe."),
|
||||||
|
/** @description Optional override for the inner iframe's sandbox attribute. */
|
||||||
|
sandbox: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Optional override for the inner iframe's sandbox attribute."),
|
||||||
|
/** @description CSP configuration from resource metadata. */
|
||||||
|
csp: McpUiResourceCspSchema.optional().describe(
|
||||||
|
"CSP configuration from resource metadata.",
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Notification containing tool execution result (Host -> Guest UI).
|
* @description Notification containing tool execution result (Host -> Guest UI).
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -219,12 +219,7 @@ export interface McpUiSandboxResourceReadyNotification {
|
||||||
/** @description Optional override for the inner iframe's sandbox attribute. */
|
/** @description Optional override for the inner iframe's sandbox attribute. */
|
||||||
sandbox?: string;
|
sandbox?: string;
|
||||||
/** @description CSP configuration from resource metadata. */
|
/** @description CSP configuration from resource metadata. */
|
||||||
csp?: {
|
csp?: McpUiResourceCsp;
|
||||||
/** @description Origins for network requests (fetch/XHR/WebSocket). */
|
|
||||||
connectDomains?: string[];
|
|
||||||
/** @description Origins for static resources (scripts, images, styles, fonts). */
|
|
||||||
resourceDomains?: string[];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -497,6 +492,10 @@ export interface McpUiResourceCsp {
|
||||||
connectDomains?: string[];
|
connectDomains?: string[];
|
||||||
/** @description Origins for static resources (scripts, images, styles, fonts). */
|
/** @description Origins for static resources (scripts, images, styles, fonts). */
|
||||||
resourceDomains?: string[];
|
resourceDomains?: string[];
|
||||||
|
/** @description Origins for nested iframes (frame-src directive). */
|
||||||
|
frameDomains?: string[];
|
||||||
|
/** @description Allowed base URIs for the document (base-uri directive). */
|
||||||
|
baseUriDomains?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue