{"version":3,"sources":["../../src/vite/plugin.ts"],"names":["TOOLS_CALL_METHOD","READ_RESOURCE_METHOD","LIST_RESOURCES_METHOD","RESOURCE_LIST_CHANGED_METHOD","resolve","existsSync","readFileSync","join","spawn"],"mappings":"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","file":"index.cjs","sourcesContent":["import { type ChildProcess, spawn } from \"node:child_process\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport type { ServerResponse } from \"node:http\";\nimport { join, resolve } from \"node:path\";\nimport type { Plugin, ViteDevServer } from \"vite\";\nimport {\n  LIST_RESOURCES_METHOD,\n  READ_RESOURCE_METHOD,\n  RESOURCE_LIST_CHANGED_METHOD,\n  TOOLS_CALL_METHOD,\n} from \"../event-map.js\";\n\n/**\n * The server-bound requests this host proxies, which is exactly what its\n * handshake announces: `serverTools` for the tool call, `serverResources` for\n * the two resource methods. An allowlist in both directions — the page sends\n * nothing else to `/__mcp`, and `/__mcp` forwards nothing else to the server,\n * so a method the host never announced cannot reach the server through it.\n *\n * Adding a method here without adding the matching capability below, or the\n * reverse, is the bug this list exists to make obvious: a host that answers\n * what it does not announce is unusable, and one that announces what it does\n * not answer leaves the caller waiting on a reply nobody will send.\n */\nconst PROXIED_SERVER_METHODS: readonly string[] = [\n  TOOLS_CALL_METHOD,\n  READ_RESOURCE_METHOD,\n  LIST_RESOURCES_METHOD,\n];\n\nexport interface SynapseVitePluginOptions {\n  /** App name. If omitted, reads from ../manifest.json */\n  appName?: string;\n  /** Path to manifest.json. Default: ../manifest.json (relative to ui/) */\n  manifest?: string;\n  /**\n   * Shell command to start the MCP server. If omitted, derived from manifest.\n   * The server runs in stdio mode — stdin/stdout JSON-RPC.\n   */\n  serverCmd?: string;\n  /** Set to false to disable the preview host page at /__preview */\n  preview?: boolean;\n}\n\ninterface Manifest {\n  name: string;\n  version?: string;\n  server?: {\n    type?: string;\n    entry_point?: string;\n    mcp_config?: {\n      command?: string;\n      args?: string[];\n    };\n  };\n}\n\n/**\n * Synapse Vite plugin — full local dev experience for MCP apps.\n *\n * What it does:\n * - Reads ../manifest.json to get app name and server config\n * - Spawns the MCP server as a child process (stdio mode)\n * - Serves a preview host page at /__preview that iframes your app\n * - Proxies the iframe's tool calls and resource reads through POST /__mcp to\n *   the server, the pair its handshake announces as `serverTools` and\n *   `serverResources`\n * - Forwards the server's `notifications/resources/list_changed` to the iframe\n *   (over GET /__events), as an MCP Apps host does, so `useDataSync` fires\n * - Handles the ext-apps handshake so Synapse hooks work\n * - HMR works inside the iframe — edit .tsx, see changes instantly\n *\n * Usage in vite.config.ts:\n *   import { synapseVite } from \"@nimblebrain/synapse/vite\";\n *   export default { plugins: [react(), viteSingleFile(), synapseVite()] };\n *\n * Then: cd ui && npm run dev && open http://localhost:5173/__preview\n */\nexport function synapseVite(options: SynapseVitePluginOptions = {}): Plugin {\n  const enablePreview = options.preview !== false;\n  let manifest: Manifest | null = null;\n  let appName = options.appName ?? \"app\";\n  let serverProcess: ChildProcess | null = null;\n  const pendingRequests = new Map<\n    string,\n    { resolve: (v: unknown) => void; reject: (e: Error) => void }\n  >();\n  // Preview pages listening on GET /__events for the server's notifications.\n  const eventClients = new Set<ServerResponse>();\n\n  /**\n   * Hand a server notification to every open preview page, which posts it into\n   * the app iframe. Only the one an MCP Apps host forwards to a view is passed\n   * on; anything else the server says stays between it and the dev server.\n   */\n  function forwardNotification(msg: { method?: unknown; params?: unknown }): void {\n    if (msg.method !== RESOURCE_LIST_CHANGED_METHOD) return;\n    const frame = `data: ${JSON.stringify({ method: msg.method, params: msg.params ?? {} })}\\n\\n`;\n    for (const client of eventClients) client.write(frame);\n  }\n  let serverBuffer = \"\";\n\n  function loadManifest(root: string): Manifest | null {\n    const manifestPath = options.manifest\n      ? resolve(options.manifest)\n      : resolve(root, \"..\", \"manifest.json\");\n\n    if (!existsSync(manifestPath)) return null;\n    try {\n      return JSON.parse(readFileSync(manifestPath, \"utf-8\"));\n    } catch {\n      return null;\n    }\n  }\n\n  function deriveServerCmd(m: Manifest, root: string): string | null {\n    if (options.serverCmd) return options.serverCmd;\n    const cfg = m.server?.mcp_config;\n    if (!cfg?.command) return null;\n\n    const serverDir = resolve(root, \"..\");\n    let cmd = cfg.command;\n    const args = cfg.args ?? [];\n\n    // Python projects: use `uv run` if pyproject.toml exists\n    if (cmd === \"python\" && existsSync(join(serverDir, \"pyproject.toml\"))) {\n      cmd = \"uv run python\";\n    }\n\n    return `cd ${JSON.stringify(serverDir)} && ${cmd} ${args.join(\" \")}`;\n  }\n\n  function startServer(cmd: string): void {\n    serverProcess = spawn(cmd, {\n      shell: true,\n      stdio: [\"pipe\", \"pipe\", \"pipe\"],\n    });\n\n    serverProcess.stderr?.on(\"data\", (d: Buffer) => {\n      process.stderr.write(`  [mcp] ${d}`);\n    });\n\n    serverProcess.stdout?.on(\"data\", (d: Buffer) => {\n      serverBuffer += d.toString();\n      // Parse line-delimited JSON-RPC responses\n      const lines = serverBuffer.split(\"\\n\");\n      serverBuffer = lines.pop() ?? \"\"; // keep incomplete line\n      for (const line of lines) {\n        if (!line.trim()) continue;\n        try {\n          const msg = JSON.parse(line);\n          if (msg.id && pendingRequests.has(msg.id)) {\n            const p = pendingRequests.get(msg.id);\n            pendingRequests.delete(msg.id);\n            p?.resolve(msg);\n          } else if (msg.id === undefined && typeof msg.method === \"string\") {\n            forwardNotification(msg);\n          }\n        } catch {\n          // Not JSON — log it\n          process.stderr.write(`  [mcp] ${line}\\n`);\n        }\n      }\n    });\n\n    serverProcess.on(\"exit\", (code) => {\n      if (code !== null && code !== 0) {\n        console.error(`  [mcp] Server exited with code ${code}`);\n      }\n      serverProcess = null;\n    });\n\n    // Send initialize\n    sendToServer({\n      jsonrpc: \"2.0\",\n      id: \"init-1\",\n      method: \"initialize\",\n      params: {\n        protocolVersion: \"2024-11-05\",\n        capabilities: {},\n        clientInfo: { name: \"synapse-preview\", version: \"0.1.0\" },\n      },\n    });\n  }\n\n  function sendToServer(msg: Record<string, unknown>): void {\n    if (!serverProcess?.stdin?.writable) return;\n    serverProcess.stdin.write(`${JSON.stringify(msg)}\\n`);\n  }\n\n  function callServer(method: string, params: Record<string, unknown>): Promise<unknown> {\n    return new Promise((resolve, reject) => {\n      const id = `preview-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n      pendingRequests.set(id, { resolve, reject });\n      sendToServer({ jsonrpc: \"2.0\", id, method, params });\n      setTimeout(() => {\n        if (pendingRequests.has(id)) {\n          pendingRequests.delete(id);\n          reject(new Error(`${method} timed out (10s)`));\n        }\n      }, 10000);\n    });\n  }\n\n  return {\n    name: \"synapse\",\n\n    config() {\n      return {\n        define: {\n          \"import.meta.env.SYNAPSE_APP_NAME\": JSON.stringify(appName),\n        },\n        server: {\n          hmr: {\n            protocol: \"ws\",\n            host: \"localhost\",\n          },\n        },\n      };\n    },\n\n    configResolved(config) {\n      manifest = loadManifest(config.root);\n      if (manifest?.name) {\n        appName = options.appName ?? manifest.name;\n      }\n    },\n\n    configureServer(server: ViteDevServer) {\n      // Start MCP server\n      if (enablePreview && manifest) {\n        const cmd = deriveServerCmd(manifest, server.config.root);\n        if (cmd) {\n          console.log(`\\n  [synapse] Starting MCP server: ${cmd}\\n`);\n          startServer(cmd);\n        }\n      }\n\n      server.middlewares.use((req, res, next) => {\n        // CORS for iframe communication\n        res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n        res.setHeader(\"Access-Control-Allow-Methods\", \"*\");\n        res.setHeader(\"Access-Control-Allow-Headers\", \"*\");\n\n        // /__preview — bridge host page\n        if (req.url === \"/__preview\" || req.url === \"/__preview/\") {\n          res.writeHead(200, { \"Content-Type\": \"text/html\" });\n          res.end(vitePreviewHostHtml(appName));\n          return;\n        }\n\n        // GET /__events — the server's notifications, pushed to the preview page\n        if (req.method === \"GET\" && req.url === \"/__events\") {\n          res.writeHead(200, {\n            \"Content-Type\": \"text/event-stream\",\n            \"Cache-Control\": \"no-cache\",\n            Connection: \"keep-alive\",\n          });\n          res.write(\": open\\n\\n\");\n          eventClients.add(res);\n          req.on(\"close\", () => {\n            eventClients.delete(res);\n          });\n          return;\n        }\n\n        // POST /__mcp — the server-bound request proxy\n        if (req.method === \"POST\" && req.url === \"/__mcp\") {\n          let body = \"\";\n          req.on(\"data\", (chunk: Buffer) => {\n            body += chunk.toString();\n          });\n          req.on(\"end\", async () => {\n            try {\n              const msg = JSON.parse(body);\n              if (!PROXIED_SERVER_METHODS.includes(msg.method)) {\n                throw new Error(`${msg.method} is not a method this host proxies`);\n              }\n              const result = await callServer(msg.method, msg.params ?? {});\n              res.writeHead(200, { \"Content-Type\": \"application/json\" });\n              res.end(JSON.stringify(result));\n            } catch (err) {\n              res.writeHead(200, { \"Content-Type\": \"application/json\" });\n              res.end(\n                JSON.stringify({\n                  jsonrpc: \"2.0\",\n                  id: JSON.parse(body).id,\n                  error: { code: -32000, message: (err as Error).message },\n                }),\n              );\n            }\n          });\n          return;\n        }\n\n        next();\n      });\n    },\n\n    buildEnd() {\n      // Kill server on build end (for production builds)\n      if (serverProcess) {\n        serverProcess.kill(\"SIGTERM\");\n        serverProcess = null;\n      }\n    },\n  };\n}\n\n/**\n * The bridge host page the dev server serves at \\`/__preview\\`.\n *\n * Named apart from \\`previewHostHtml\\` in \\`src/preview/server.ts\\`, which is the\n * standalone \\`synapse preview\\` harness: this package has two hand-written\n * hosts, and the conformance suite drives both, so one name for two pages would\n * be a name that resolves to whichever was imported last.\n */\nexport function vitePreviewHostHtml(appName: string): string {\n  return `<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\" />\n  <title>${appName} — 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: 10px 16px; background: #1e293b; border-bottom: 1px solid #334155; display: flex; align-items: center; gap: 10px; font-size: 13px; }\n    header .dot { width: 8px; height: 8px; border-radius: 50%; background: #22c55e; }\n    header .name { font-weight: 600; }\n    header .spacer { flex: 1; }\n    header button { background: #334155; border: none; color: #e2e8f0; padding: 3px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; }\n    header .url { color: #64748b; font-size: 11px; font-family: monospace; }\n    iframe { width: 100%; height: calc(100vh - 41px); border: none; }\n  </style>\n</head>\n<body>\n  <header>\n    <span class=\"dot\"></span>\n    <span class=\"name\">${appName}</span>\n    <span class=\"spacer\"></span>\n    <button id=\"toggle\">Toggle Theme</button>\n    <span class=\"url\">Synapse Preview</span>\n  </header>\n  <iframe id=\"app\"></iframe>\n\n  <script>\n    var iframe = document.getElementById(\"app\");\n    var dark = true;\n    var PROXIED = ${JSON.stringify(PROXIED_SERVER_METHODS)};\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.\n    function getTokens(d) {\n      return d ? {\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\",\"--font-sans\":\"-apple-system,BlinkMacSystemFont,sans-serif\"\n      } : {\n        \"--color-background-primary\":\"#ffffff\",\"--color-background-secondary\":\"#f8fafc\",\n        \"--color-background-tertiary\":\"#f1f5f9\",\"--color-text-primary\":\"#0f172a\",\n        \"--color-text-secondary\":\"#64748b\",\"--color-text-tertiary\":\"#94a3b8\",\n        \"--color-border-primary\":\"#e2e8f0\",\"--color-border-secondary\":\"#cbd5e1\",\n        \"--color-ring-primary\":\"#6366f1\",\n        \"--border-radius-sm\":\"0.5rem\",\"--font-sans\":\"-apple-system,BlinkMacSystemFont,sans-serif\"\n      };\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(e) {\n      if (e.source !== iframe.contentWindow) return;\n      var msg = e.data;\n      if (!msg || typeof msg !== \"object\") return;\n\n      // ext-apps handshake\n      // \\`hostInfo\\`/\\`hostCapabilities\\` are the spec's field names, and a spec\n      // client validates the result against them — the official ext-apps\n      // \\`App\\` refuses to connect to anything else.\n      if (msg.method === \"ui/initialize\" && isRequest(msg)) {\n        post({ jsonrpc:\"2.0\", id:msg.id, result: {\n          protocolVersion:\"2026-01-26\",\n          hostInfo:{name:\"nimblebrain\",version:\"preview\"},\n          hostCapabilities:{openLinks:{},serverTools:{},serverResources:{listChanged:true},\n            updateModelContext:{text:{},structuredContent:{}},\n            experimental:{\"ai.nimblebrain/action\":{},\"ai.nimblebrain/keydown\":{}}},\n          hostContext:{theme:dark?\"dark\":\"light\",styles:{variables:getTokens(dark)}}\n        }});\n        return;\n      }\n      if (msg.method === \"ui/notifications/initialized\") return;\n\n      // Server-bound requests — proxied via the Vite middleware. The set is\n      // exactly what \\`serverTools\\` and \\`serverResources\\` above announce, so\n      // neither is declared without something on the other end.\n      if (PROXIED.indexOf(msg.method) !== -1 && isRequest(msg)) {\n        var originalId = msg.id;\n        try {\n          var r = await fetch(\"/__mcp\", {\n            method:\"POST\", headers:{\"Content-Type\":\"application/json\"},\n            body: JSON.stringify({jsonrpc:\"2.0\",id:msg.id,method:msg.method,params:msg.params||{}})\n          });\n          var response = await r.json();\n          response.id = originalId;\n          post(response);\n          // The page announces no data change of its own. A change is the\n          // server's to announce, and that reaches the app over /__events. One\n          // fired here on the app's own call would loop: the call fires it, the\n          // app re-fetches, and the re-fetch fires it again.\n        } catch(err) {\n          post({jsonrpc:\"2.0\",id:originalId,error:{code:-32000,message:err.message}});\n        }\n        return;\n      }\n\n      // Log other messages\n      if (msg.method === \"ai.nimblebrain/action\") console.log(\"[action]\", msg.params?.action, msg.params);\n      else if (msg.method === \"ui/update-model-context\") { console.log(\"[model-context]\", msg.params?.structuredContent); if (isRequest(msg)) post({jsonrpc:\"2.0\",id:msg.id,result:{}}); }\n      else if (msg.method === \"ai.nimblebrain/keydown\") { /* ignore */ }\n      else if (msg.method) console.log(\"[bridge]\", msg.method, msg);\n    });\n\n    document.getElementById(\"toggle\").onclick = function() {\n      dark = !dark;\n      document.body.style.background = dark ? \"#0f172a\" : \"#f1f5f9\";\n      post({jsonrpc:\"2.0\",method:\"ui/notifications/host-context-changed\",params:{theme:dark?\"dark\":\"light\",styles:{variables:getTokens(dark)}}});\n    };\n\n    // The server's own notifications, forwarded into the app as an MCP Apps\n    // host forwards them. The dev server passes on only the ones a host would.\n    new EventSource(\"/__events\").onmessage = function(e) {\n      var n = JSON.parse(e.data);\n      post({jsonrpc:\"2.0\",method:n.method,params:n.params});\n    };\n\n    // Load the iframe AFTER the message listener is attached to avoid\n    // a race where the app sends ui/initialize before the bridge is ready.\n    iframe.src = \"/\";\n  </script>\n</body>\n</html>`;\n}\n"]}