@nimblebrain/synapse 0.18.0 → 0.19.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 (93) hide show
  1. package/README.md +13 -13
  2. package/dist/chunk-54W6IAHM.js +24 -0
  3. package/dist/chunk-54W6IAHM.js.map +1 -0
  4. package/dist/{chunk-HKRTDGXN.js → chunk-DRLVEHCD.js} +3 -13
  5. package/dist/chunk-DRLVEHCD.js.map +1 -0
  6. package/dist/chunk-NP7ROSH2.cjs +33 -0
  7. package/dist/chunk-NP7ROSH2.cjs.map +1 -0
  8. package/dist/{chunk-ZLASWV4N.js → chunk-OYLZ5OCK.js} +4 -4
  9. package/dist/chunk-OYLZ5OCK.js.map +1 -0
  10. package/dist/chunk-P5DBJFSQ.js +8459 -0
  11. package/dist/chunk-P5DBJFSQ.js.map +1 -0
  12. package/dist/chunk-PZ5AY32C.js +9 -0
  13. package/dist/chunk-PZ5AY32C.js.map +1 -0
  14. package/dist/chunk-Q7SFCCGT.cjs +11 -0
  15. package/dist/chunk-Q7SFCCGT.cjs.map +1 -0
  16. package/dist/chunk-QXDCA7QW.cjs +8466 -0
  17. package/dist/chunk-QXDCA7QW.cjs.map +1 -0
  18. package/dist/{chunk-VDQZQDU3.cjs → chunk-RU64F5RS.cjs} +7 -7
  19. package/dist/chunk-RU64F5RS.cjs.map +1 -0
  20. package/dist/{chunk-LPWEQCZV.cjs → chunk-SZWHFTXV.cjs} +2 -13
  21. package/dist/chunk-SZWHFTXV.cjs.map +1 -0
  22. package/dist/codegen/cli.cjs +4 -4
  23. package/dist/codegen/cli.js +4 -4
  24. package/dist/codegen/index.cjs +1 -0
  25. package/dist/codegen/index.d.cts +1 -1
  26. package/dist/codegen/index.d.ts +1 -1
  27. package/dist/codegen/index.js +1 -0
  28. package/dist/connect.iife.global.js +66 -43
  29. package/dist/{detect-Bf8Q_0dK.d.ts → detect-DBwc8a21.d.ts} +2 -2
  30. package/dist/{detect-BHYg26_d.d.cts → detect-DZMJsyvZ.d.cts} +2 -2
  31. package/dist/host/index.cjs +15 -14
  32. package/dist/host/index.d.cts +3 -3
  33. package/dist/host/index.d.ts +3 -3
  34. package/dist/host/index.js +3 -2
  35. package/dist/index.cjs +42 -18
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.d.cts +40 -13
  38. package/dist/index.d.ts +40 -13
  39. package/dist/index.js +35 -3
  40. package/dist/index.js.map +1 -1
  41. package/dist/react/index.cjs +12 -10
  42. package/dist/react/index.cjs.map +1 -1
  43. package/dist/react/index.d.cts +14 -5
  44. package/dist/react/index.d.ts +14 -5
  45. package/dist/react/index.js +7 -5
  46. package/dist/react/index.js.map +1 -1
  47. package/dist/schema-reader-GM4A4L44.js +4 -0
  48. package/dist/{schema-reader-776246MJ.js.map → schema-reader-GM4A4L44.js.map} +1 -1
  49. package/dist/{schema-reader-B6LTGBMJ.cjs → schema-reader-MWBRQ2TQ.cjs} +3 -2
  50. package/dist/{schema-reader-B6LTGBMJ.cjs.map → schema-reader-MWBRQ2TQ.cjs.map} +1 -1
  51. package/dist/{server-QIYIJHD5.cjs → server-LR6T6C44.cjs} +32 -20
  52. package/dist/server-LR6T6C44.cjs.map +1 -0
  53. package/dist/{server-5N76YCWC.js → server-UHPEE55X.js} +32 -21
  54. package/dist/server-UHPEE55X.js.map +1 -0
  55. package/dist/synapse-runtime.iife.global.js +66 -43
  56. package/dist/synapse-ui.iife.global.js +2 -2
  57. package/dist/{type-generator-UA3L4OIA.cjs → type-generator-YTPFWLVG.cjs} +3 -2
  58. package/dist/{type-generator-UA3L4OIA.cjs.map → type-generator-YTPFWLVG.cjs.map} +1 -1
  59. package/dist/type-generator-ZYSVFK3X.js +4 -0
  60. package/dist/{type-generator-MZ4X4DE5.js.map → type-generator-ZYSVFK3X.js.map} +1 -1
  61. package/dist/{types-uEO4VFJ2.d.cts → types-CG08aO82.d.cts} +14 -83
  62. package/dist/{types-uEO4VFJ2.d.ts → types-CG08aO82.d.ts} +14 -83
  63. package/dist/ui/base.cjs +1 -0
  64. package/dist/ui/base.cjs.map +1 -1
  65. package/dist/ui/base.js +1 -0
  66. package/dist/ui/base.js.map +1 -1
  67. package/dist/ui/index.cjs +1 -0
  68. package/dist/ui/index.cjs.map +1 -1
  69. package/dist/ui/index.js +1 -0
  70. package/dist/ui/index.js.map +1 -1
  71. package/dist/vite/index.cjs +74 -30
  72. package/dist/vite/index.cjs.map +1 -1
  73. package/dist/vite/index.d.cts +5 -1
  74. package/dist/vite/index.d.ts +5 -1
  75. package/dist/vite/index.js +74 -30
  76. package/dist/vite/index.js.map +1 -1
  77. package/dist/{writer-NDK4U6C5.cjs → writer-HDRCQSX2.cjs} +3 -3
  78. package/dist/{writer-NDK4U6C5.cjs.map → writer-HDRCQSX2.cjs.map} +1 -1
  79. package/dist/{writer-ZNBXYUYC.js → writer-QNDPMEYD.js} +3 -3
  80. package/dist/{writer-ZNBXYUYC.js.map → writer-QNDPMEYD.js.map} +1 -1
  81. package/package.json +8 -5
  82. package/dist/chunk-3YSXPBEQ.js +0 -781
  83. package/dist/chunk-3YSXPBEQ.js.map +0 -1
  84. package/dist/chunk-FB7GPBEM.cjs +0 -788
  85. package/dist/chunk-FB7GPBEM.cjs.map +0 -1
  86. package/dist/chunk-HKRTDGXN.js.map +0 -1
  87. package/dist/chunk-LPWEQCZV.cjs.map +0 -1
  88. package/dist/chunk-VDQZQDU3.cjs.map +0 -1
  89. package/dist/chunk-ZLASWV4N.js.map +0 -1
  90. package/dist/schema-reader-776246MJ.js +0 -3
  91. package/dist/server-5N76YCWC.js.map +0 -1
  92. package/dist/server-QIYIJHD5.cjs.map +0 -1
  93. package/dist/type-generator-MZ4X4DE5.js +0 -3
@@ -1,16 +1,30 @@
1
1
  'use strict';
2
2
 
3
+ var chunkNP7ROSH2_cjs = require('../chunk-NP7ROSH2.cjs');
4
+ require('../chunk-Q7SFCCGT.cjs');
3
5
  var child_process = require('child_process');
4
6
  var fs = require('fs');
5
7
  var path = require('path');
6
8
 
7
- // src/vite/plugin.ts
9
+ var PROXIED_SERVER_METHODS = [
10
+ chunkNP7ROSH2_cjs.TOOLS_CALL_METHOD,
11
+ chunkNP7ROSH2_cjs.READ_RESOURCE_METHOD,
12
+ chunkNP7ROSH2_cjs.LIST_RESOURCES_METHOD
13
+ ];
8
14
  function synapseVite(options = {}) {
9
15
  const enablePreview = options.preview !== false;
10
16
  let manifest = null;
11
17
  let appName = options.appName ?? "app";
12
18
  let serverProcess = null;
13
19
  const pendingRequests = /* @__PURE__ */ new Map();
20
+ const eventClients = /* @__PURE__ */ new Set();
21
+ function forwardNotification(msg) {
22
+ if (msg.method !== chunkNP7ROSH2_cjs.RESOURCE_LIST_CHANGED_METHOD) return;
23
+ const frame = `data: ${JSON.stringify({ method: msg.method, params: msg.params ?? {} })}
24
+
25
+ `;
26
+ for (const client of eventClients) client.write(frame);
27
+ }
14
28
  let serverBuffer = "";
15
29
  function loadManifest(root) {
16
30
  const manifestPath = options.manifest ? path.resolve(options.manifest) : path.resolve(root, "..", "manifest.json");
@@ -53,6 +67,8 @@ function synapseVite(options = {}) {
53
67
  const p = pendingRequests.get(msg.id);
54
68
  pendingRequests.delete(msg.id);
55
69
  p?.resolve(msg);
70
+ } else if (msg.id === void 0 && typeof msg.method === "string") {
71
+ forwardNotification(msg);
56
72
  }
57
73
  } catch {
58
74
  process.stderr.write(` [mcp] ${line}
@@ -82,20 +98,15 @@ function synapseVite(options = {}) {
82
98
  serverProcess.stdin.write(`${JSON.stringify(msg)}
83
99
  `);
84
100
  }
85
- function callServerTool(name, args) {
101
+ function callServer(method, params) {
86
102
  return new Promise((resolve2, reject) => {
87
103
  const id = `preview-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
88
104
  pendingRequests.set(id, { resolve: resolve2, reject });
89
- sendToServer({
90
- jsonrpc: "2.0",
91
- id,
92
- method: "tools/call",
93
- params: { name, arguments: args }
94
- });
105
+ sendToServer({ jsonrpc: "2.0", id, method, params });
95
106
  setTimeout(() => {
96
107
  if (pendingRequests.has(id)) {
97
108
  pendingRequests.delete(id);
98
- reject(new Error("Tool call timed out (10s)"));
109
+ reject(new Error(`${method} timed out (10s)`));
99
110
  }
100
111
  }, 1e4);
101
112
  });
@@ -137,7 +148,20 @@ function synapseVite(options = {}) {
137
148
  res.setHeader("Access-Control-Allow-Headers", "*");
138
149
  if (req.url === "/__preview" || req.url === "/__preview/") {
139
150
  res.writeHead(200, { "Content-Type": "text/html" });
140
- res.end(previewHostHtml(appName));
151
+ res.end(vitePreviewHostHtml(appName));
152
+ return;
153
+ }
154
+ if (req.method === "GET" && req.url === "/__events") {
155
+ res.writeHead(200, {
156
+ "Content-Type": "text/event-stream",
157
+ "Cache-Control": "no-cache",
158
+ Connection: "keep-alive"
159
+ });
160
+ res.write(": open\n\n");
161
+ eventClients.add(res);
162
+ req.on("close", () => {
163
+ eventClients.delete(res);
164
+ });
141
165
  return;
142
166
  }
143
167
  if (req.method === "POST" && req.url === "/__mcp") {
@@ -148,7 +172,10 @@ function synapseVite(options = {}) {
148
172
  req.on("end", async () => {
149
173
  try {
150
174
  const msg = JSON.parse(body);
151
- const result = await callServerTool(msg.params.name, msg.params.arguments || {});
175
+ if (!PROXIED_SERVER_METHODS.includes(msg.method)) {
176
+ throw new Error(`${msg.method} is not a method this host proxies`);
177
+ }
178
+ const result = await callServer(msg.method, msg.params ?? {});
152
179
  res.writeHead(200, { "Content-Type": "application/json" });
153
180
  res.end(JSON.stringify(result));
154
181
  } catch (err) {
@@ -175,7 +202,7 @@ function synapseVite(options = {}) {
175
202
  }
176
203
  };
177
204
  }
178
- function previewHostHtml(appName) {
205
+ function vitePreviewHostHtml(appName) {
179
206
  return `<!DOCTYPE html>
180
207
  <html>
181
208
  <head>
@@ -206,64 +233,74 @@ function previewHostHtml(appName) {
206
233
  <script>
207
234
  var iframe = document.getElementById("app");
208
235
  var dark = true;
236
+ var PROXIED = ${JSON.stringify(PROXIED_SERVER_METHODS)};
209
237
 
238
+ // Every key here is one the spec's style-variable enum names. That enum is
239
+ // a strict record, so a single extra key makes a spec client reject the
240
+ // whole \`ui/initialize\` result and refuse to connect \u2014 not just ignore the
241
+ // key. The kit's own neutral defaults back anything a host leaves out.
210
242
  function getTokens(d) {
211
243
  return d ? {
212
244
  "--color-background-primary":"#0f172a","--color-background-secondary":"#1e293b",
213
245
  "--color-background-tertiary":"#2a374a","--color-text-primary":"#e2e8f0",
214
246
  "--color-text-secondary":"#94a3b8","--color-text-tertiary":"#64748b",
215
- "--color-text-accent":"#6366f1","--nb-color-accent-foreground":"#fff",
216
247
  "--color-border-primary":"#334155","--color-border-secondary":"#475569",
217
- "--color-ring-primary":"#6366f1","--nb-color-danger":"#ef4444",
248
+ "--color-ring-primary":"#6366f1",
218
249
  "--border-radius-sm":"0.5rem","--font-sans":"-apple-system,BlinkMacSystemFont,sans-serif"
219
250
  } : {
220
251
  "--color-background-primary":"#ffffff","--color-background-secondary":"#f8fafc",
221
252
  "--color-background-tertiary":"#f1f5f9","--color-text-primary":"#0f172a",
222
253
  "--color-text-secondary":"#64748b","--color-text-tertiary":"#94a3b8",
223
- "--color-text-accent":"#6366f1","--nb-color-accent-foreground":"#fff",
224
254
  "--color-border-primary":"#e2e8f0","--color-border-secondary":"#cbd5e1",
225
- "--color-ring-primary":"#6366f1","--nb-color-danger":"#ef4444",
255
+ "--color-ring-primary":"#6366f1",
226
256
  "--border-radius-sm":"0.5rem","--font-sans":"-apple-system,BlinkMacSystemFont,sans-serif"
227
257
  };
228
258
  }
229
259
 
230
260
  function post(msg) { iframe.contentWindow.postMessage(msg, "*"); }
231
261
 
262
+ // A request is a frame carrying an id \u2014 and \`0\` is a perfectly good id.
263
+ // The MCP SDK numbers from zero, so a truthiness test drops the first
264
+ // request every spec client ever sends, which is its \`ui/initialize\`.
265
+ function isRequest(msg) { return msg.id !== undefined && msg.id !== null; }
266
+
232
267
  window.addEventListener("message", async function(e) {
233
268
  if (e.source !== iframe.contentWindow) return;
234
269
  var msg = e.data;
235
270
  if (!msg || typeof msg !== "object") return;
236
271
 
237
272
  // ext-apps handshake
238
- if (msg.method === "ui/initialize" && msg.id) {
273
+ // \`hostInfo\`/\`hostCapabilities\` are the spec's field names, and a spec
274
+ // client validates the result against them \u2014 the official ext-apps
275
+ // \`App\` refuses to connect to anything else.
276
+ if (msg.method === "ui/initialize" && isRequest(msg)) {
239
277
  post({ jsonrpc:"2.0", id:msg.id, result: {
240
278
  protocolVersion:"2026-01-26",
241
- serverInfo:{name:"nimblebrain",version:"preview"},
242
- capabilities:{openLinks:{},serverTools:{}},
279
+ hostInfo:{name:"nimblebrain",version:"preview"},
280
+ hostCapabilities:{openLinks:{},serverTools:{},serverResources:{listChanged:true}},
243
281
  hostContext:{theme:dark?"dark":"light",styles:{variables:getTokens(dark)}}
244
282
  }});
245
283
  return;
246
284
  }
247
285
  if (msg.method === "ui/notifications/initialized") return;
248
286
 
249
- // Tool calls \u2014 proxy via Vite middleware
250
- if (msg.method === "tools/call" && msg.id) {
287
+ // Server-bound requests \u2014 proxied via the Vite middleware. The set is
288
+ // exactly what \`serverTools\` and \`serverResources\` above announce, so
289
+ // neither is declared without something on the other end.
290
+ if (PROXIED.indexOf(msg.method) !== -1 && isRequest(msg)) {
251
291
  var originalId = msg.id;
252
292
  try {
253
293
  var r = await fetch("/__mcp", {
254
294
  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||{}}})
295
+ body: JSON.stringify({jsonrpc:"2.0",id:msg.id,method:msg.method,params:msg.params||{}})
256
296
  });
257
297
  var response = await r.json();
258
298
  response.id = originalId;
259
299
  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.
300
+ // The page announces no data change of its own. A change is the
301
+ // server's to announce, and that reaches the app over /__events. One
302
+ // fired here on the app's own call would loop: the call fires it, the
303
+ // app re-fetches, and the re-fetch fires it again.
267
304
  } catch(err) {
268
305
  post({jsonrpc:"2.0",id:originalId,error:{code:-32000,message:err.message}});
269
306
  }
@@ -273,7 +310,7 @@ function previewHostHtml(appName) {
273
310
  // Log other messages
274
311
  if (msg.method === "synapse/chat") console.log("[chat]", msg.params?.message);
275
312
  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:{}}); }
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:{}}); }
277
314
  else if (msg.method === "synapse/keydown") { /* ignore */ }
278
315
  else if (msg.method) console.log("[bridge]", msg.method, msg);
279
316
  });
@@ -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 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 === \"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 (isRequest(msg)) 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 // 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,28 @@
1
+ import { TOOLS_CALL_METHOD, READ_RESOURCE_METHOD, LIST_RESOURCES_METHOD, RESOURCE_LIST_CHANGED_METHOD } from '../chunk-54W6IAHM.js';
2
+ import '../chunk-PZ5AY32C.js';
1
3
  import { spawn } from 'child_process';
2
4
  import { existsSync, readFileSync } from 'fs';
3
5
  import { resolve, join } from 'path';
4
6
 
5
- // src/vite/plugin.ts
7
+ var PROXIED_SERVER_METHODS = [
8
+ TOOLS_CALL_METHOD,
9
+ READ_RESOURCE_METHOD,
10
+ LIST_RESOURCES_METHOD
11
+ ];
6
12
  function synapseVite(options = {}) {
7
13
  const enablePreview = options.preview !== false;
8
14
  let manifest = null;
9
15
  let appName = options.appName ?? "app";
10
16
  let serverProcess = null;
11
17
  const pendingRequests = /* @__PURE__ */ new Map();
18
+ const eventClients = /* @__PURE__ */ new Set();
19
+ function forwardNotification(msg) {
20
+ if (msg.method !== RESOURCE_LIST_CHANGED_METHOD) return;
21
+ const frame = `data: ${JSON.stringify({ method: msg.method, params: msg.params ?? {} })}
22
+
23
+ `;
24
+ for (const client of eventClients) client.write(frame);
25
+ }
12
26
  let serverBuffer = "";
13
27
  function loadManifest(root) {
14
28
  const manifestPath = options.manifest ? resolve(options.manifest) : resolve(root, "..", "manifest.json");
@@ -51,6 +65,8 @@ function synapseVite(options = {}) {
51
65
  const p = pendingRequests.get(msg.id);
52
66
  pendingRequests.delete(msg.id);
53
67
  p?.resolve(msg);
68
+ } else if (msg.id === void 0 && typeof msg.method === "string") {
69
+ forwardNotification(msg);
54
70
  }
55
71
  } catch {
56
72
  process.stderr.write(` [mcp] ${line}
@@ -80,20 +96,15 @@ function synapseVite(options = {}) {
80
96
  serverProcess.stdin.write(`${JSON.stringify(msg)}
81
97
  `);
82
98
  }
83
- function callServerTool(name, args) {
99
+ function callServer(method, params) {
84
100
  return new Promise((resolve2, reject) => {
85
101
  const id = `preview-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
86
102
  pendingRequests.set(id, { resolve: resolve2, reject });
87
- sendToServer({
88
- jsonrpc: "2.0",
89
- id,
90
- method: "tools/call",
91
- params: { name, arguments: args }
92
- });
103
+ sendToServer({ jsonrpc: "2.0", id, method, params });
93
104
  setTimeout(() => {
94
105
  if (pendingRequests.has(id)) {
95
106
  pendingRequests.delete(id);
96
- reject(new Error("Tool call timed out (10s)"));
107
+ reject(new Error(`${method} timed out (10s)`));
97
108
  }
98
109
  }, 1e4);
99
110
  });
@@ -135,7 +146,20 @@ function synapseVite(options = {}) {
135
146
  res.setHeader("Access-Control-Allow-Headers", "*");
136
147
  if (req.url === "/__preview" || req.url === "/__preview/") {
137
148
  res.writeHead(200, { "Content-Type": "text/html" });
138
- res.end(previewHostHtml(appName));
149
+ res.end(vitePreviewHostHtml(appName));
150
+ return;
151
+ }
152
+ if (req.method === "GET" && req.url === "/__events") {
153
+ res.writeHead(200, {
154
+ "Content-Type": "text/event-stream",
155
+ "Cache-Control": "no-cache",
156
+ Connection: "keep-alive"
157
+ });
158
+ res.write(": open\n\n");
159
+ eventClients.add(res);
160
+ req.on("close", () => {
161
+ eventClients.delete(res);
162
+ });
139
163
  return;
140
164
  }
141
165
  if (req.method === "POST" && req.url === "/__mcp") {
@@ -146,7 +170,10 @@ function synapseVite(options = {}) {
146
170
  req.on("end", async () => {
147
171
  try {
148
172
  const msg = JSON.parse(body);
149
- const result = await callServerTool(msg.params.name, msg.params.arguments || {});
173
+ if (!PROXIED_SERVER_METHODS.includes(msg.method)) {
174
+ throw new Error(`${msg.method} is not a method this host proxies`);
175
+ }
176
+ const result = await callServer(msg.method, msg.params ?? {});
150
177
  res.writeHead(200, { "Content-Type": "application/json" });
151
178
  res.end(JSON.stringify(result));
152
179
  } catch (err) {
@@ -173,7 +200,7 @@ function synapseVite(options = {}) {
173
200
  }
174
201
  };
175
202
  }
176
- function previewHostHtml(appName) {
203
+ function vitePreviewHostHtml(appName) {
177
204
  return `<!DOCTYPE html>
178
205
  <html>
179
206
  <head>
@@ -204,64 +231,74 @@ function previewHostHtml(appName) {
204
231
  <script>
205
232
  var iframe = document.getElementById("app");
206
233
  var dark = true;
234
+ var PROXIED = ${JSON.stringify(PROXIED_SERVER_METHODS)};
207
235
 
236
+ // Every key here is one the spec's style-variable enum names. That enum is
237
+ // a strict record, so a single extra key makes a spec client reject the
238
+ // whole \`ui/initialize\` result and refuse to connect \u2014 not just ignore the
239
+ // key. The kit's own neutral defaults back anything a host leaves out.
208
240
  function getTokens(d) {
209
241
  return d ? {
210
242
  "--color-background-primary":"#0f172a","--color-background-secondary":"#1e293b",
211
243
  "--color-background-tertiary":"#2a374a","--color-text-primary":"#e2e8f0",
212
244
  "--color-text-secondary":"#94a3b8","--color-text-tertiary":"#64748b",
213
- "--color-text-accent":"#6366f1","--nb-color-accent-foreground":"#fff",
214
245
  "--color-border-primary":"#334155","--color-border-secondary":"#475569",
215
- "--color-ring-primary":"#6366f1","--nb-color-danger":"#ef4444",
246
+ "--color-ring-primary":"#6366f1",
216
247
  "--border-radius-sm":"0.5rem","--font-sans":"-apple-system,BlinkMacSystemFont,sans-serif"
217
248
  } : {
218
249
  "--color-background-primary":"#ffffff","--color-background-secondary":"#f8fafc",
219
250
  "--color-background-tertiary":"#f1f5f9","--color-text-primary":"#0f172a",
220
251
  "--color-text-secondary":"#64748b","--color-text-tertiary":"#94a3b8",
221
- "--color-text-accent":"#6366f1","--nb-color-accent-foreground":"#fff",
222
252
  "--color-border-primary":"#e2e8f0","--color-border-secondary":"#cbd5e1",
223
- "--color-ring-primary":"#6366f1","--nb-color-danger":"#ef4444",
253
+ "--color-ring-primary":"#6366f1",
224
254
  "--border-radius-sm":"0.5rem","--font-sans":"-apple-system,BlinkMacSystemFont,sans-serif"
225
255
  };
226
256
  }
227
257
 
228
258
  function post(msg) { iframe.contentWindow.postMessage(msg, "*"); }
229
259
 
260
+ // A request is a frame carrying an id \u2014 and \`0\` is a perfectly good id.
261
+ // The MCP SDK numbers from zero, so a truthiness test drops the first
262
+ // request every spec client ever sends, which is its \`ui/initialize\`.
263
+ function isRequest(msg) { return msg.id !== undefined && msg.id !== null; }
264
+
230
265
  window.addEventListener("message", async function(e) {
231
266
  if (e.source !== iframe.contentWindow) return;
232
267
  var msg = e.data;
233
268
  if (!msg || typeof msg !== "object") return;
234
269
 
235
270
  // ext-apps handshake
236
- if (msg.method === "ui/initialize" && msg.id) {
271
+ // \`hostInfo\`/\`hostCapabilities\` are the spec's field names, and a spec
272
+ // client validates the result against them \u2014 the official ext-apps
273
+ // \`App\` refuses to connect to anything else.
274
+ if (msg.method === "ui/initialize" && isRequest(msg)) {
237
275
  post({ jsonrpc:"2.0", id:msg.id, result: {
238
276
  protocolVersion:"2026-01-26",
239
- serverInfo:{name:"nimblebrain",version:"preview"},
240
- capabilities:{openLinks:{},serverTools:{}},
277
+ hostInfo:{name:"nimblebrain",version:"preview"},
278
+ hostCapabilities:{openLinks:{},serverTools:{},serverResources:{listChanged:true}},
241
279
  hostContext:{theme:dark?"dark":"light",styles:{variables:getTokens(dark)}}
242
280
  }});
243
281
  return;
244
282
  }
245
283
  if (msg.method === "ui/notifications/initialized") return;
246
284
 
247
- // Tool calls \u2014 proxy via Vite middleware
248
- if (msg.method === "tools/call" && msg.id) {
285
+ // Server-bound requests \u2014 proxied via the Vite middleware. The set is
286
+ // exactly what \`serverTools\` and \`serverResources\` above announce, so
287
+ // neither is declared without something on the other end.
288
+ if (PROXIED.indexOf(msg.method) !== -1 && isRequest(msg)) {
249
289
  var originalId = msg.id;
250
290
  try {
251
291
  var r = await fetch("/__mcp", {
252
292
  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||{}}})
293
+ body: JSON.stringify({jsonrpc:"2.0",id:msg.id,method:msg.method,params:msg.params||{}})
254
294
  });
255
295
  var response = await r.json();
256
296
  response.id = originalId;
257
297
  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.
298
+ // The page announces no data change of its own. A change is the
299
+ // server's to announce, and that reaches the app over /__events. One
300
+ // fired here on the app's own call would loop: the call fires it, the
301
+ // app re-fetches, and the re-fetch fires it again.
265
302
  } catch(err) {
266
303
  post({jsonrpc:"2.0",id:originalId,error:{code:-32000,message:err.message}});
267
304
  }
@@ -271,7 +308,7 @@ function previewHostHtml(appName) {
271
308
  // Log other messages
272
309
  if (msg.method === "synapse/chat") console.log("[chat]", msg.params?.message);
273
310
  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:{}}); }
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:{}}); }
275
312
  else if (msg.method === "synapse/keydown") { /* ignore */ }
276
313
  else if (msg.method) console.log("[bridge]", msg.method, msg);
277
314
  });
@@ -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 = "/";