@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.
- package/README.md +32 -37
- package/dist/check-JEM5PN64.cjs +435 -0
- package/dist/check-JEM5PN64.cjs.map +1 -0
- package/dist/check-YDZUQT6A.js +429 -0
- package/dist/check-YDZUQT6A.js.map +1 -0
- package/dist/{chunk-VDQZQDU3.cjs → chunk-BNF352SA.cjs} +56 -145
- package/dist/chunk-BNF352SA.cjs.map +1 -0
- package/dist/{chunk-HKRTDGXN.js → chunk-DJS2FKBN.js} +21 -89
- package/dist/chunk-DJS2FKBN.js.map +1 -0
- package/dist/chunk-H3INUKA3.js +32 -0
- package/dist/chunk-H3INUKA3.js.map +1 -0
- package/dist/{chunk-FB7GPBEM.cjs → chunk-HQLSVUOR.cjs} +193 -314
- package/dist/chunk-HQLSVUOR.cjs.map +1 -0
- package/dist/chunk-ISV6HFQE.cjs +40 -0
- package/dist/chunk-ISV6HFQE.cjs.map +1 -0
- package/dist/{chunk-3YSXPBEQ.js → chunk-TR2YNWZ5.js} +190 -313
- package/dist/chunk-TR2YNWZ5.js.map +1 -0
- package/dist/{chunk-ZLASWV4N.js → chunk-UKP4W6DI.js} +56 -145
- package/dist/chunk-UKP4W6DI.js.map +1 -0
- package/dist/{chunk-LPWEQCZV.cjs → chunk-YSL7KJNU.cjs} +21 -92
- package/dist/chunk-YSL7KJNU.cjs.map +1 -0
- package/dist/codegen/cli.cjs +38 -1
- package/dist/codegen/cli.cjs.map +1 -1
- package/dist/codegen/cli.js +38 -1
- package/dist/codegen/cli.js.map +1 -1
- package/dist/codegen/index.d.cts +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/connect.iife.global.js +66 -43
- package/dist/{detect-BHYg26_d.d.cts → detect-DVGaL2bH.d.cts} +39 -33
- package/dist/{detect-Bf8Q_0dK.d.ts → detect-DVGaL2bH.d.ts} +39 -33
- package/dist/host/index.cjs +17 -17
- package/dist/host/index.d.cts +11 -27
- package/dist/host/index.d.ts +11 -27
- package/dist/host/index.js +2 -2
- package/dist/index.cjs +52 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -31
- package/dist/index.d.ts +104 -31
- package/dist/index.js +34 -3
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +11 -10
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +49 -12
- package/dist/react/index.d.ts +49 -12
- package/dist/react/index.js +6 -5
- package/dist/react/index.js.map +1 -1
- package/dist/{server-QIYIJHD5.cjs → server-BR5MNFOB.cjs} +40 -29
- package/dist/server-BR5MNFOB.cjs.map +1 -0
- package/dist/{server-5N76YCWC.js → server-OOEEWAEG.js} +40 -30
- package/dist/server-OOEEWAEG.js.map +1 -0
- package/dist/synapse-runtime.iife.global.js +66 -43
- package/dist/synapse-ui.iife.global.js +4 -4
- package/dist/{types-uEO4VFJ2.d.cts → types-BxPfGHKO.d.cts} +48 -125
- package/dist/{types-uEO4VFJ2.d.ts → types-BxPfGHKO.d.ts} +48 -125
- package/dist/vite/index.cjs +77 -33
- package/dist/vite/index.cjs.map +1 -1
- package/dist/vite/index.d.cts +5 -1
- package/dist/vite/index.d.ts +5 -1
- package/dist/vite/index.js +77 -33
- package/dist/vite/index.js.map +1 -1
- package/package.json +12 -6
- package/dist/chunk-3YSXPBEQ.js.map +0 -1
- package/dist/chunk-FB7GPBEM.cjs.map +0 -1
- package/dist/chunk-HKRTDGXN.js.map +0 -1
- package/dist/chunk-LPWEQCZV.cjs.map +0 -1
- package/dist/chunk-VDQZQDU3.cjs.map +0 -1
- package/dist/chunk-ZLASWV4N.js.map +0 -1
- package/dist/server-5N76YCWC.js.map +0 -1
- package/dist/server-QIYIJHD5.cjs.map +0 -1
package/dist/vite/index.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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",
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
//
|
|
250
|
-
|
|
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:
|
|
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
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
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 === "
|
|
275
|
-
else if (msg.method === "
|
|
276
|
-
else if (msg.method === "
|
|
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 = "/";
|
package/dist/vite/index.cjs.map
CHANGED
|
@@ -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"]}
|
package/dist/vite/index.d.cts
CHANGED
|
@@ -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
|
|
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
|
*
|
package/dist/vite/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
*
|
package/dist/vite/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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",
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
//
|
|
248
|
-
|
|
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:
|
|
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
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
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 === "
|
|
273
|
-
else if (msg.method === "
|
|
274
|
-
else if (msg.method === "
|
|
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 = "/";
|