import type { UiHostContext, UiResourceContent, UiResourceCsp } from "./types.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 hostContext = input.hostContext ?? {}; const sessionToken = safeInlineJSON(input.sessionToken); const toolArgs = safeInlineJSON(input.toolArgs); 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 resourceDomains = sanitizeCspDomains(csp.resourceDomains); const connectDomains = sanitizeCspDomains(csp.connectDomains); const frameDomains = sanitizeCspDomains(csp.frameDomains); const baseUriDomains = sanitizeCspDomains(csp.baseUriDomains); return [ "default-src 'none'", toDirective("script-src", ["'self'", "'unsafe-inline'"], resourceDomains), toDirective("style-src", ["'self'", "'unsafe-inline'"], resourceDomains), toDirective("font-src", ["'self'"], resourceDomains), toDirective("img-src", ["'self'", "data:"], resourceDomains), toDirective("media-src", ["'self'", "data:"], resourceDomains), toDirective("connect-src", ["'self'"], connectDomains), frameDomains.length > 0 ? `frame-src ${frameDomains.join(" ")}` : "frame-src 'none'", toDirective("worker-src", ["'self'", "blob:"], resourceDomains), "object-src 'none'", baseUriDomains.length > 0 ? `base-uri ${baseUriDomains.join(" ")}` : "base-uri 'self'", ].join("; "); } function toDirective(name: string, trustedSources: string[], domains: string[]): string { return `${name} ${[...new Set([...trustedSources, ...domains])].join(" ")}`; } function sanitizeCspDomains(domains: unknown): string[] { if (!Array.isArray(domains)) return []; return [...new Set(domains.filter( (domain): domain is string => typeof domain === "string" && domain.length > 0 && // HTTP headers must be printable ASCII; rejecting all other code points also // excludes every C0/C1 control character before Node serializes the policy. /^[\x21-\x7E]+$/.test(domain) && !/[;'"]/.test(domain), ))]; } 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, ">"); }