@nimblebrain/synapse 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +32 -37
  2. package/dist/check-JEM5PN64.cjs +435 -0
  3. package/dist/check-JEM5PN64.cjs.map +1 -0
  4. package/dist/check-YDZUQT6A.js +429 -0
  5. package/dist/check-YDZUQT6A.js.map +1 -0
  6. package/dist/{chunk-VDQZQDU3.cjs → chunk-BNF352SA.cjs} +56 -145
  7. package/dist/chunk-BNF352SA.cjs.map +1 -0
  8. package/dist/{chunk-HKRTDGXN.js → chunk-DJS2FKBN.js} +21 -89
  9. package/dist/chunk-DJS2FKBN.js.map +1 -0
  10. package/dist/chunk-H3INUKA3.js +32 -0
  11. package/dist/chunk-H3INUKA3.js.map +1 -0
  12. package/dist/{chunk-FB7GPBEM.cjs → chunk-HQLSVUOR.cjs} +193 -314
  13. package/dist/chunk-HQLSVUOR.cjs.map +1 -0
  14. package/dist/chunk-ISV6HFQE.cjs +40 -0
  15. package/dist/chunk-ISV6HFQE.cjs.map +1 -0
  16. package/dist/{chunk-3YSXPBEQ.js → chunk-TR2YNWZ5.js} +190 -313
  17. package/dist/chunk-TR2YNWZ5.js.map +1 -0
  18. package/dist/{chunk-ZLASWV4N.js → chunk-UKP4W6DI.js} +56 -145
  19. package/dist/chunk-UKP4W6DI.js.map +1 -0
  20. package/dist/{chunk-LPWEQCZV.cjs → chunk-YSL7KJNU.cjs} +21 -92
  21. package/dist/chunk-YSL7KJNU.cjs.map +1 -0
  22. package/dist/codegen/cli.cjs +38 -1
  23. package/dist/codegen/cli.cjs.map +1 -1
  24. package/dist/codegen/cli.js +38 -1
  25. package/dist/codegen/cli.js.map +1 -1
  26. package/dist/codegen/index.d.cts +1 -1
  27. package/dist/codegen/index.d.ts +1 -1
  28. package/dist/connect.iife.global.js +66 -43
  29. package/dist/{detect-BHYg26_d.d.cts → detect-DVGaL2bH.d.cts} +39 -33
  30. package/dist/{detect-Bf8Q_0dK.d.ts → detect-DVGaL2bH.d.ts} +39 -33
  31. package/dist/host/index.cjs +17 -17
  32. package/dist/host/index.d.cts +11 -27
  33. package/dist/host/index.d.ts +11 -27
  34. package/dist/host/index.js +2 -2
  35. package/dist/index.cjs +52 -17
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.d.cts +104 -31
  38. package/dist/index.d.ts +104 -31
  39. package/dist/index.js +34 -3
  40. package/dist/index.js.map +1 -1
  41. package/dist/react/index.cjs +11 -10
  42. package/dist/react/index.cjs.map +1 -1
  43. package/dist/react/index.d.cts +49 -12
  44. package/dist/react/index.d.ts +49 -12
  45. package/dist/react/index.js +6 -5
  46. package/dist/react/index.js.map +1 -1
  47. package/dist/{server-QIYIJHD5.cjs → server-BR5MNFOB.cjs} +40 -29
  48. package/dist/server-BR5MNFOB.cjs.map +1 -0
  49. package/dist/{server-5N76YCWC.js → server-OOEEWAEG.js} +40 -30
  50. package/dist/server-OOEEWAEG.js.map +1 -0
  51. package/dist/synapse-runtime.iife.global.js +66 -43
  52. package/dist/synapse-ui.iife.global.js +4 -4
  53. package/dist/{types-uEO4VFJ2.d.cts → types-BxPfGHKO.d.cts} +48 -125
  54. package/dist/{types-uEO4VFJ2.d.ts → types-BxPfGHKO.d.ts} +48 -125
  55. package/dist/vite/index.cjs +77 -33
  56. package/dist/vite/index.cjs.map +1 -1
  57. package/dist/vite/index.d.cts +5 -1
  58. package/dist/vite/index.d.ts +5 -1
  59. package/dist/vite/index.js +77 -33
  60. package/dist/vite/index.js.map +1 -1
  61. package/package.json +12 -6
  62. package/dist/chunk-3YSXPBEQ.js.map +0 -1
  63. package/dist/chunk-FB7GPBEM.cjs.map +0 -1
  64. package/dist/chunk-HKRTDGXN.js.map +0 -1
  65. package/dist/chunk-LPWEQCZV.cjs.map +0 -1
  66. package/dist/chunk-VDQZQDU3.cjs.map +0 -1
  67. package/dist/chunk-ZLASWV4N.js.map +0 -1
  68. package/dist/server-5N76YCWC.js.map +0 -1
  69. package/dist/server-QIYIJHD5.cjs.map +0 -1
@@ -1,16 +1,29 @@
1
1
  'use strict';
2
2
 
3
+ var chunkISV6HFQE_cjs = require('../chunk-ISV6HFQE.cjs');
3
4
  var child_process = require('child_process');
4
5
  var fs = require('fs');
5
6
  var path = require('path');
6
7
 
7
- // src/vite/plugin.ts
8
+ var PROXIED_SERVER_METHODS = [
9
+ chunkISV6HFQE_cjs.TOOLS_CALL_METHOD,
10
+ chunkISV6HFQE_cjs.READ_RESOURCE_METHOD,
11
+ chunkISV6HFQE_cjs.LIST_RESOURCES_METHOD
12
+ ];
8
13
  function synapseVite(options = {}) {
9
14
  const enablePreview = options.preview !== false;
10
15
  let manifest = null;
11
16
  let appName = options.appName ?? "app";
12
17
  let serverProcess = null;
13
18
  const pendingRequests = /* @__PURE__ */ new Map();
19
+ const eventClients = /* @__PURE__ */ new Set();
20
+ function forwardNotification(msg) {
21
+ if (msg.method !== chunkISV6HFQE_cjs.RESOURCE_LIST_CHANGED_METHOD) return;
22
+ const frame = `data: ${JSON.stringify({ method: msg.method, params: msg.params ?? {} })}
23
+
24
+ `;
25
+ for (const client of eventClients) client.write(frame);
26
+ }
14
27
  let serverBuffer = "";
15
28
  function loadManifest(root) {
16
29
  const manifestPath = options.manifest ? path.resolve(options.manifest) : path.resolve(root, "..", "manifest.json");
@@ -53,6 +66,8 @@ function synapseVite(options = {}) {
53
66
  const p = pendingRequests.get(msg.id);
54
67
  pendingRequests.delete(msg.id);
55
68
  p?.resolve(msg);
69
+ } else if (msg.id === void 0 && typeof msg.method === "string") {
70
+ forwardNotification(msg);
56
71
  }
57
72
  } catch {
58
73
  process.stderr.write(` [mcp] ${line}
@@ -82,20 +97,15 @@ function synapseVite(options = {}) {
82
97
  serverProcess.stdin.write(`${JSON.stringify(msg)}
83
98
  `);
84
99
  }
85
- function callServerTool(name, args) {
100
+ function callServer(method, params) {
86
101
  return new Promise((resolve2, reject) => {
87
102
  const id = `preview-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
88
103
  pendingRequests.set(id, { resolve: resolve2, reject });
89
- sendToServer({
90
- jsonrpc: "2.0",
91
- id,
92
- method: "tools/call",
93
- params: { name, arguments: args }
94
- });
104
+ sendToServer({ jsonrpc: "2.0", id, method, params });
95
105
  setTimeout(() => {
96
106
  if (pendingRequests.has(id)) {
97
107
  pendingRequests.delete(id);
98
- reject(new Error("Tool call timed out (10s)"));
108
+ reject(new Error(`${method} timed out (10s)`));
99
109
  }
100
110
  }, 1e4);
101
111
  });
@@ -137,7 +147,20 @@ function synapseVite(options = {}) {
137
147
  res.setHeader("Access-Control-Allow-Headers", "*");
138
148
  if (req.url === "/__preview" || req.url === "/__preview/") {
139
149
  res.writeHead(200, { "Content-Type": "text/html" });
140
- res.end(previewHostHtml(appName));
150
+ res.end(vitePreviewHostHtml(appName));
151
+ return;
152
+ }
153
+ if (req.method === "GET" && req.url === "/__events") {
154
+ res.writeHead(200, {
155
+ "Content-Type": "text/event-stream",
156
+ "Cache-Control": "no-cache",
157
+ Connection: "keep-alive"
158
+ });
159
+ res.write(": open\n\n");
160
+ eventClients.add(res);
161
+ req.on("close", () => {
162
+ eventClients.delete(res);
163
+ });
141
164
  return;
142
165
  }
143
166
  if (req.method === "POST" && req.url === "/__mcp") {
@@ -148,7 +171,10 @@ function synapseVite(options = {}) {
148
171
  req.on("end", async () => {
149
172
  try {
150
173
  const msg = JSON.parse(body);
151
- const result = await callServerTool(msg.params.name, msg.params.arguments || {});
174
+ if (!PROXIED_SERVER_METHODS.includes(msg.method)) {
175
+ throw new Error(`${msg.method} is not a method this host proxies`);
176
+ }
177
+ const result = await callServer(msg.method, msg.params ?? {});
152
178
  res.writeHead(200, { "Content-Type": "application/json" });
153
179
  res.end(JSON.stringify(result));
154
180
  } catch (err) {
@@ -175,7 +201,7 @@ function synapseVite(options = {}) {
175
201
  }
176
202
  };
177
203
  }
178
- function previewHostHtml(appName) {
204
+ function vitePreviewHostHtml(appName) {
179
205
  return `<!DOCTYPE html>
180
206
  <html>
181
207
  <head>
@@ -206,64 +232,76 @@ function previewHostHtml(appName) {
206
232
  <script>
207
233
  var iframe = document.getElementById("app");
208
234
  var dark = true;
235
+ var PROXIED = ${JSON.stringify(PROXIED_SERVER_METHODS)};
209
236
 
237
+ // Every key here is one the spec's style-variable enum names. That enum is
238
+ // a strict record, so a single extra key makes a spec client reject the
239
+ // whole \`ui/initialize\` result and refuse to connect \u2014 not just ignore the
240
+ // key. The kit's own neutral defaults back anything a host leaves out.
210
241
  function getTokens(d) {
211
242
  return d ? {
212
243
  "--color-background-primary":"#0f172a","--color-background-secondary":"#1e293b",
213
244
  "--color-background-tertiary":"#2a374a","--color-text-primary":"#e2e8f0",
214
245
  "--color-text-secondary":"#94a3b8","--color-text-tertiary":"#64748b",
215
- "--color-text-accent":"#6366f1","--nb-color-accent-foreground":"#fff",
216
246
  "--color-border-primary":"#334155","--color-border-secondary":"#475569",
217
- "--color-ring-primary":"#6366f1","--nb-color-danger":"#ef4444",
247
+ "--color-ring-primary":"#6366f1",
218
248
  "--border-radius-sm":"0.5rem","--font-sans":"-apple-system,BlinkMacSystemFont,sans-serif"
219
249
  } : {
220
250
  "--color-background-primary":"#ffffff","--color-background-secondary":"#f8fafc",
221
251
  "--color-background-tertiary":"#f1f5f9","--color-text-primary":"#0f172a",
222
252
  "--color-text-secondary":"#64748b","--color-text-tertiary":"#94a3b8",
223
- "--color-text-accent":"#6366f1","--nb-color-accent-foreground":"#fff",
224
253
  "--color-border-primary":"#e2e8f0","--color-border-secondary":"#cbd5e1",
225
- "--color-ring-primary":"#6366f1","--nb-color-danger":"#ef4444",
254
+ "--color-ring-primary":"#6366f1",
226
255
  "--border-radius-sm":"0.5rem","--font-sans":"-apple-system,BlinkMacSystemFont,sans-serif"
227
256
  };
228
257
  }
229
258
 
230
259
  function post(msg) { iframe.contentWindow.postMessage(msg, "*"); }
231
260
 
261
+ // A request is a frame carrying an id \u2014 and \`0\` is a perfectly good id.
262
+ // The MCP SDK numbers from zero, so a truthiness test drops the first
263
+ // request every spec client ever sends, which is its \`ui/initialize\`.
264
+ function isRequest(msg) { return msg.id !== undefined && msg.id !== null; }
265
+
232
266
  window.addEventListener("message", async function(e) {
233
267
  if (e.source !== iframe.contentWindow) return;
234
268
  var msg = e.data;
235
269
  if (!msg || typeof msg !== "object") return;
236
270
 
237
271
  // ext-apps handshake
238
- if (msg.method === "ui/initialize" && msg.id) {
272
+ // \`hostInfo\`/\`hostCapabilities\` are the spec's field names, and a spec
273
+ // client validates the result against them \u2014 the official ext-apps
274
+ // \`App\` refuses to connect to anything else.
275
+ if (msg.method === "ui/initialize" && isRequest(msg)) {
239
276
  post({ jsonrpc:"2.0", id:msg.id, result: {
240
277
  protocolVersion:"2026-01-26",
241
- serverInfo:{name:"nimblebrain",version:"preview"},
242
- capabilities:{openLinks:{},serverTools:{}},
278
+ hostInfo:{name:"nimblebrain",version:"preview"},
279
+ hostCapabilities:{openLinks:{},serverTools:{},serverResources:{listChanged:true},
280
+ updateModelContext:{text:{},structuredContent:{}},
281
+ experimental:{"ai.nimblebrain/action":{},"ai.nimblebrain/keydown":{}}},
243
282
  hostContext:{theme:dark?"dark":"light",styles:{variables:getTokens(dark)}}
244
283
  }});
245
284
  return;
246
285
  }
247
286
  if (msg.method === "ui/notifications/initialized") return;
248
287
 
249
- // Tool calls \u2014 proxy via Vite middleware
250
- if (msg.method === "tools/call" && msg.id) {
288
+ // Server-bound requests \u2014 proxied via the Vite middleware. The set is
289
+ // exactly what \`serverTools\` and \`serverResources\` above announce, so
290
+ // neither is declared without something on the other end.
291
+ if (PROXIED.indexOf(msg.method) !== -1 && isRequest(msg)) {
251
292
  var originalId = msg.id;
252
293
  try {
253
294
  var r = await fetch("/__mcp", {
254
295
  method:"POST", headers:{"Content-Type":"application/json"},
255
- body: JSON.stringify({jsonrpc:"2.0",id:msg.id,method:"tools/call",params:{name:msg.params.name,arguments:msg.params.arguments||{}}})
296
+ body: JSON.stringify({jsonrpc:"2.0",id:msg.id,method:msg.method,params:msg.params||{}})
256
297
  });
257
298
  var response = await r.json();
258
299
  response.id = originalId;
259
300
  post(response);
260
- // Note: we intentionally do NOT emit synapse/data-changed here.
261
- // data-changed signals that an *external actor* (the agent) mutated
262
- // server state, prompting the UI to re-fetch. Tool calls originating
263
- // from the UI itself should not trigger this \u2014 doing so creates an
264
- // infinite loop (UI calls tool \u2192 data-changed \u2192 useDataSync re-fetches
265
- // \u2192 calls tool \u2192 data-changed \u2192 ...). In production, the host only
266
- // emits data-changed for agent-initiated tool calls.
301
+ // The page announces no data change of its own. A change is the
302
+ // server's to announce, and that reaches the app over /__events. One
303
+ // fired here on the app's own call would loop: the call fires it, the
304
+ // app re-fetches, and the re-fetch fires it again.
267
305
  } catch(err) {
268
306
  post({jsonrpc:"2.0",id:originalId,error:{code:-32000,message:err.message}});
269
307
  }
@@ -271,10 +309,9 @@ function previewHostHtml(appName) {
271
309
  }
272
310
 
273
311
  // Log other messages
274
- if (msg.method === "synapse/chat") console.log("[chat]", msg.params?.message);
275
- else if (msg.method === "synapse/action") console.log("[action]", msg.params?.action, msg.params);
276
- else if (msg.method === "ui/update-model-context") { console.log("[model-context]", msg.params?.structuredContent); if (msg.id) post({jsonrpc:"2.0",id:msg.id,result:{}}); }
277
- else if (msg.method === "synapse/keydown") { /* ignore */ }
312
+ if (msg.method === "ai.nimblebrain/action") console.log("[action]", msg.params?.action, msg.params);
313
+ 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:{}}); }
314
+ else if (msg.method === "ai.nimblebrain/keydown") { /* ignore */ }
278
315
  else if (msg.method) console.log("[bridge]", msg.method, msg);
279
316
  });
280
317
 
@@ -284,6 +321,13 @@ function previewHostHtml(appName) {
284
321
  post({jsonrpc:"2.0",method:"ui/notifications/host-context-changed",params:{theme:dark?"dark":"light",styles:{variables:getTokens(dark)}}});
285
322
  };
286
323
 
324
+ // The server's own notifications, forwarded into the app as an MCP Apps
325
+ // host forwards them. The dev server passes on only the ones a host would.
326
+ new EventSource("/__events").onmessage = function(e) {
327
+ var n = JSON.parse(e.data);
328
+ post({jsonrpc:"2.0",method:n.method,params:n.params});
329
+ };
330
+
287
331
  // Load the iframe AFTER the message listener is attached to avoid
288
332
  // a race where the app sends ui/initialize before the bridge is ready.
289
333
  iframe.src = "/";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/vite/plugin.ts"],"names":["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 { join, resolve } from \"node:path\";\nimport type { Plugin, ViteDevServer } from \"vite\";\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 tool calls from the iframe through POST /__mcp to the server\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 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 }\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 callServerTool(name: string, args: 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({\n jsonrpc: \"2.0\",\n id,\n method: \"tools/call\",\n params: { name, arguments: args },\n });\n setTimeout(() => {\n if (pendingRequests.has(id)) {\n pendingRequests.delete(id);\n reject(new Error(\"Tool call 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(previewHostHtml(appName));\n return;\n }\n\n // POST /__mcp — tool call 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 const result = await callServerTool(msg.params.name, msg.params.arguments || {});\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\nfunction previewHostHtml(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\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-text-accent\":\"#6366f1\",\"--nb-color-accent-foreground\":\"#fff\",\n \"--color-border-primary\":\"#334155\",\"--color-border-secondary\":\"#475569\",\n \"--color-ring-primary\":\"#6366f1\",\"--nb-color-danger\":\"#ef4444\",\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-text-accent\":\"#6366f1\",\"--nb-color-accent-foreground\":\"#fff\",\n \"--color-border-primary\":\"#e2e8f0\",\"--color-border-secondary\":\"#cbd5e1\",\n \"--color-ring-primary\":\"#6366f1\",\"--nb-color-danger\":\"#ef4444\",\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 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 if (msg.method === \"ui/initialize\" && msg.id) {\n post({ jsonrpc:\"2.0\", id:msg.id, result: {\n protocolVersion:\"2026-01-26\",\n serverInfo:{name:\"nimblebrain\",version:\"preview\"},\n capabilities:{openLinks:{},serverTools:{}},\n hostContext:{theme:dark?\"dark\":\"light\",styles:{variables:getTokens(dark)}}\n }});\n return;\n }\n if (msg.method === \"ui/notifications/initialized\") return;\n\n // Tool calls — proxy via Vite middleware\n if (msg.method === \"tools/call\" && msg.id) {\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:\"tools/call\",params:{name:msg.params.name,arguments:msg.params.arguments||{}}})\n });\n var response = await r.json();\n response.id = originalId;\n post(response);\n // Note: we intentionally do NOT emit synapse/data-changed here.\n // data-changed signals that an *external actor* (the agent) mutated\n // server state, prompting the UI to re-fetch. Tool calls originating\n // from the UI itself should not trigger this — doing so creates an\n // infinite loop (UI calls tool → data-changed → useDataSync re-fetches\n // → calls tool → data-changed → ...). In production, the host only\n // emits data-changed for agent-initiated tool calls.\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 === \"synapse/chat\") console.log(\"[chat]\", msg.params?.message);\n else if (msg.method === \"synapse/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 (msg.id) post({jsonrpc:\"2.0\",id:msg.id,result:{}}); }\n else if (msg.method === \"synapse/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 // 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"]}
1
+ {"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"]}
@@ -20,7 +20,11 @@ interface SynapseVitePluginOptions {
20
20
  * - Reads ../manifest.json to get app name and server config
21
21
  * - Spawns the MCP server as a child process (stdio mode)
22
22
  * - Serves a preview host page at /__preview that iframes your app
23
- * - Proxies tool calls from the iframe through POST /__mcp to the server
23
+ * - Proxies the iframe's tool calls and resource reads through POST /__mcp to
24
+ * the server, the pair its handshake announces as `serverTools` and
25
+ * `serverResources`
26
+ * - Forwards the server's `notifications/resources/list_changed` to the iframe
27
+ * (over GET /__events), as an MCP Apps host does, so `useDataSync` fires
24
28
  * - Handles the ext-apps handshake so Synapse hooks work
25
29
  * - HMR works inside the iframe — edit .tsx, see changes instantly
26
30
  *
@@ -20,7 +20,11 @@ interface SynapseVitePluginOptions {
20
20
  * - Reads ../manifest.json to get app name and server config
21
21
  * - Spawns the MCP server as a child process (stdio mode)
22
22
  * - Serves a preview host page at /__preview that iframes your app
23
- * - Proxies tool calls from the iframe through POST /__mcp to the server
23
+ * - Proxies the iframe's tool calls and resource reads through POST /__mcp to
24
+ * the server, the pair its handshake announces as `serverTools` and
25
+ * `serverResources`
26
+ * - Forwards the server's `notifications/resources/list_changed` to the iframe
27
+ * (over GET /__events), as an MCP Apps host does, so `useDataSync` fires
24
28
  * - Handles the ext-apps handshake so Synapse hooks work
25
29
  * - HMR works inside the iframe — edit .tsx, see changes instantly
26
30
  *
@@ -1,14 +1,27 @@
1
+ import { TOOLS_CALL_METHOD, READ_RESOURCE_METHOD, LIST_RESOURCES_METHOD, RESOURCE_LIST_CHANGED_METHOD } from '../chunk-H3INUKA3.js';
1
2
  import { spawn } from 'child_process';
2
3
  import { existsSync, readFileSync } from 'fs';
3
4
  import { resolve, join } from 'path';
4
5
 
5
- // src/vite/plugin.ts
6
+ var PROXIED_SERVER_METHODS = [
7
+ TOOLS_CALL_METHOD,
8
+ READ_RESOURCE_METHOD,
9
+ LIST_RESOURCES_METHOD
10
+ ];
6
11
  function synapseVite(options = {}) {
7
12
  const enablePreview = options.preview !== false;
8
13
  let manifest = null;
9
14
  let appName = options.appName ?? "app";
10
15
  let serverProcess = null;
11
16
  const pendingRequests = /* @__PURE__ */ new Map();
17
+ const eventClients = /* @__PURE__ */ new Set();
18
+ function forwardNotification(msg) {
19
+ if (msg.method !== RESOURCE_LIST_CHANGED_METHOD) return;
20
+ const frame = `data: ${JSON.stringify({ method: msg.method, params: msg.params ?? {} })}
21
+
22
+ `;
23
+ for (const client of eventClients) client.write(frame);
24
+ }
12
25
  let serverBuffer = "";
13
26
  function loadManifest(root) {
14
27
  const manifestPath = options.manifest ? resolve(options.manifest) : resolve(root, "..", "manifest.json");
@@ -51,6 +64,8 @@ function synapseVite(options = {}) {
51
64
  const p = pendingRequests.get(msg.id);
52
65
  pendingRequests.delete(msg.id);
53
66
  p?.resolve(msg);
67
+ } else if (msg.id === void 0 && typeof msg.method === "string") {
68
+ forwardNotification(msg);
54
69
  }
55
70
  } catch {
56
71
  process.stderr.write(` [mcp] ${line}
@@ -80,20 +95,15 @@ function synapseVite(options = {}) {
80
95
  serverProcess.stdin.write(`${JSON.stringify(msg)}
81
96
  `);
82
97
  }
83
- function callServerTool(name, args) {
98
+ function callServer(method, params) {
84
99
  return new Promise((resolve2, reject) => {
85
100
  const id = `preview-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
86
101
  pendingRequests.set(id, { resolve: resolve2, reject });
87
- sendToServer({
88
- jsonrpc: "2.0",
89
- id,
90
- method: "tools/call",
91
- params: { name, arguments: args }
92
- });
102
+ sendToServer({ jsonrpc: "2.0", id, method, params });
93
103
  setTimeout(() => {
94
104
  if (pendingRequests.has(id)) {
95
105
  pendingRequests.delete(id);
96
- reject(new Error("Tool call timed out (10s)"));
106
+ reject(new Error(`${method} timed out (10s)`));
97
107
  }
98
108
  }, 1e4);
99
109
  });
@@ -135,7 +145,20 @@ function synapseVite(options = {}) {
135
145
  res.setHeader("Access-Control-Allow-Headers", "*");
136
146
  if (req.url === "/__preview" || req.url === "/__preview/") {
137
147
  res.writeHead(200, { "Content-Type": "text/html" });
138
- res.end(previewHostHtml(appName));
148
+ res.end(vitePreviewHostHtml(appName));
149
+ return;
150
+ }
151
+ if (req.method === "GET" && req.url === "/__events") {
152
+ res.writeHead(200, {
153
+ "Content-Type": "text/event-stream",
154
+ "Cache-Control": "no-cache",
155
+ Connection: "keep-alive"
156
+ });
157
+ res.write(": open\n\n");
158
+ eventClients.add(res);
159
+ req.on("close", () => {
160
+ eventClients.delete(res);
161
+ });
139
162
  return;
140
163
  }
141
164
  if (req.method === "POST" && req.url === "/__mcp") {
@@ -146,7 +169,10 @@ function synapseVite(options = {}) {
146
169
  req.on("end", async () => {
147
170
  try {
148
171
  const msg = JSON.parse(body);
149
- const result = await callServerTool(msg.params.name, msg.params.arguments || {});
172
+ if (!PROXIED_SERVER_METHODS.includes(msg.method)) {
173
+ throw new Error(`${msg.method} is not a method this host proxies`);
174
+ }
175
+ const result = await callServer(msg.method, msg.params ?? {});
150
176
  res.writeHead(200, { "Content-Type": "application/json" });
151
177
  res.end(JSON.stringify(result));
152
178
  } catch (err) {
@@ -173,7 +199,7 @@ function synapseVite(options = {}) {
173
199
  }
174
200
  };
175
201
  }
176
- function previewHostHtml(appName) {
202
+ function vitePreviewHostHtml(appName) {
177
203
  return `<!DOCTYPE html>
178
204
  <html>
179
205
  <head>
@@ -204,64 +230,76 @@ function previewHostHtml(appName) {
204
230
  <script>
205
231
  var iframe = document.getElementById("app");
206
232
  var dark = true;
233
+ var PROXIED = ${JSON.stringify(PROXIED_SERVER_METHODS)};
207
234
 
235
+ // Every key here is one the spec's style-variable enum names. That enum is
236
+ // a strict record, so a single extra key makes a spec client reject the
237
+ // whole \`ui/initialize\` result and refuse to connect \u2014 not just ignore the
238
+ // key. The kit's own neutral defaults back anything a host leaves out.
208
239
  function getTokens(d) {
209
240
  return d ? {
210
241
  "--color-background-primary":"#0f172a","--color-background-secondary":"#1e293b",
211
242
  "--color-background-tertiary":"#2a374a","--color-text-primary":"#e2e8f0",
212
243
  "--color-text-secondary":"#94a3b8","--color-text-tertiary":"#64748b",
213
- "--color-text-accent":"#6366f1","--nb-color-accent-foreground":"#fff",
214
244
  "--color-border-primary":"#334155","--color-border-secondary":"#475569",
215
- "--color-ring-primary":"#6366f1","--nb-color-danger":"#ef4444",
245
+ "--color-ring-primary":"#6366f1",
216
246
  "--border-radius-sm":"0.5rem","--font-sans":"-apple-system,BlinkMacSystemFont,sans-serif"
217
247
  } : {
218
248
  "--color-background-primary":"#ffffff","--color-background-secondary":"#f8fafc",
219
249
  "--color-background-tertiary":"#f1f5f9","--color-text-primary":"#0f172a",
220
250
  "--color-text-secondary":"#64748b","--color-text-tertiary":"#94a3b8",
221
- "--color-text-accent":"#6366f1","--nb-color-accent-foreground":"#fff",
222
251
  "--color-border-primary":"#e2e8f0","--color-border-secondary":"#cbd5e1",
223
- "--color-ring-primary":"#6366f1","--nb-color-danger":"#ef4444",
252
+ "--color-ring-primary":"#6366f1",
224
253
  "--border-radius-sm":"0.5rem","--font-sans":"-apple-system,BlinkMacSystemFont,sans-serif"
225
254
  };
226
255
  }
227
256
 
228
257
  function post(msg) { iframe.contentWindow.postMessage(msg, "*"); }
229
258
 
259
+ // A request is a frame carrying an id \u2014 and \`0\` is a perfectly good id.
260
+ // The MCP SDK numbers from zero, so a truthiness test drops the first
261
+ // request every spec client ever sends, which is its \`ui/initialize\`.
262
+ function isRequest(msg) { return msg.id !== undefined && msg.id !== null; }
263
+
230
264
  window.addEventListener("message", async function(e) {
231
265
  if (e.source !== iframe.contentWindow) return;
232
266
  var msg = e.data;
233
267
  if (!msg || typeof msg !== "object") return;
234
268
 
235
269
  // ext-apps handshake
236
- if (msg.method === "ui/initialize" && msg.id) {
270
+ // \`hostInfo\`/\`hostCapabilities\` are the spec's field names, and a spec
271
+ // client validates the result against them \u2014 the official ext-apps
272
+ // \`App\` refuses to connect to anything else.
273
+ if (msg.method === "ui/initialize" && isRequest(msg)) {
237
274
  post({ jsonrpc:"2.0", id:msg.id, result: {
238
275
  protocolVersion:"2026-01-26",
239
- serverInfo:{name:"nimblebrain",version:"preview"},
240
- capabilities:{openLinks:{},serverTools:{}},
276
+ hostInfo:{name:"nimblebrain",version:"preview"},
277
+ hostCapabilities:{openLinks:{},serverTools:{},serverResources:{listChanged:true},
278
+ updateModelContext:{text:{},structuredContent:{}},
279
+ experimental:{"ai.nimblebrain/action":{},"ai.nimblebrain/keydown":{}}},
241
280
  hostContext:{theme:dark?"dark":"light",styles:{variables:getTokens(dark)}}
242
281
  }});
243
282
  return;
244
283
  }
245
284
  if (msg.method === "ui/notifications/initialized") return;
246
285
 
247
- // Tool calls \u2014 proxy via Vite middleware
248
- if (msg.method === "tools/call" && msg.id) {
286
+ // Server-bound requests \u2014 proxied via the Vite middleware. The set is
287
+ // exactly what \`serverTools\` and \`serverResources\` above announce, so
288
+ // neither is declared without something on the other end.
289
+ if (PROXIED.indexOf(msg.method) !== -1 && isRequest(msg)) {
249
290
  var originalId = msg.id;
250
291
  try {
251
292
  var r = await fetch("/__mcp", {
252
293
  method:"POST", headers:{"Content-Type":"application/json"},
253
- body: JSON.stringify({jsonrpc:"2.0",id:msg.id,method:"tools/call",params:{name:msg.params.name,arguments:msg.params.arguments||{}}})
294
+ body: JSON.stringify({jsonrpc:"2.0",id:msg.id,method:msg.method,params:msg.params||{}})
254
295
  });
255
296
  var response = await r.json();
256
297
  response.id = originalId;
257
298
  post(response);
258
- // Note: we intentionally do NOT emit synapse/data-changed here.
259
- // data-changed signals that an *external actor* (the agent) mutated
260
- // server state, prompting the UI to re-fetch. Tool calls originating
261
- // from the UI itself should not trigger this \u2014 doing so creates an
262
- // infinite loop (UI calls tool \u2192 data-changed \u2192 useDataSync re-fetches
263
- // \u2192 calls tool \u2192 data-changed \u2192 ...). In production, the host only
264
- // emits data-changed for agent-initiated tool calls.
299
+ // The page announces no data change of its own. A change is the
300
+ // server's to announce, and that reaches the app over /__events. One
301
+ // fired here on the app's own call would loop: the call fires it, the
302
+ // app re-fetches, and the re-fetch fires it again.
265
303
  } catch(err) {
266
304
  post({jsonrpc:"2.0",id:originalId,error:{code:-32000,message:err.message}});
267
305
  }
@@ -269,10 +307,9 @@ function previewHostHtml(appName) {
269
307
  }
270
308
 
271
309
  // Log other messages
272
- if (msg.method === "synapse/chat") console.log("[chat]", msg.params?.message);
273
- else if (msg.method === "synapse/action") console.log("[action]", msg.params?.action, msg.params);
274
- else if (msg.method === "ui/update-model-context") { console.log("[model-context]", msg.params?.structuredContent); if (msg.id) post({jsonrpc:"2.0",id:msg.id,result:{}}); }
275
- else if (msg.method === "synapse/keydown") { /* ignore */ }
310
+ if (msg.method === "ai.nimblebrain/action") console.log("[action]", msg.params?.action, msg.params);
311
+ 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:{}}); }
312
+ else if (msg.method === "ai.nimblebrain/keydown") { /* ignore */ }
276
313
  else if (msg.method) console.log("[bridge]", msg.method, msg);
277
314
  });
278
315
 
@@ -282,6 +319,13 @@ function previewHostHtml(appName) {
282
319
  post({jsonrpc:"2.0",method:"ui/notifications/host-context-changed",params:{theme:dark?"dark":"light",styles:{variables:getTokens(dark)}}});
283
320
  };
284
321
 
322
+ // The server's own notifications, forwarded into the app as an MCP Apps
323
+ // host forwards them. The dev server passes on only the ones a host would.
324
+ new EventSource("/__events").onmessage = function(e) {
325
+ var n = JSON.parse(e.data);
326
+ post({jsonrpc:"2.0",method:n.method,params:n.params});
327
+ };
328
+
285
329
  // Load the iframe AFTER the message listener is attached to avoid
286
330
  // a race where the app sends ui/initialize before the bridge is ready.
287
331
  iframe.src = "/";