{"version":3,"sources":["../src/preview/server.ts"],"names":["spawn","resolve","existsSync","join","createServer"],"mappings":"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","file":"server-BR5MNFOB.cjs","sourcesContent":["/**\n * Synapse Preview — standalone dev harness for MCP apps with UIs.\n *\n * Starts the MCP server (HTTP mode) and a minimal bridge host page\n * that iframes the app UI and proxies tool calls to the server.\n *\n * Usage:\n *   npx synapse preview --server \"uv run uvicorn mcp_hello.server:app --port 8001\" --ui ./ui\n *   npx synapse preview --server \"node dist/index.js\" --ui ./ui --server-port 8001\n */\n\nimport { type ChildProcess, spawn } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { createServer, type IncomingMessage, type ServerResponse } from \"node:http\";\nimport { join, resolve } from \"node:path\";\n\nexport interface PreviewOptions {\n  /** Shell command to start the MCP server in HTTP mode */\n  serverCmd: string;\n  /** Port the MCP server listens on (default: 8001) */\n  serverPort: number;\n  /** Path to the UI directory (must have package.json with dev script) */\n  uiDir: string;\n  /** Port for the UI Vite dev server (default: 5173) */\n  uiPort: number;\n  /** Port for the preview harness (default: 5180) */\n  previewPort: number;\n}\n\n/**\n * The preview harness page: the bridge host that frames the app UI on\n * `uiPort` and proxies its `tools/call` to the MCP server on `serverPort`.\n *\n * Exported so the conformance suite can serve this exact page and connect a\n * real spec client to it. Asserting the handshake against a copy would prove\n * nothing about what the preview actually answers.\n */\nexport const previewHostHtml = (uiPort: number, serverPort: number) => `<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\" />\n  <title>Synapse Preview</title>\n  <style>\n    * { margin: 0; padding: 0; box-sizing: border-box; }\n    body { font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; background: #0f172a; color: #e2e8f0; }\n    header { padding: 12px 20px; background: #1e293b; border-bottom: 1px solid #334155; display: flex; align-items: center; gap: 12px; }\n    header h1 { font-size: 14px; font-weight: 500; }\n    header .dot { width: 8px; height: 8px; border-radius: 50%; background: #22c55e; }\n    header .info { font-size: 12px; color: #94a3b8; margin-left: auto; }\n    .theme-toggle { background: #334155; border: none; color: #e2e8f0; padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 12px; }\n    iframe { width: 100%; height: calc(100vh - 45px); border: none; }\n  </style>\n</head>\n<body>\n  <header>\n    <span class=\"dot\"></span>\n    <h1>Synapse Preview</h1>\n    <button class=\"theme-toggle\" id=\"toggle\">Toggle Dark/Light</button>\n    <span class=\"info\">MCP: localhost:${serverPort} | UI: localhost:${uiPort}</span>\n  </header>\n  <iframe id=\"app\" src=\"http://localhost:${uiPort}\"></iframe>\n\n  <script>\n    var iframe = document.getElementById(\"app\");\n    var darkMode = true;\n\n    // Minimal NimbleBrain bridge host — just enough to make Synapse work.\n    //\n    // Every key here is one the spec's style-variable enum names. That enum is\n    // a strict record, so a single extra key makes a spec client reject the\n    // whole \\`ui/initialize\\` result and refuse to connect — not just ignore the\n    // key. The kit's own neutral defaults back anything a host leaves out, so\n    // the accent and danger colours still render; they come from the cascade\n    // layer instead of the wire.\n    var tokens = darkMode ? {\n      \"--color-background-primary\": \"#0f172a\", \"--color-background-secondary\": \"#1e293b\",\n      \"--color-background-tertiary\": \"#2a374a\", \"--color-text-primary\": \"#e2e8f0\",\n      \"--color-text-secondary\": \"#94a3b8\", \"--color-text-tertiary\": \"#64748b\",\n      \"--color-border-primary\": \"#334155\", \"--color-border-secondary\": \"#475569\",\n      \"--color-ring-primary\": \"#6366f1\",\n      \"--border-radius-sm\": \"0.5rem\",\n    } : {\n      \"--color-background-primary\": \"#ffffff\", \"--color-background-secondary\": \"#f9fafb\",\n      \"--color-background-tertiary\": \"#f1f5f9\", \"--color-text-primary\": \"#1a1a1a\",\n      \"--color-text-secondary\": \"#6b7280\", \"--color-text-tertiary\": \"#94a3b8\",\n      \"--color-border-primary\": \"#e5e7eb\", \"--color-border-secondary\": \"#cbd5e1\",\n      \"--color-ring-primary\": \"#6366f1\",\n      \"--border-radius-sm\": \"0.5rem\",\n    };\n\n    function post(msg) { iframe.contentWindow.postMessage(msg, \"*\"); }\n\n    // A request is a frame carrying an id — and \\`0\\` is a perfectly good id.\n    // The MCP SDK numbers from zero, so a truthiness test drops the first\n    // request every spec client ever sends, which is its \\`ui/initialize\\`.\n    function isRequest(msg) { return msg.id !== undefined && msg.id !== null; }\n\n    window.addEventListener(\"message\", async function (event) {\n      if (event.source !== iframe.contentWindow) return;\n      var msg = event.data;\n      if (!msg || typeof msg !== \"object\") return;\n\n      // ext-apps handshake. \\`hostInfo\\`/\\`hostCapabilities\\` are the spec's field\n      // names, and a spec client validates the result against them — the\n      // official ext-apps \\`App\\` refuses to connect to anything else, so an app\n      // built on it would render only in the real host and not in preview.\n      if (msg.method === \"ui/initialize\" && isRequest(msg)) {\n        post({\n          jsonrpc: \"2.0\", id: msg.id,\n          result: {\n            protocolVersion: \"2026-01-26\",\n            hostInfo: { name: \"nimblebrain\", version: \"preview\" },\n            hostCapabilities: {\n              openLinks: {},\n              serverTools: {},\n              updateModelContext: { text: {}, structuredContent: {} },\n              experimental: { \"ai.nimblebrain/action\": {}, \"ai.nimblebrain/keydown\": {} }\n            },\n            hostContext: { theme: darkMode ? \"dark\" : \"light\", styles: { variables: tokens } }\n          }\n        });\n        return;\n      }\n\n      if (msg.method === \"ui/notifications/initialized\") return;\n\n      // Tool call proxy — forward to the MCP server\n      if (msg.method === \"tools/call\" && isRequest(msg)) {\n        try {\n          var resp = await fetch(\"http://localhost:${serverPort}/mcp\", {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application/json\" },\n            body: JSON.stringify({\n              jsonrpc: \"2.0\", id: msg.id,\n              method: \"tools/call\",\n              params: { name: msg.params.name, arguments: msg.params.arguments || {} }\n            })\n          });\n          var result = await resp.json();\n          // Forward the JSON-RPC response back to the iframe\n          post(result);\n        } catch (err) {\n          post({ jsonrpc: \"2.0\", id: msg.id, error: { code: -32000, message: err.message || \"Server error\" } });\n        }\n        return;\n      }\n\n      // ai.nimblebrain/action — log to console\n      if (msg.method === \"ai.nimblebrain/action\") {\n        console.log(\"[action]\", msg.params?.action, msg.params);\n        return;\n      }\n\n      // ai.nimblebrain/keydown — ignore in preview\n      if (msg.method === \"ai.nimblebrain/keydown\") return;\n\n      // ui/update-model-context — log (ext-apps spec)\n      if (msg.method === \"ui/update-model-context\") {\n        console.log(\"[model-context]\", msg.params?.structuredContent);\n        if (isRequest(msg)) post({ jsonrpc: \"2.0\", id: msg.id, result: {} });\n        return;\n      }\n\n      console.log(\"[bridge] unhandled:\", msg.method, msg);\n    });\n\n    // Theme toggle\n    document.getElementById(\"toggle\").addEventListener(\"click\", function () {\n      darkMode = !darkMode;\n      document.body.style.background = darkMode ? \"#0f172a\" : \"#f1f5f9\";\n      tokens = darkMode ? {\n        \"--color-background-primary\": \"#0f172a\", \"--color-background-secondary\": \"#1e293b\",\n        \"--color-background-tertiary\": \"#2a374a\", \"--color-text-primary\": \"#e2e8f0\",\n        \"--color-text-secondary\": \"#94a3b8\", \"--color-text-tertiary\": \"#64748b\",\n        \"--color-border-primary\": \"#334155\", \"--color-border-secondary\": \"#475569\",\n        \"--color-ring-primary\": \"#6366f1\",\n        \"--border-radius-sm\": \"0.5rem\",\n      } : {\n        \"--color-background-primary\": \"#ffffff\", \"--color-background-secondary\": \"#f9fafb\",\n        \"--color-background-tertiary\": \"#f1f5f9\", \"--color-text-primary\": \"#1a1a1a\",\n        \"--color-text-secondary\": \"#6b7280\", \"--color-text-tertiary\": \"#94a3b8\",\n        \"--color-border-primary\": \"#e5e7eb\", \"--color-border-secondary\": \"#cbd5e1\",\n        \"--color-ring-primary\": \"#6366f1\",\n        \"--border-radius-sm\": \"0.5rem\",\n      };\n      post({ jsonrpc: \"2.0\", method: \"ui/notifications/host-context-changed\", params: { theme: darkMode ? \"dark\" : \"light\", styles: { variables: tokens } } });\n    });\n  </script>\n</body>\n</html>`;\n\nexport async function startPreview(options: PreviewOptions): Promise<void> {\n  const { serverCmd, serverPort, uiDir, uiPort, previewPort } = options;\n  const children: ChildProcess[] = [];\n\n  console.log(`\\n  Synapse Preview\\n`);\n\n  // 1. Start MCP server\n  console.log(`  [server] ${serverCmd}`);\n  const serverProc = spawn(serverCmd, {\n    shell: true,\n    stdio: [\"ignore\", \"pipe\", \"pipe\"],\n  });\n  children.push(serverProc);\n  serverProc.stdout?.on(\"data\", (d: Buffer) => process.stdout.write(`  [server] ${d}`));\n  serverProc.stderr?.on(\"data\", (d: Buffer) => process.stderr.write(`  [server] ${d}`));\n\n  // 2. Start UI Vite dev server\n  const resolvedUi = resolve(uiDir);\n  if (!existsSync(join(resolvedUi, \"package.json\"))) {\n    console.error(`  [ui] Error: no package.json found at ${resolvedUi}`);\n    process.exit(1);\n  }\n  console.log(`  [ui] cd ${resolvedUi} && npx vite --port ${uiPort}`);\n  const uiProc = spawn(\"npx\", [\"vite\", \"--port\", String(uiPort)], {\n    cwd: resolvedUi,\n    stdio: [\"ignore\", \"pipe\", \"pipe\"],\n  });\n  children.push(uiProc);\n  uiProc.stdout?.on(\"data\", (d: Buffer) => process.stdout.write(`  [ui] ${d}`));\n  uiProc.stderr?.on(\"data\", (d: Buffer) => process.stderr.write(`  [ui] ${d}`));\n\n  // 3. Start preview host\n  const html = previewHostHtml(uiPort, serverPort);\n  const host = createServer((_req: IncomingMessage, res: ServerResponse) => {\n    res.writeHead(200, { \"Content-Type\": \"text/html\" });\n    res.end(html);\n  });\n  host.listen(previewPort, () => {\n    console.log(`\\n  Preview:  http://localhost:${previewPort}`);\n    console.log(`  UI:       http://localhost:${uiPort}`);\n    console.log(`  Server:   http://localhost:${serverPort}`);\n    console.log(`\\n  Press Ctrl+C to stop.\\n`);\n  });\n\n  // Shutdown\n  const shutdown = () => {\n    console.log(\"\\n  Shutting down...\");\n    host.close();\n    for (const child of children) {\n      try {\n        child.kill(\"SIGTERM\");\n      } catch {\n        /* already dead */\n      }\n    }\n    process.exit(0);\n  };\n  process.on(\"SIGINT\", shutdown);\n  process.on(\"SIGTERM\", shutdown);\n}\n"]}