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) .replaceAll('<', '\\u003c') .replaceAll('>', '\\u003e') .replaceAll('&', '\\u0026') .replaceAll('\u2028', '\\u2028') .replaceAll('\u2029', '\\u2029'); } function escapeHtml(value: string): string { return value .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); }