@nimblebrain/synapse 0.17.1 → 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.
- package/README.md +20 -18
- package/dist/chunk-54W6IAHM.js +24 -0
- package/dist/chunk-54W6IAHM.js.map +1 -0
- package/dist/{chunk-UPDBDXQB.js → chunk-DRLVEHCD.js} +6 -13
- package/dist/chunk-DRLVEHCD.js.map +1 -0
- package/dist/chunk-NP7ROSH2.cjs +33 -0
- package/dist/chunk-NP7ROSH2.cjs.map +1 -0
- package/dist/{chunk-SRQNQ26I.js → chunk-OYLZ5OCK.js} +4 -4
- package/dist/chunk-OYLZ5OCK.js.map +1 -0
- package/dist/chunk-P5DBJFSQ.js +8459 -0
- package/dist/chunk-P5DBJFSQ.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +9 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-Q7SFCCGT.cjs +11 -0
- package/dist/chunk-Q7SFCCGT.cjs.map +1 -0
- package/dist/chunk-QXDCA7QW.cjs +8466 -0
- package/dist/chunk-QXDCA7QW.cjs.map +1 -0
- package/dist/{chunk-EHLQWCYV.cjs → chunk-RU64F5RS.cjs} +7 -7
- package/dist/chunk-RU64F5RS.cjs.map +1 -0
- package/dist/{chunk-TXJMGQYF.cjs → chunk-SZWHFTXV.cjs} +5 -13
- package/dist/chunk-SZWHFTXV.cjs.map +1 -0
- package/dist/codegen/cli.cjs +4 -4
- package/dist/codegen/cli.js +4 -4
- package/dist/codegen/index.cjs +1 -0
- package/dist/codegen/index.d.cts +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/codegen/index.js +1 -0
- package/dist/connect.iife.global.js +66 -43
- package/dist/{detect-Bf8Q_0dK.d.ts → detect-DBwc8a21.d.ts} +2 -2
- package/dist/{detect-BHYg26_d.d.cts → detect-DZMJsyvZ.d.cts} +2 -2
- package/dist/host/index.cjs +15 -14
- package/dist/host/index.d.cts +3 -3
- package/dist/host/index.d.ts +3 -3
- package/dist/host/index.js +3 -2
- package/dist/index.cjs +42 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -13
- package/dist/index.d.ts +40 -13
- package/dist/index.js +35 -3
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +12 -10
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +14 -5
- package/dist/react/index.d.ts +14 -5
- package/dist/react/index.js +7 -5
- package/dist/react/index.js.map +1 -1
- package/dist/schema-reader-GM4A4L44.js +4 -0
- package/dist/{schema-reader-776246MJ.js.map → schema-reader-GM4A4L44.js.map} +1 -1
- package/dist/{schema-reader-B6LTGBMJ.cjs → schema-reader-MWBRQ2TQ.cjs} +3 -2
- package/dist/{schema-reader-B6LTGBMJ.cjs.map → schema-reader-MWBRQ2TQ.cjs.map} +1 -1
- package/dist/{server-QIYIJHD5.cjs → server-LR6T6C44.cjs} +32 -20
- package/dist/server-LR6T6C44.cjs.map +1 -0
- package/dist/{server-5N76YCWC.js → server-UHPEE55X.js} +32 -21
- package/dist/server-UHPEE55X.js.map +1 -0
- package/dist/synapse-runtime.iife.global.js +66 -43
- package/dist/synapse-ui.iife.global.js +2 -2
- package/dist/{type-generator-UA3L4OIA.cjs → type-generator-YTPFWLVG.cjs} +3 -2
- package/dist/{type-generator-UA3L4OIA.cjs.map → type-generator-YTPFWLVG.cjs.map} +1 -1
- package/dist/type-generator-ZYSVFK3X.js +4 -0
- package/dist/{type-generator-MZ4X4DE5.js.map → type-generator-ZYSVFK3X.js.map} +1 -1
- package/dist/{types-uEO4VFJ2.d.ts → types-CG08aO82.d.cts} +14 -83
- package/dist/{types-uEO4VFJ2.d.cts → types-CG08aO82.d.ts} +14 -83
- package/dist/ui/base.cjs +1 -0
- package/dist/ui/base.cjs.map +1 -1
- package/dist/ui/base.js +1 -0
- package/dist/ui/base.js.map +1 -1
- package/dist/ui/index.cjs +314 -87
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +29 -2
- package/dist/ui/index.d.ts +29 -2
- package/dist/ui/index.js +315 -89
- package/dist/ui/index.js.map +1 -1
- package/dist/vite/index.cjs +74 -30
- 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 +74 -30
- package/dist/vite/index.js.map +1 -1
- package/dist/{writer-NDK4U6C5.cjs → writer-HDRCQSX2.cjs} +3 -3
- package/dist/{writer-NDK4U6C5.cjs.map → writer-HDRCQSX2.cjs.map} +1 -1
- package/dist/{writer-ZNBXYUYC.js → writer-QNDPMEYD.js} +3 -3
- package/dist/{writer-ZNBXYUYC.js.map → writer-QNDPMEYD.js.map} +1 -1
- package/package.json +8 -5
- package/dist/chunk-EHLQWCYV.cjs.map +0 -1
- package/dist/chunk-FDMRJZKT.cjs +0 -788
- package/dist/chunk-FDMRJZKT.cjs.map +0 -1
- package/dist/chunk-SRQNQ26I.js.map +0 -1
- package/dist/chunk-TXJMGQYF.cjs.map +0 -1
- package/dist/chunk-UPDBDXQB.js.map +0 -1
- package/dist/chunk-VYR6V47N.js +0 -781
- package/dist/chunk-VYR6V47N.js.map +0 -1
- package/dist/schema-reader-776246MJ.js +0 -3
- package/dist/server-5N76YCWC.js.map +0 -1
- package/dist/server-QIYIJHD5.cjs.map +0 -1
- package/dist/type-generator-MZ4X4DE5.js +0 -3
package/dist/vite/index.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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",
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
//
|
|
250
|
-
|
|
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:
|
|
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
|
-
//
|
|
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.
|
|
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
|
|
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 = "/";
|
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 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"]}
|
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,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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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",
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
//
|
|
248
|
-
|
|
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:
|
|
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
|
-
//
|
|
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.
|
|
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
|
|
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 = "/";
|