import type { UiHostContext, UiResourceContent, UiResourceCsp } from "./types.ts"; import { emit } from "../visual/event-sink.ts"; // Use locally bundled AppBridge to avoid CDN Zod bundling issues const DEFAULT_APP_BRIDGE_MODULE_URL = "/app-bridge.bundle.js"; export interface HostHtmlTemplateInput { sessionToken: string; serverName: string; toolName: string; toolArgs: Record; resource: UiResourceContent; allowAttribute: string; requireToolConsent: boolean; cacheToolConsent: boolean; hostContext?: UiHostContext; appBridgeModuleUrl?: string; } export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string { const cspContent = buildCspMetaContent(input.resource.meta.csp); const resourceHtml = applyCspMeta(input.resource.html, cspContent); const hostContext = input.hostContext ?? {}; const sessionToken = safeInlineJSON(input.sessionToken); const toolArgs = safeInlineJSON(input.toolArgs); const uiHtml = safeInlineJSON(resourceHtml); const serverName = safeInlineJSON(input.serverName); const toolName = safeInlineJSON(input.toolName); const hostContextJson = safeInlineJSON(hostContext); const allowAttribute = safeInlineJSON(input.allowAttribute); const requireToolConsent = safeInlineJSON(input.requireToolConsent); const cacheToolConsent = safeInlineJSON(input.cacheToolConsent); const moduleUrl = safeInlineJSON(input.appBridgeModuleUrl ?? DEFAULT_APP_BRIDGE_MODULE_URL); return ` MCP UI - ${escapeHtml(input.serverName)} / ${escapeHtml(input.toolName)}
MCP · Sandboxed
Loading UI...

UI Error

`; } export function buildCspMetaContent(csp: UiResourceCsp | undefined): string | undefined { if (!csp) return undefined; const directives: string[] = []; directives.push("default-src 'none'"); const scriptSrc = toDirective("script-src", csp.scriptDomains); const styleSrc = toDirective("style-src", csp.styleDomains); const fontSrc = toDirective("font-src", csp.fontDomains); const imgSrc = toDirective("img-src", csp.imgDomains); const mediaSrc = toDirective("media-src", csp.mediaDomains); const connectSrc = toDirective("connect-src", csp.connectDomains); const frameSrc = toDirective("frame-src", csp.frameDomains); const workerSrc = toDirective("worker-src", csp.workerDomains); const baseUri = toDirective("base-uri", csp.baseUriDomains); if (scriptSrc) directives.push(scriptSrc); if (styleSrc) directives.push(styleSrc); if (fontSrc) directives.push(fontSrc); if (imgSrc) directives.push(imgSrc); if (mediaSrc) directives.push(mediaSrc); if (connectSrc) directives.push(connectSrc); if (frameSrc) directives.push(frameSrc); if (workerSrc) directives.push(workerSrc); if (baseUri) directives.push(baseUri); return directives.join("; "); } function toDirective(name: string, domains: string[] | undefined): string | null { if (!domains || domains.length === 0) return null; return `${name} ${domains.join(" ")}`; } export function applyCspMeta(html: string, cspContent: string | undefined): string { if (!cspContent) return html; if (/http-equiv=["']Content-Security-Policy["']/i.test(html)) return html; const metaTag = ``; if (/]*>/i.test(html)) { return html.replace(/]*>/i, (match) => `${match}\n${metaTag}`); } return `${metaTag}\n${html}`; } function safeInlineJSON(value: unknown): string { return JSON.stringify(value) .replace(//g, "\\u003e") .replace(/&/g, "\\u0026") .replace(/\u2028/g, "\\u2028") .replace(/\u2029/g, "\\u2029"); } function escapeHtml(value: string): string { return value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function escapeHtmlAttribute(value: string): string { return value .replace(/&/g, "&") .replace(/"/g, """) .replace(//g, ">"); }