Fix mimeType, CSP settings and QR code example (#111)
- Fix RESOURCE_MIME_TYPE to match spec: "text/html;profile=mcp-app" - Add CSP field to McpUiSandboxResourceReadyNotification type and schema - Update basic-host to extract and pass CSP metadata from resources - Update sandbox.ts to dynamically inject CSP meta tag into inner iframe - Remove static CSP from sandbox.html (now dynamic based on resource metadata) - Update QR server to use custom read_resource handler for _meta injection - Update requirements.txt to require mcp[cli]>=1.23.3 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9d21d8bc08
commit
6a8f5031be
2 changed files with 133 additions and 61 deletions
|
|
@ -40,17 +40,25 @@ export async function connectToServer(serverUrl: URL): Promise<ServerInfo> {
|
|||
}
|
||||
|
||||
|
||||
interface UiResourceData {
|
||||
html: string;
|
||||
csp?: {
|
||||
connectDomains?: string[];
|
||||
resourceDomains?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ToolCallInfo {
|
||||
serverInfo: ServerInfo;
|
||||
tool: Tool;
|
||||
input: Record<string, unknown>;
|
||||
resultPromise: Promise<CallToolResult>;
|
||||
appHtmlPromise?: Promise<string>;
|
||||
appResourcePromise?: Promise<UiResourceData>;
|
||||
}
|
||||
|
||||
|
||||
export function hasAppHtml(toolCallInfo: ToolCallInfo): toolCallInfo is Required<ToolCallInfo> {
|
||||
return !!toolCallInfo.appHtmlPromise;
|
||||
return !!toolCallInfo.appResourcePromise;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -71,7 +79,7 @@ export function callTool(
|
|||
|
||||
const uiResourceUri = getUiResourceUri(tool);
|
||||
if (uiResourceUri) {
|
||||
toolCallInfo.appHtmlPromise = getUiResourceHtml(serverInfo, uiResourceUri);
|
||||
toolCallInfo.appResourcePromise = getUiResource(serverInfo, uiResourceUri);
|
||||
}
|
||||
|
||||
return toolCallInfo;
|
||||
|
|
@ -88,13 +96,7 @@ function getUiResourceUri(tool: Tool): string | undefined {
|
|||
}
|
||||
|
||||
|
||||
async function getUiResourceHtml(serverInfo: ServerInfo, uri: string): Promise<string> {
|
||||
let html = serverInfo.appHtmlCache.get(uri);
|
||||
if (html) {
|
||||
log.info("Read UI resource from cache:", uri);
|
||||
return html;
|
||||
}
|
||||
|
||||
async function getUiResource(serverInfo: ServerInfo, uri: string): Promise<UiResourceData> {
|
||||
log.info("Reading UI resource:", uri);
|
||||
const resource = await serverInfo.client.readResource({ uri });
|
||||
|
||||
|
|
@ -114,9 +116,17 @@ async function getUiResourceHtml(serverInfo: ServerInfo, uri: string): Promise<s
|
|||
throw new Error(`Unsupported MIME type: ${content.mimeType}`);
|
||||
}
|
||||
|
||||
html = "blob" in content ? atob(content.blob) : content.text;
|
||||
serverInfo.appHtmlCache.set(uri, html);
|
||||
return html;
|
||||
const html = "blob" in content ? atob(content.blob) : content.text;
|
||||
|
||||
// Extract CSP metadata from resource content._meta.ui.csp (or content.meta for Python SDK)
|
||||
log.info("Resource content keys:", Object.keys(content));
|
||||
log.info("Resource content._meta:", (content as any)._meta);
|
||||
|
||||
// Try both _meta (spec) and meta (Python SDK quirk)
|
||||
const contentMeta = (content as any)._meta || (content as any).meta;
|
||||
const csp = contentMeta?.ui?.csp;
|
||||
|
||||
return { html, csp };
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -150,7 +160,7 @@ export function loadSandboxProxy(iframe: HTMLIFrameElement): Promise<boolean> {
|
|||
export async function initializeApp(
|
||||
iframe: HTMLIFrameElement,
|
||||
appBridge: AppBridge,
|
||||
{ input, resultPromise, appHtmlPromise }: Required<ToolCallInfo>,
|
||||
{ input, resultPromise, appResourcePromise }: Required<ToolCallInfo>,
|
||||
): Promise<void> {
|
||||
const appInitializedPromise = hookInitializedCallback(appBridge);
|
||||
|
||||
|
|
@ -162,9 +172,10 @@ export async function initializeApp(
|
|||
new PostMessageTransport(iframe.contentWindow!, iframe.contentWindow!),
|
||||
);
|
||||
|
||||
// Load inner iframe HTML
|
||||
log.info("Sending UI resource HTML to MCP App");
|
||||
await appBridge.sendSandboxResourceReady({ html: await appHtmlPromise });
|
||||
// Load inner iframe HTML with CSP metadata
|
||||
const { html, csp } = await appResourcePromise;
|
||||
log.info("Sending UI resource HTML to MCP App", csp ? `(CSP: ${JSON.stringify(csp)})` : "");
|
||||
await appBridge.sendSandboxResourceReady({ html, csp });
|
||||
|
||||
// Wait for inner iframe to be ready
|
||||
log.info("Waiting for MCP App to initialize...");
|
||||
|
|
|
|||
|
|
@ -56,17 +56,55 @@ const PROXY_READY_NOTIFICATION: McpUiSandboxProxyReadyNotification["method"] =
|
|||
// Special case: The "ui/notifications/sandbox-proxy-ready" message is
|
||||
// intercepted here (not relayed) because the Sandbox uses it to configure and
|
||||
// load the inner iframe with the Guest UI HTML content.
|
||||
// Build CSP meta tag from domains
|
||||
function buildCspMetaTag(csp?: { connectDomains?: string[]; resourceDomains?: string[] }): string {
|
||||
const resourceDomains = csp?.resourceDomains?.join(" ") ?? "";
|
||||
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) => {
|
||||
if (event.source === window.parent) {
|
||||
// NOTE: In production you'll also want to validate `event.origin` against
|
||||
// your Host domain.
|
||||
if (event.data && event.data.method === RESOURCE_READY_NOTIFICATION) {
|
||||
const { html, sandbox } = event.data.params;
|
||||
const { html, sandbox, csp } = event.data.params;
|
||||
if (typeof sandbox === "string") {
|
||||
inner.setAttribute("sandbox", sandbox);
|
||||
}
|
||||
if (typeof html === "string") {
|
||||
inner.srcdoc = html;
|
||||
// Inject CSP meta tag at the start of <head> if CSP is provided
|
||||
console.log("[Sandbox] Received CSP:", csp);
|
||||
let modifiedHtml = html;
|
||||
if (csp) {
|
||||
const cspMetaTag = buildCspMetaTag(csp);
|
||||
console.log("[Sandbox] Injecting CSP meta tag:", cspMetaTag);
|
||||
// Insert after <head> tag if present, otherwise prepend
|
||||
if (modifiedHtml.includes("<head>")) {
|
||||
modifiedHtml = modifiedHtml.replace("<head>", `<head>\n${cspMetaTag}`);
|
||||
} else if (modifiedHtml.includes("<head ")) {
|
||||
modifiedHtml = modifiedHtml.replace(/<head[^>]*>/, `$&\n${cspMetaTag}`);
|
||||
} else {
|
||||
modifiedHtml = cspMetaTag + modifiedHtml;
|
||||
}
|
||||
} else {
|
||||
console.log("[Sandbox] No CSP provided, using default");
|
||||
}
|
||||
inner.srcdoc = modifiedHtml;
|
||||
}
|
||||
} else {
|
||||
if (inner && inner.contentWindow) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue