@bonnard/mcp-charts 0.1.3 → 0.2.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 +133 -17
- package/dist/bigquery.d.ts +1 -1
- package/dist/bigquery.js +1 -1
- package/dist/chunk-7MYIOIDH.js +17 -0
- package/dist/chunk-7MYIOIDH.js.map +1 -0
- package/dist/chunk-EZYN3JQ4.js +706 -0
- package/dist/chunk-EZYN3JQ4.js.map +1 -0
- package/dist/{chunk-I3HINKHN.js → chunk-IXRGLZX2.js} +98 -5
- package/dist/chunk-IXRGLZX2.js.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +512 -0
- package/dist/cli.js.map +1 -0
- package/dist/databricks.d.ts +1 -1
- package/dist/databricks.js +1 -1
- package/dist/duckdb.d.ts +1 -1
- package/dist/duckdb.js +1 -1
- package/dist/fixtures.d.ts +30 -0
- package/dist/fixtures.js +156 -0
- package/dist/fixtures.js.map +1 -0
- package/dist/index.d.ts +119 -35
- package/dist/index.js +260 -713
- package/dist/index.js.map +1 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.js +1 -1
- package/dist/snowflake.d.ts +1 -1
- package/dist/snowflake.js +1 -1
- package/dist/sql-BYkJnQOP.d.ts +35 -0
- package/dist/{types-6ALzXWjE.d.ts → types-DUKpye7m.d.ts} +63 -1
- package/package.json +8 -1
- package/dist/chunk-I3HINKHN.js.map +0 -1
package/dist/cli.js
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
WIDGET_HTML,
|
|
4
|
+
isChartSpec,
|
|
5
|
+
isDashboardSpec
|
|
6
|
+
} from "./chunk-7MYIOIDH.js";
|
|
7
|
+
|
|
8
|
+
// src/cli.ts
|
|
9
|
+
import { existsSync, readFileSync as readFileSync2, watch } from "fs";
|
|
10
|
+
import { spawn } from "child_process";
|
|
11
|
+
import { basename, dirname, resolve as resolvePath } from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { parseArgs } from "util";
|
|
14
|
+
|
|
15
|
+
// src/cli/load-spec.ts
|
|
16
|
+
import { readFileSync } from "fs";
|
|
17
|
+
var SpecLoadError = class extends Error {
|
|
18
|
+
};
|
|
19
|
+
function describe(value) {
|
|
20
|
+
if (value === null) return "null";
|
|
21
|
+
if (Array.isArray(value)) return "an array (a spec is an object; did you pass raw rows?)";
|
|
22
|
+
if (typeof value !== "object") return `a ${typeof value}`;
|
|
23
|
+
const keys = Object.keys(value);
|
|
24
|
+
return keys.length ? `an object with keys ${keys.slice(0, 8).join(", ")}` : "an empty object";
|
|
25
|
+
}
|
|
26
|
+
function coerceSpec(value, source) {
|
|
27
|
+
if (isDashboardSpec(value)) return value;
|
|
28
|
+
if (isChartSpec(value)) return value;
|
|
29
|
+
throw new SpecLoadError(
|
|
30
|
+
`${source} is not a ChartSpec or DashboardSpec: got ${describe(value)}. Expected the JSON a chart tool returns in structuredContent: a ChartSpec ({ chartType, data: [...], ... }) or a DashboardSpec ({ items: [...], ... }).`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
function parseSpec(raw, source) {
|
|
34
|
+
let value;
|
|
35
|
+
try {
|
|
36
|
+
value = JSON.parse(raw);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39
|
+
throw new SpecLoadError(`${source} is not valid JSON: ${message}`);
|
|
40
|
+
}
|
|
41
|
+
return coerceSpec(value, source);
|
|
42
|
+
}
|
|
43
|
+
function loadSpecFile(path) {
|
|
44
|
+
let raw;
|
|
45
|
+
try {
|
|
46
|
+
raw = readFileSync(path, "utf8");
|
|
47
|
+
} catch (err) {
|
|
48
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
49
|
+
throw new SpecLoadError(`cannot read ${path}: ${message}`);
|
|
50
|
+
}
|
|
51
|
+
return parseSpec(raw, path);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/cli/mcp-client.ts
|
|
55
|
+
var McpClientError = class extends Error {
|
|
56
|
+
};
|
|
57
|
+
function normalizeMcpUrl(input) {
|
|
58
|
+
let url;
|
|
59
|
+
try {
|
|
60
|
+
url = new URL(input);
|
|
61
|
+
} catch {
|
|
62
|
+
throw new McpClientError(`invalid --mcp URL "${input}" (expected e.g. http://localhost:3000/mcp)`);
|
|
63
|
+
}
|
|
64
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
65
|
+
throw new McpClientError(`--mcp URL must be http(s), got "${input}"`);
|
|
66
|
+
}
|
|
67
|
+
if (url.pathname === "" || url.pathname === "/") url.pathname = "/mcp";
|
|
68
|
+
return url.toString();
|
|
69
|
+
}
|
|
70
|
+
function parseSseJsonRpc(body, id) {
|
|
71
|
+
for (const event of body.split(/\r?\n\r?\n/)) {
|
|
72
|
+
const data = event.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
|
|
73
|
+
if (!data) continue;
|
|
74
|
+
let parsed;
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(data);
|
|
77
|
+
} catch {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
for (const msg of Array.isArray(parsed) ? parsed : [parsed]) {
|
|
81
|
+
const rpc = msg;
|
|
82
|
+
if (rpc && typeof rpc === "object" && rpc.id === id && ("result" in rpc || "error" in rpc)) return rpc;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return void 0;
|
|
86
|
+
}
|
|
87
|
+
async function post(url, message, session) {
|
|
88
|
+
const headers = {
|
|
89
|
+
"content-type": "application/json",
|
|
90
|
+
accept: "application/json, text/event-stream"
|
|
91
|
+
};
|
|
92
|
+
if (session.sessionId) headers["mcp-session-id"] = session.sessionId;
|
|
93
|
+
if (session.protocolVersion) headers["mcp-protocol-version"] = session.protocolVersion;
|
|
94
|
+
try {
|
|
95
|
+
return await fetch(url, { method: "POST", headers, body: JSON.stringify(message) });
|
|
96
|
+
} catch (err) {
|
|
97
|
+
const reason = err instanceof Error ? err.cause instanceof Error ? err.cause.message : err.message : String(err);
|
|
98
|
+
throw new McpClientError(`cannot reach MCP server at ${url}: ${reason}. Is your server running?`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function request(url, method, params, id, session) {
|
|
102
|
+
const res = await post(url, { jsonrpc: "2.0", id, method, params }, session);
|
|
103
|
+
const body = await res.text();
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
throw new McpClientError(`${method} failed: HTTP ${res.status}${body ? ` ${body.slice(0, 300)}` : ""}`);
|
|
106
|
+
}
|
|
107
|
+
const newSession = res.headers.get("mcp-session-id");
|
|
108
|
+
if (newSession) session.sessionId = newSession;
|
|
109
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
110
|
+
let rpc;
|
|
111
|
+
if (contentType.includes("text/event-stream")) {
|
|
112
|
+
rpc = parseSseJsonRpc(body, id);
|
|
113
|
+
} else {
|
|
114
|
+
let parsed;
|
|
115
|
+
try {
|
|
116
|
+
parsed = JSON.parse(body);
|
|
117
|
+
} catch {
|
|
118
|
+
throw new McpClientError(`${method} failed: server returned non-JSON (${contentType || "no content-type"})`);
|
|
119
|
+
}
|
|
120
|
+
rpc = (Array.isArray(parsed) ? parsed : [parsed]).find((m) => m.id === id);
|
|
121
|
+
}
|
|
122
|
+
if (!rpc) throw new McpClientError(`${method} failed: no JSON-RPC response for request id ${id} in server reply`);
|
|
123
|
+
if (rpc.error) throw new McpClientError(`${method} failed: ${rpc.error.message}`);
|
|
124
|
+
return rpc.result ?? {};
|
|
125
|
+
}
|
|
126
|
+
async function callMcpTool(url, tool, args = {}) {
|
|
127
|
+
const endpoint = normalizeMcpUrl(url);
|
|
128
|
+
const session = {};
|
|
129
|
+
const init = await request(
|
|
130
|
+
endpoint,
|
|
131
|
+
"initialize",
|
|
132
|
+
{
|
|
133
|
+
protocolVersion: "2025-06-18",
|
|
134
|
+
capabilities: {},
|
|
135
|
+
clientInfo: { name: "mcp-charts-preview", version: "0.0.0" }
|
|
136
|
+
},
|
|
137
|
+
0,
|
|
138
|
+
session
|
|
139
|
+
);
|
|
140
|
+
if (typeof init.protocolVersion === "string") session.protocolVersion = init.protocolVersion;
|
|
141
|
+
const notifyRes = await post(endpoint, { jsonrpc: "2.0", method: "notifications/initialized" }, session);
|
|
142
|
+
void notifyRes.body?.cancel().catch(() => {
|
|
143
|
+
});
|
|
144
|
+
const result = await request(endpoint, "tools/call", { name: tool, arguments: args }, 1, session);
|
|
145
|
+
const content = Array.isArray(result.content) ? result.content : [];
|
|
146
|
+
const text = content.find((c) => c.type === "text")?.text;
|
|
147
|
+
if (result.isError) {
|
|
148
|
+
throw new McpClientError(`tool "${tool}" returned an error: ${text ?? "(no error text)"}`);
|
|
149
|
+
}
|
|
150
|
+
const spec = coerceSpec(result.structuredContent, `tool "${tool}" structuredContent`);
|
|
151
|
+
return { spec, summary: text };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/cli/preview-server.ts
|
|
155
|
+
import { createServer } from "http";
|
|
156
|
+
async function startPreviewServer(opts) {
|
|
157
|
+
const widgetHtml = opts.widgetHtml ?? WIDGET_HTML;
|
|
158
|
+
const state = { version: 0, payload: opts.initialPayload ?? null, error: null };
|
|
159
|
+
let lastJson = state.payload ? JSON.stringify(state.payload) : "";
|
|
160
|
+
const sseClients = /* @__PURE__ */ new Set();
|
|
161
|
+
function broadcast() {
|
|
162
|
+
state.version += 1;
|
|
163
|
+
for (const res of sseClients) res.write(`event: render
|
|
164
|
+
data: {"version":${state.version}}
|
|
165
|
+
|
|
166
|
+
`);
|
|
167
|
+
}
|
|
168
|
+
function update(payload) {
|
|
169
|
+
const json = JSON.stringify(payload);
|
|
170
|
+
if (json === lastJson && state.error === null) return;
|
|
171
|
+
lastJson = json;
|
|
172
|
+
state.payload = payload;
|
|
173
|
+
state.error = null;
|
|
174
|
+
broadcast();
|
|
175
|
+
}
|
|
176
|
+
function fail(error) {
|
|
177
|
+
if (state.error === error) return;
|
|
178
|
+
state.error = error;
|
|
179
|
+
broadcast();
|
|
180
|
+
}
|
|
181
|
+
async function rerun() {
|
|
182
|
+
if (!opts.rerun) return;
|
|
183
|
+
try {
|
|
184
|
+
update(await opts.rerun());
|
|
185
|
+
} catch (err) {
|
|
186
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function send(res, status, contentType, body) {
|
|
190
|
+
res.writeHead(status, { "content-type": contentType, "cache-control": "no-store" });
|
|
191
|
+
res.end(body);
|
|
192
|
+
}
|
|
193
|
+
function handle(req, res) {
|
|
194
|
+
const path = new URL(req.url ?? "/", "http://localhost").pathname;
|
|
195
|
+
if (req.method === "GET" && path === "/") return send(res, 200, "text/html; charset=utf-8", opts.html);
|
|
196
|
+
if (req.method === "GET" && path === "/widget") return send(res, 200, "text/html; charset=utf-8", widgetHtml);
|
|
197
|
+
if (req.method === "GET" && path === "/spec") return send(res, 200, "application/json", JSON.stringify(state));
|
|
198
|
+
if (req.method === "GET" && path === "/events") {
|
|
199
|
+
res.writeHead(200, {
|
|
200
|
+
"content-type": "text/event-stream",
|
|
201
|
+
"cache-control": "no-store",
|
|
202
|
+
connection: "keep-alive"
|
|
203
|
+
});
|
|
204
|
+
res.write("retry: 1000\n\n");
|
|
205
|
+
sseClients.add(res);
|
|
206
|
+
res.on("close", () => sseClients.delete(res));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (req.method === "POST" && path === "/rerun") {
|
|
210
|
+
rerun().then(() => send(res, 200, "application/json", JSON.stringify(state))).catch(() => send(res, 500, "application/json", "{}"));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
send(res, 404, "text/plain", "Not found");
|
|
214
|
+
}
|
|
215
|
+
const server = createServer(handle);
|
|
216
|
+
const host = opts.host ?? "127.0.0.1";
|
|
217
|
+
await new Promise((resolve, reject) => {
|
|
218
|
+
server.once("error", reject);
|
|
219
|
+
server.listen(opts.port ?? 0, host, resolve);
|
|
220
|
+
});
|
|
221
|
+
const address = server.address();
|
|
222
|
+
const port = typeof address === "object" && address ? address.port : opts.port ?? 0;
|
|
223
|
+
return {
|
|
224
|
+
server,
|
|
225
|
+
port,
|
|
226
|
+
url: `http://${host}:${port}`,
|
|
227
|
+
state: () => state,
|
|
228
|
+
update,
|
|
229
|
+
fail,
|
|
230
|
+
close: () => new Promise((resolve, reject) => {
|
|
231
|
+
for (const res of sseClients) res.end();
|
|
232
|
+
server.closeAllConnections();
|
|
233
|
+
server.close((err) => err ? reject(err) : resolve());
|
|
234
|
+
})
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/cli/shell-html.ts
|
|
239
|
+
var esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
240
|
+
function renderShellHtml(opts) {
|
|
241
|
+
const config = JSON.stringify({ theme: opts.theme, mode: opts.mode }).replace(/</g, "\\u003c");
|
|
242
|
+
const rerunLabel = opts.mode === "mcp" ? "Re-run tool" : "Reload file";
|
|
243
|
+
return `<!doctype html>
|
|
244
|
+
<html lang="en" data-theme="${opts.theme}">
|
|
245
|
+
<head>
|
|
246
|
+
<meta charset="UTF-8" />
|
|
247
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
248
|
+
<title>mcp-charts preview</title>
|
|
249
|
+
<style>
|
|
250
|
+
:root { --bg: #f6f7f9; --fg: #1a1a1a; --muted: #6b7280; --border: #e5e7eb; --err-bg: #fef2f2; --err-fg: #b91c1c; }
|
|
251
|
+
html[data-theme="dark"] { --bg: #111418; --fg: #e5e7eb; --muted: #9ca3af; --border: #2d333b; --err-bg: #3a1d1d; --err-fg: #fca5a5; }
|
|
252
|
+
* { box-sizing: border-box; }
|
|
253
|
+
html, body { height: 100%; margin: 0; }
|
|
254
|
+
body { display: flex; flex-direction: column; background: var(--bg); color: var(--fg);
|
|
255
|
+
font: 13px/1.4 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
|
|
256
|
+
header { display: flex; align-items: center; gap: 12px; padding: 8px 12px; border-bottom: 1px solid var(--border); }
|
|
257
|
+
header .name { font-weight: 600; }
|
|
258
|
+
header .source { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
|
259
|
+
button { font: inherit; color: var(--fg); background: transparent; border: 1px solid var(--border);
|
|
260
|
+
border-radius: 6px; padding: 3px 10px; cursor: pointer; }
|
|
261
|
+
button[aria-pressed="true"] { background: var(--fg); color: var(--bg); }
|
|
262
|
+
#err { display: none; padding: 6px 12px; background: var(--err-bg); color: var(--err-fg);
|
|
263
|
+
border-bottom: 1px solid var(--border); white-space: pre-wrap; }
|
|
264
|
+
iframe { flex: 1; width: 100%; border: 0; background: #fff; }
|
|
265
|
+
html[data-theme="dark"] iframe { background: #111418; }
|
|
266
|
+
</style>
|
|
267
|
+
</head>
|
|
268
|
+
<body>
|
|
269
|
+
<header>
|
|
270
|
+
<span class="name">mcp-charts preview</span>
|
|
271
|
+
<span class="source" title="${esc(opts.source)}">${esc(opts.source)}</span>
|
|
272
|
+
<span id="theme">
|
|
273
|
+
<button data-theme-btn="light">Light</button>
|
|
274
|
+
<button data-theme-btn="dark">Dark</button>
|
|
275
|
+
</span>
|
|
276
|
+
<button id="rerun">${rerunLabel}</button>
|
|
277
|
+
</header>
|
|
278
|
+
<div id="err"></div>
|
|
279
|
+
<iframe id="stage" src="/widget#harness" title="chart widget"></iframe>
|
|
280
|
+
<script>
|
|
281
|
+
const cfg = ${config};
|
|
282
|
+
let theme = cfg.theme;
|
|
283
|
+
let payload = null;
|
|
284
|
+
const iframe = document.getElementById("stage");
|
|
285
|
+
const errEl = document.getElementById("err");
|
|
286
|
+
|
|
287
|
+
function feed() {
|
|
288
|
+
if (!payload) return;
|
|
289
|
+
iframe.contentWindow.postMessage(
|
|
290
|
+
{ type: "bonnard:harness-render", structuredContent: payload, theme },
|
|
291
|
+
"*",
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function refresh() {
|
|
296
|
+
try {
|
|
297
|
+
const state = await (await fetch("/spec")).json();
|
|
298
|
+
errEl.style.display = state.error ? "block" : "none";
|
|
299
|
+
errEl.textContent = state.error ?? "";
|
|
300
|
+
if (state.payload) {
|
|
301
|
+
payload = state.payload;
|
|
302
|
+
feed();
|
|
303
|
+
}
|
|
304
|
+
} catch (e) {
|
|
305
|
+
errEl.style.display = "block";
|
|
306
|
+
errEl.textContent = "preview server unreachable: " + e;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// The widget posts harness-ready when (re)loaded; the load event is a fallback for embeds
|
|
311
|
+
// that predate the handshake.
|
|
312
|
+
window.addEventListener("message", (e) => {
|
|
313
|
+
if (e.data && e.data.type === "bonnard:harness-ready") feed();
|
|
314
|
+
});
|
|
315
|
+
iframe.addEventListener("load", feed);
|
|
316
|
+
|
|
317
|
+
new EventSource("/events").addEventListener("render", refresh);
|
|
318
|
+
|
|
319
|
+
function setTheme(next) {
|
|
320
|
+
theme = next;
|
|
321
|
+
document.documentElement.dataset.theme = next;
|
|
322
|
+
document.querySelectorAll("[data-theme-btn]").forEach((b) => {
|
|
323
|
+
b.setAttribute("aria-pressed", String(b.dataset.themeBtn === next));
|
|
324
|
+
});
|
|
325
|
+
feed();
|
|
326
|
+
}
|
|
327
|
+
document.querySelectorAll("[data-theme-btn]").forEach((b) => {
|
|
328
|
+
b.addEventListener("click", () => setTheme(b.dataset.themeBtn));
|
|
329
|
+
});
|
|
330
|
+
setTheme(theme);
|
|
331
|
+
|
|
332
|
+
document.getElementById("rerun").addEventListener("click", () => {
|
|
333
|
+
fetch("/rerun", { method: "POST" }).catch(() => {});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
refresh();
|
|
337
|
+
</script>
|
|
338
|
+
</body>
|
|
339
|
+
</html>
|
|
340
|
+
`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/cli.ts
|
|
344
|
+
var DEFAULT_PORT = 4400;
|
|
345
|
+
var MCP_WATCH_INTERVAL_MS = 2e3;
|
|
346
|
+
var HELP = `mcp-charts - preview @bonnard/mcp-charts specs in the real chart widget
|
|
347
|
+
|
|
348
|
+
Usage:
|
|
349
|
+
mcp-charts preview <spec.json> [options]
|
|
350
|
+
Render a ChartSpec/DashboardSpec JSON file (e.g. a saved structuredContent).
|
|
351
|
+
|
|
352
|
+
mcp-charts preview --mcp <url> --tool <name> [--args '<json>'] [options]
|
|
353
|
+
Call a tool on your running Streamable HTTP MCP server and render the spec
|
|
354
|
+
it returns in structuredContent. --mcp accepts an origin (/mcp is appended)
|
|
355
|
+
or a full endpoint URL.
|
|
356
|
+
|
|
357
|
+
Options:
|
|
358
|
+
--watch File mode: re-render when the file changes.
|
|
359
|
+
MCP mode: re-run the tool every ${MCP_WATCH_INTERVAL_MS / 1e3}s (re-renders only on change).
|
|
360
|
+
--port <n> Preview server port (default ${DEFAULT_PORT}).
|
|
361
|
+
--theme <t> Initial widget theme: light | dark (default light; toggle in the UI).
|
|
362
|
+
--args <json> JSON object of tool arguments, e.g. '{"view_id":"sales_overview"}'.
|
|
363
|
+
--no-open Do not open the browser.
|
|
364
|
+
-h, --help Show this help.
|
|
365
|
+
-v, --version Show the package version.
|
|
366
|
+
|
|
367
|
+
Examples:
|
|
368
|
+
mcp-charts preview ./spec.json --watch
|
|
369
|
+
mcp-charts preview --mcp http://localhost:3000 --tool render_view --args '{"view_id":"exec_summary"}'
|
|
370
|
+
`;
|
|
371
|
+
var CliError = class extends Error {
|
|
372
|
+
};
|
|
373
|
+
function packageVersion() {
|
|
374
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
375
|
+
for (const rel of ["../package.json", "../../package.json"]) {
|
|
376
|
+
const path = resolvePath(here, rel);
|
|
377
|
+
if (!existsSync(path)) continue;
|
|
378
|
+
const pkg = JSON.parse(readFileSync2(path, "utf8"));
|
|
379
|
+
if (pkg.name === "@bonnard/mcp-charts" && pkg.version) return pkg.version;
|
|
380
|
+
}
|
|
381
|
+
return "unknown";
|
|
382
|
+
}
|
|
383
|
+
function openBrowser(url) {
|
|
384
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
385
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
386
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
387
|
+
child.on("error", () => {
|
|
388
|
+
});
|
|
389
|
+
child.unref();
|
|
390
|
+
}
|
|
391
|
+
function parseToolArgs(raw) {
|
|
392
|
+
if (raw === void 0) return {};
|
|
393
|
+
let parsed;
|
|
394
|
+
try {
|
|
395
|
+
parsed = JSON.parse(raw);
|
|
396
|
+
} catch (err) {
|
|
397
|
+
throw new CliError(`--args is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
398
|
+
}
|
|
399
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
400
|
+
throw new CliError(`--args must be a JSON object, e.g. '{"view_id":"sales_overview"}'`);
|
|
401
|
+
}
|
|
402
|
+
return parsed;
|
|
403
|
+
}
|
|
404
|
+
async function preview(positionals, values) {
|
|
405
|
+
const file = positionals[0];
|
|
406
|
+
const mcpUrl = values.mcp;
|
|
407
|
+
const tool = values.tool;
|
|
408
|
+
const theme = values.theme ?? "light";
|
|
409
|
+
const port = values.port !== void 0 ? Number(values.port) : DEFAULT_PORT;
|
|
410
|
+
const watchMode = values.watch === true;
|
|
411
|
+
if (theme !== "light" && theme !== "dark") throw new CliError(`--theme must be "light" or "dark", got "${theme}"`);
|
|
412
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new CliError(`--port must be 0-65535`);
|
|
413
|
+
if (file && mcpUrl) throw new CliError("pass a spec file OR --mcp, not both");
|
|
414
|
+
if (!file && !mcpUrl) throw new CliError(`pass a spec file or --mcp <url> --tool <name>. See mcp-charts --help.`);
|
|
415
|
+
if (mcpUrl && !tool) throw new CliError("--mcp requires --tool <name>");
|
|
416
|
+
if (!mcpUrl && (tool || values.args)) throw new CliError("--tool/--args only apply with --mcp <url>");
|
|
417
|
+
let source;
|
|
418
|
+
let rerun;
|
|
419
|
+
if (file) {
|
|
420
|
+
const path = resolvePath(file);
|
|
421
|
+
source = path;
|
|
422
|
+
rerun = () => Promise.resolve(loadSpecFile(path));
|
|
423
|
+
} else {
|
|
424
|
+
const args = parseToolArgs(values.args);
|
|
425
|
+
source = `${tool} on ${mcpUrl}`;
|
|
426
|
+
rerun = async () => (await callMcpTool(mcpUrl, tool, args)).spec;
|
|
427
|
+
}
|
|
428
|
+
const initial = await rerun();
|
|
429
|
+
const html = renderShellHtml({ source, mode: file ? "file" : "mcp", theme });
|
|
430
|
+
const srv = await startPreviewServer({ html, rerun, initialPayload: initial, port }).catch((err) => {
|
|
431
|
+
if (err.code === "EADDRINUSE") {
|
|
432
|
+
throw new CliError(`port ${port} is in use; pass --port <n> to pick another`);
|
|
433
|
+
}
|
|
434
|
+
throw err;
|
|
435
|
+
});
|
|
436
|
+
const refresh = async () => {
|
|
437
|
+
try {
|
|
438
|
+
srv.update(await rerun());
|
|
439
|
+
} catch (err) {
|
|
440
|
+
srv.fail(err instanceof Error ? err.message : String(err));
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
if (watchMode && file) {
|
|
444
|
+
const path = resolvePath(file);
|
|
445
|
+
let timer;
|
|
446
|
+
watch(dirname(path), (_event, filename) => {
|
|
447
|
+
if (filename && filename !== basename(path)) return;
|
|
448
|
+
clearTimeout(timer);
|
|
449
|
+
timer = setTimeout(() => void refresh(), 100);
|
|
450
|
+
});
|
|
451
|
+
} else if (watchMode) {
|
|
452
|
+
setInterval(() => void refresh(), MCP_WATCH_INTERVAL_MS).unref();
|
|
453
|
+
}
|
|
454
|
+
console.log(`mcp-charts preview: ${srv.url}`);
|
|
455
|
+
console.log(` source: ${source}${watchMode ? " (watching)" : ""}`);
|
|
456
|
+
console.log(` Ctrl-C to stop`);
|
|
457
|
+
if (values["no-open"] !== true) openBrowser(srv.url);
|
|
458
|
+
await new Promise((resolve) => {
|
|
459
|
+
const stop = () => void srv.close().finally(resolve);
|
|
460
|
+
process.once("SIGINT", stop);
|
|
461
|
+
process.once("SIGTERM", stop);
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
async function main(argv) {
|
|
465
|
+
let parsed;
|
|
466
|
+
try {
|
|
467
|
+
parsed = parseArgs({
|
|
468
|
+
args: argv,
|
|
469
|
+
allowPositionals: true,
|
|
470
|
+
options: {
|
|
471
|
+
mcp: { type: "string" },
|
|
472
|
+
tool: { type: "string" },
|
|
473
|
+
args: { type: "string" },
|
|
474
|
+
port: { type: "string" },
|
|
475
|
+
theme: { type: "string" },
|
|
476
|
+
watch: { type: "boolean" },
|
|
477
|
+
"no-open": { type: "boolean" },
|
|
478
|
+
help: { type: "boolean", short: "h" },
|
|
479
|
+
version: { type: "boolean", short: "v" }
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
} catch (err) {
|
|
483
|
+
throw new CliError(`${err instanceof Error ? err.message : String(err)}. See mcp-charts --help.`);
|
|
484
|
+
}
|
|
485
|
+
const { values, positionals } = parsed;
|
|
486
|
+
if (values.version) {
|
|
487
|
+
console.log(packageVersion());
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const [command, ...rest] = positionals;
|
|
491
|
+
if (values.help || !command) {
|
|
492
|
+
console.log(HELP);
|
|
493
|
+
if (!values.help && !command) process.exitCode = 1;
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (command !== "preview") {
|
|
497
|
+
throw new CliError(`unknown command "${command}". See mcp-charts --help.`);
|
|
498
|
+
}
|
|
499
|
+
await preview(rest, values);
|
|
500
|
+
}
|
|
501
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
502
|
+
if (err instanceof CliError || err instanceof SpecLoadError || err instanceof McpClientError) {
|
|
503
|
+
console.error(`mcp-charts: ${err.message}`);
|
|
504
|
+
} else {
|
|
505
|
+
console.error(err);
|
|
506
|
+
}
|
|
507
|
+
process.exit(1);
|
|
508
|
+
});
|
|
509
|
+
export {
|
|
510
|
+
main
|
|
511
|
+
};
|
|
512
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/cli/load-spec.ts","../src/cli/mcp-client.ts","../src/cli/preview-server.ts","../src/cli/shell-html.ts"],"sourcesContent":["#!/usr/bin/env node\n// The consumer-facing preview CLI (`mcp-charts`). A separate build entry from the library:\n// importing @bonnard/mcp-charts never loads this. Runtime deps are Node built-ins only.\n// Design + full surface: docs/PREVIEW-CLI.md.\nimport { existsSync, readFileSync, watch } from \"node:fs\";\nimport { spawn } from \"node:child_process\";\nimport { basename, dirname, resolve as resolvePath } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\nimport { loadSpecFile, SpecLoadError, type PreviewSpec } from \"./cli/load-spec.js\";\nimport { callMcpTool, McpClientError } from \"./cli/mcp-client.js\";\nimport { startPreviewServer } from \"./cli/preview-server.js\";\nimport { renderShellHtml } from \"./cli/shell-html.js\";\n\nconst DEFAULT_PORT = 4400;\nconst MCP_WATCH_INTERVAL_MS = 2000;\n\nconst HELP = `mcp-charts - preview @bonnard/mcp-charts specs in the real chart widget\n\nUsage:\n mcp-charts preview <spec.json> [options]\n Render a ChartSpec/DashboardSpec JSON file (e.g. a saved structuredContent).\n\n mcp-charts preview --mcp <url> --tool <name> [--args '<json>'] [options]\n Call a tool on your running Streamable HTTP MCP server and render the spec\n it returns in structuredContent. --mcp accepts an origin (/mcp is appended)\n or a full endpoint URL.\n\nOptions:\n --watch File mode: re-render when the file changes.\n MCP mode: re-run the tool every ${MCP_WATCH_INTERVAL_MS / 1000}s (re-renders only on change).\n --port <n> Preview server port (default ${DEFAULT_PORT}).\n --theme <t> Initial widget theme: light | dark (default light; toggle in the UI).\n --args <json> JSON object of tool arguments, e.g. '{\"view_id\":\"sales_overview\"}'.\n --no-open Do not open the browser.\n -h, --help Show this help.\n -v, --version Show the package version.\n\nExamples:\n mcp-charts preview ./spec.json --watch\n mcp-charts preview --mcp http://localhost:3000 --tool render_view --args '{\"view_id\":\"exec_summary\"}'\n`;\n\nclass CliError extends Error {}\n\nfunction packageVersion(): string {\n const here = dirname(fileURLToPath(import.meta.url));\n for (const rel of [\"../package.json\", \"../../package.json\"]) {\n const path = resolvePath(here, rel);\n if (!existsSync(path)) continue;\n const pkg = JSON.parse(readFileSync(path, \"utf8\")) as { name?: string; version?: string };\n if (pkg.name === \"@bonnard/mcp-charts\" && pkg.version) return pkg.version;\n }\n return \"unknown\";\n}\n\nfunction openBrowser(url: string): void {\n const cmd = process.platform === \"darwin\" ? \"open\" : process.platform === \"win32\" ? \"cmd\" : \"xdg-open\";\n const args = process.platform === \"win32\" ? [\"/c\", \"start\", \"\", url] : [url];\n const child = spawn(cmd, args, { stdio: \"ignore\", detached: true });\n child.on(\"error\", () => {});\n child.unref();\n}\n\nfunction parseToolArgs(raw: string | undefined): Record<string, unknown> {\n if (raw === undefined) return {};\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new CliError(`--args is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new CliError(`--args must be a JSON object, e.g. '{\"view_id\":\"sales_overview\"}'`);\n }\n return parsed as Record<string, unknown>;\n}\n\nasync function preview(positionals: string[], values: Record<string, unknown>): Promise<void> {\n const file = positionals[0];\n const mcpUrl = values.mcp as string | undefined;\n const tool = values.tool as string | undefined;\n const theme = (values.theme as string | undefined) ?? \"light\";\n const port = values.port !== undefined ? Number(values.port) : DEFAULT_PORT;\n const watchMode = values.watch === true;\n\n if (theme !== \"light\" && theme !== \"dark\") throw new CliError(`--theme must be \"light\" or \"dark\", got \"${theme}\"`);\n if (!Number.isInteger(port) || port < 0 || port > 65535) throw new CliError(`--port must be 0-65535`);\n if (file && mcpUrl) throw new CliError(\"pass a spec file OR --mcp, not both\");\n if (!file && !mcpUrl) throw new CliError(`pass a spec file or --mcp <url> --tool <name>. See mcp-charts --help.`);\n if (mcpUrl && !tool) throw new CliError(\"--mcp requires --tool <name>\");\n if (!mcpUrl && (tool || values.args)) throw new CliError(\"--tool/--args only apply with --mcp <url>\");\n\n let source: string;\n let rerun: () => Promise<PreviewSpec>;\n if (file) {\n const path = resolvePath(file);\n source = path;\n rerun = () => Promise.resolve(loadSpecFile(path));\n } else {\n const args = parseToolArgs(values.args as string | undefined);\n source = `${tool} on ${mcpUrl}`;\n rerun = async () => (await callMcpTool(mcpUrl!, tool!, args)).spec;\n }\n\n // Fail fast, before the server boots: a bad file / unreachable server is a CLI error, not\n // something to discover in the browser.\n const initial = await rerun();\n\n const html = renderShellHtml({ source, mode: file ? \"file\" : \"mcp\", theme });\n const srv = await startPreviewServer({ html, rerun, initialPayload: initial, port }).catch((err: unknown) => {\n if ((err as NodeJS.ErrnoException).code === \"EADDRINUSE\") {\n throw new CliError(`port ${port} is in use; pass --port <n> to pick another`);\n }\n throw err;\n });\n\n const refresh = async () => {\n try {\n srv.update(await rerun());\n } catch (err) {\n srv.fail(err instanceof Error ? err.message : String(err));\n }\n };\n\n if (watchMode && file) {\n // Watch the directory, not the file: editors that replace-on-save break a file watcher.\n const path = resolvePath(file);\n let timer: NodeJS.Timeout | undefined;\n watch(dirname(path), (_event, filename) => {\n if (filename && filename !== basename(path)) return;\n clearTimeout(timer);\n timer = setTimeout(() => void refresh(), 100);\n });\n } else if (watchMode) {\n setInterval(() => void refresh(), MCP_WATCH_INTERVAL_MS).unref();\n }\n\n console.log(`mcp-charts preview: ${srv.url}`);\n console.log(` source: ${source}${watchMode ? \" (watching)\" : \"\"}`);\n console.log(` Ctrl-C to stop`);\n if (values[\"no-open\"] !== true) openBrowser(srv.url);\n\n // Keep the process alive on the server; resolve only on signal so Ctrl-C exits cleanly.\n await new Promise<void>((resolve) => {\n const stop = () => void srv.close().finally(resolve);\n process.once(\"SIGINT\", stop);\n process.once(\"SIGTERM\", stop);\n });\n}\n\nexport async function main(argv: string[]): Promise<void> {\n let parsed: ReturnType<typeof parseArgs>;\n try {\n parsed = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n mcp: { type: \"string\" },\n tool: { type: \"string\" },\n args: { type: \"string\" },\n port: { type: \"string\" },\n theme: { type: \"string\" },\n watch: { type: \"boolean\" },\n \"no-open\": { type: \"boolean\" },\n help: { type: \"boolean\", short: \"h\" },\n version: { type: \"boolean\", short: \"v\" },\n },\n });\n } catch (err) {\n throw new CliError(`${err instanceof Error ? err.message : String(err)}. See mcp-charts --help.`);\n }\n const { values, positionals } = parsed;\n\n if (values.version) {\n console.log(packageVersion());\n return;\n }\n const [command, ...rest] = positionals;\n if (values.help || !command) {\n console.log(HELP);\n if (!values.help && !command) process.exitCode = 1;\n return;\n }\n if (command !== \"preview\") {\n throw new CliError(`unknown command \"${command}\". See mcp-charts --help.`);\n }\n await preview(rest, values);\n}\n\nmain(process.argv.slice(2)).catch((err: unknown) => {\n if (err instanceof CliError || err instanceof SpecLoadError || err instanceof McpClientError) {\n console.error(`mcp-charts: ${err.message}`);\n } else {\n console.error(err);\n }\n process.exit(1);\n});\n","// Spec loading for the preview CLI: read a JSON file (or an arbitrary value) and narrow it to a\n// ChartSpec/DashboardSpec with the same guards the widget uses, failing with actionable messages.\nimport { readFileSync } from \"node:fs\";\nimport { isChartSpec, isDashboardSpec } from \"../dashboard.js\";\nimport type { ChartSpec, DashboardSpec } from \"../types.js\";\n\nexport type PreviewSpec = ChartSpec | DashboardSpec;\n\n/** A user-facing load/validation failure; the CLI prints `message` and exits 1. */\nexport class SpecLoadError extends Error {}\n\nfunction describe(value: unknown): string {\n if (value === null) return \"null\";\n if (Array.isArray(value)) return \"an array (a spec is an object; did you pass raw rows?)\";\n if (typeof value !== \"object\") return `a ${typeof value}`;\n const keys = Object.keys(value);\n return keys.length ? `an object with keys ${keys.slice(0, 8).join(\", \")}` : \"an empty object\";\n}\n\n/** Narrow an arbitrary value to a spec, or throw a SpecLoadError naming what was found. */\nexport function coerceSpec(value: unknown, source: string): PreviewSpec {\n if (isDashboardSpec(value)) return value;\n if (isChartSpec(value)) return value;\n throw new SpecLoadError(\n `${source} is not a ChartSpec or DashboardSpec: got ${describe(value)}. ` +\n `Expected the JSON a chart tool returns in structuredContent: a ChartSpec ` +\n `({ chartType, data: [...], ... }) or a DashboardSpec ({ items: [...], ... }).`,\n );\n}\n\n/** Parse a JSON string into a spec. */\nexport function parseSpec(raw: string, source: string): PreviewSpec {\n let value: unknown;\n try {\n value = JSON.parse(raw);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new SpecLoadError(`${source} is not valid JSON: ${message}`);\n }\n return coerceSpec(value, source);\n}\n\n/** Read + parse + validate a spec file. */\nexport function loadSpecFile(path: string): PreviewSpec {\n let raw: string;\n try {\n raw = readFileSync(path, \"utf8\");\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new SpecLoadError(`cannot read ${path}: ${message}`);\n }\n return parseSpec(raw, path);\n}\n","// Minimal Streamable HTTP MCP client for the preview CLI: initialize -> initialized -> tools/call\n// over plain fetch, zero deps. Handles both application/json and text/event-stream response\n// framing, and echoes mcp-session-id for sessioned servers (stateless servers issue none).\nimport { coerceSpec, type PreviewSpec } from \"./load-spec.js\";\n\n/** A user-facing MCP failure; the CLI prints `message` (and the preview error bar shows it). */\nexport class McpClientError extends Error {}\n\ninterface JsonRpcResponse {\n jsonrpc: \"2.0\";\n id?: number | string | null;\n result?: Record<string, unknown>;\n error?: { code: number; message: string };\n}\n\ninterface ToolContent {\n type: string;\n text?: string;\n}\n\n/** Accept a bare origin (append /mcp) or a full endpoint URL. */\nexport function normalizeMcpUrl(input: string): string {\n let url: URL;\n try {\n url = new URL(input);\n } catch {\n throw new McpClientError(`invalid --mcp URL \"${input}\" (expected e.g. http://localhost:3000/mcp)`);\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new McpClientError(`--mcp URL must be http(s), got \"${input}\"`);\n }\n if (url.pathname === \"\" || url.pathname === \"/\") url.pathname = \"/mcp\";\n return url.toString();\n}\n\n/** Pull the JSON-RPC response with a matching id out of an SSE body (data: lines per event). */\nexport function parseSseJsonRpc(body: string, id: number | string): JsonRpcResponse | undefined {\n for (const event of body.split(/\\r?\\n\\r?\\n/)) {\n const data = event\n .split(/\\r?\\n/)\n .filter((line) => line.startsWith(\"data:\"))\n .map((line) => line.slice(5).replace(/^ /, \"\"))\n .join(\"\\n\");\n if (!data) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(data);\n } catch {\n continue;\n }\n for (const msg of Array.isArray(parsed) ? parsed : [parsed]) {\n const rpc = msg as JsonRpcResponse;\n if (rpc && typeof rpc === \"object\" && rpc.id === id && (\"result\" in rpc || \"error\" in rpc)) return rpc;\n }\n }\n return undefined;\n}\n\ninterface Session {\n sessionId?: string;\n protocolVersion?: string;\n}\n\nasync function post(url: string, message: unknown, session: Session): Promise<Response> {\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n accept: \"application/json, text/event-stream\",\n };\n if (session.sessionId) headers[\"mcp-session-id\"] = session.sessionId;\n if (session.protocolVersion) headers[\"mcp-protocol-version\"] = session.protocolVersion;\n try {\n return await fetch(url, { method: \"POST\", headers, body: JSON.stringify(message) });\n } catch (err) {\n const reason = err instanceof Error ? (err.cause instanceof Error ? err.cause.message : err.message) : String(err);\n throw new McpClientError(`cannot reach MCP server at ${url}: ${reason}. Is your server running?`);\n }\n}\n\nasync function request(\n url: string,\n method: string,\n params: unknown,\n id: number,\n session: Session,\n): Promise<Record<string, unknown>> {\n const res = await post(url, { jsonrpc: \"2.0\", id, method, params }, session);\n const body = await res.text();\n if (!res.ok) {\n throw new McpClientError(`${method} failed: HTTP ${res.status}${body ? ` ${body.slice(0, 300)}` : \"\"}`);\n }\n const newSession = res.headers.get(\"mcp-session-id\");\n if (newSession) session.sessionId = newSession;\n\n const contentType = res.headers.get(\"content-type\") ?? \"\";\n let rpc: JsonRpcResponse | undefined;\n if (contentType.includes(\"text/event-stream\")) {\n rpc = parseSseJsonRpc(body, id);\n } else {\n let parsed: unknown;\n try {\n parsed = JSON.parse(body);\n } catch {\n throw new McpClientError(`${method} failed: server returned non-JSON (${contentType || \"no content-type\"})`);\n }\n rpc = (Array.isArray(parsed) ? parsed : [parsed]).find((m) => (m as JsonRpcResponse).id === id) as\n | JsonRpcResponse\n | undefined;\n }\n if (!rpc) throw new McpClientError(`${method} failed: no JSON-RPC response for request id ${id} in server reply`);\n if (rpc.error) throw new McpClientError(`${method} failed: ${rpc.error.message}`);\n return rpc.result ?? {};\n}\n\nexport interface McpToolCallResult {\n spec: PreviewSpec;\n /** The text-content summary the tool returned alongside the spec, if any. */\n summary?: string;\n}\n\n/**\n * Call one tool on a Streamable HTTP MCP server and extract the ChartSpec/DashboardSpec from\n * structuredContent. Runs the full handshake per call: stateless servers (the common consumer\n * shape, e.g. examples/dashboard) require nothing less, and sessioned servers get a fresh session.\n */\nexport async function callMcpTool(\n url: string,\n tool: string,\n args: Record<string, unknown> = {},\n): Promise<McpToolCallResult> {\n const endpoint = normalizeMcpUrl(url);\n const session: Session = {};\n\n const init = await request(\n endpoint,\n \"initialize\",\n {\n protocolVersion: \"2025-06-18\",\n capabilities: {},\n clientInfo: { name: \"mcp-charts-preview\", version: \"0.0.0\" },\n },\n 0,\n session,\n );\n if (typeof init.protocolVersion === \"string\") session.protocolVersion = init.protocolVersion;\n\n const notifyRes = await post(endpoint, { jsonrpc: \"2.0\", method: \"notifications/initialized\" }, session);\n void notifyRes.body?.cancel().catch(() => {});\n\n const result = await request(endpoint, \"tools/call\", { name: tool, arguments: args }, 1, session);\n const content = Array.isArray(result.content) ? (result.content as ToolContent[]) : [];\n const text = content.find((c) => c.type === \"text\")?.text;\n if (result.isError) {\n throw new McpClientError(`tool \"${tool}\" returned an error: ${text ?? \"(no error text)\"}`);\n }\n const spec = coerceSpec(result.structuredContent, `tool \"${tool}\" structuredContent`);\n return { spec, summary: text };\n}\n","// The preview server: node:http only, no framework. Serves the shell page, the embedded\n// production widget (at /widget, loaded by the shell's iframe with #harness), the current spec\n// as JSON, an SSE channel that announces spec-version bumps, and a POST /rerun that re-invokes\n// the spec source (file re-read or MCP tool re-call).\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\";\nimport type { PreviewSpec } from \"./load-spec.js\";\nimport { WIDGET_HTML } from \"../generated/widget-html.js\";\n\nexport interface PreviewState {\n version: number;\n payload: PreviewSpec | null;\n /** Last load/call failure; the shell shows it and keeps the last good render. */\n error: string | null;\n}\n\nexport interface PreviewServerOptions {\n /** The shell page served at \"/\". */\n html: string;\n /** Re-invoke the spec source; called on POST /rerun. */\n rerun?: () => Promise<PreviewSpec>;\n initialPayload?: PreviewSpec;\n port?: number;\n host?: string;\n /** Override the served widget (tests); defaults to the embedded production widget. */\n widgetHtml?: string;\n}\n\nexport interface PreviewServer {\n server: Server;\n port: number;\n url: string;\n state(): PreviewState;\n /** Set a new payload; no-op (no version bump, no SSE ping) when the spec is unchanged. */\n update(payload: PreviewSpec): void;\n /** Record a failure; keeps the last good payload so the chart stays up. */\n fail(error: string): void;\n close(): Promise<void>;\n}\n\nexport async function startPreviewServer(opts: PreviewServerOptions): Promise<PreviewServer> {\n const widgetHtml = opts.widgetHtml ?? WIDGET_HTML;\n const state: PreviewState = { version: 0, payload: opts.initialPayload ?? null, error: null };\n let lastJson = state.payload ? JSON.stringify(state.payload) : \"\";\n const sseClients = new Set<ServerResponse>();\n\n function broadcast(): void {\n state.version += 1;\n for (const res of sseClients) res.write(`event: render\\ndata: {\"version\":${state.version}}\\n\\n`);\n }\n\n function update(payload: PreviewSpec): void {\n const json = JSON.stringify(payload);\n if (json === lastJson && state.error === null) return;\n lastJson = json;\n state.payload = payload;\n state.error = null;\n broadcast();\n }\n\n function fail(error: string): void {\n if (state.error === error) return;\n state.error = error;\n broadcast();\n }\n\n async function rerun(): Promise<void> {\n if (!opts.rerun) return;\n try {\n update(await opts.rerun());\n } catch (err) {\n fail(err instanceof Error ? err.message : String(err));\n }\n }\n\n function send(res: ServerResponse, status: number, contentType: string, body: string): void {\n res.writeHead(status, { \"content-type\": contentType, \"cache-control\": \"no-store\" });\n res.end(body);\n }\n\n function handle(req: IncomingMessage, res: ServerResponse): void {\n const path = new URL(req.url ?? \"/\", \"http://localhost\").pathname;\n if (req.method === \"GET\" && path === \"/\") return send(res, 200, \"text/html; charset=utf-8\", opts.html);\n if (req.method === \"GET\" && path === \"/widget\") return send(res, 200, \"text/html; charset=utf-8\", widgetHtml);\n if (req.method === \"GET\" && path === \"/spec\") return send(res, 200, \"application/json\", JSON.stringify(state));\n if (req.method === \"GET\" && path === \"/events\") {\n res.writeHead(200, {\n \"content-type\": \"text/event-stream\",\n \"cache-control\": \"no-store\",\n connection: \"keep-alive\",\n });\n res.write(\"retry: 1000\\n\\n\");\n sseClients.add(res);\n res.on(\"close\", () => sseClients.delete(res));\n return;\n }\n if (req.method === \"POST\" && path === \"/rerun\") {\n rerun()\n .then(() => send(res, 200, \"application/json\", JSON.stringify(state)))\n .catch(() => send(res, 500, \"application/json\", \"{}\"));\n return;\n }\n send(res, 404, \"text/plain\", \"Not found\");\n }\n\n const server = createServer(handle);\n const host = opts.host ?? \"127.0.0.1\";\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject);\n server.listen(opts.port ?? 0, host, resolve);\n });\n const address = server.address();\n const port = typeof address === \"object\" && address ? address.port : (opts.port ?? 0);\n\n return {\n server,\n port,\n url: `http://${host}:${port}`,\n state: () => state,\n update,\n fail,\n close: () =>\n new Promise<void>((resolve, reject) => {\n for (const res of sseClients) res.end();\n server.closeAllConnections();\n server.close((err) => (err ? reject(err) : resolve()));\n }),\n };\n}\n","// The preview shell page: a control bar + the REAL embedded widget in an iframe at /widget#harness,\n// fed specs over the widget's harness postMessage protocol (the same one the internal dev harness\n// speaks). The spec itself travels via fetch(\"/spec\"), not inlined here, so watch/re-run is just a\n// version bump announced over SSE.\n\nexport interface ShellHtmlOptions {\n /** Shown in the control bar: the spec file path or \"tool on url\". */\n source: string;\n mode: \"file\" | \"mcp\";\n theme: \"light\" | \"dark\";\n}\n\nconst esc = (s: string) => s.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\");\n\nexport function renderShellHtml(opts: ShellHtmlOptions): string {\n const config = JSON.stringify({ theme: opts.theme, mode: opts.mode }).replace(/</g, \"\\\\u003c\");\n const rerunLabel = opts.mode === \"mcp\" ? \"Re-run tool\" : \"Reload file\";\n return `<!doctype html>\n<html lang=\"en\" data-theme=\"${opts.theme}\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>mcp-charts preview</title>\n <style>\n :root { --bg: #f6f7f9; --fg: #1a1a1a; --muted: #6b7280; --border: #e5e7eb; --err-bg: #fef2f2; --err-fg: #b91c1c; }\n html[data-theme=\"dark\"] { --bg: #111418; --fg: #e5e7eb; --muted: #9ca3af; --border: #2d333b; --err-bg: #3a1d1d; --err-fg: #fca5a5; }\n * { box-sizing: border-box; }\n html, body { height: 100%; margin: 0; }\n body { display: flex; flex-direction: column; background: var(--bg); color: var(--fg);\n font: 13px/1.4 ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif; }\n header { display: flex; align-items: center; gap: 12px; padding: 8px 12px; border-bottom: 1px solid var(--border); }\n header .name { font-weight: 600; }\n header .source { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }\n button { font: inherit; color: var(--fg); background: transparent; border: 1px solid var(--border);\n border-radius: 6px; padding: 3px 10px; cursor: pointer; }\n button[aria-pressed=\"true\"] { background: var(--fg); color: var(--bg); }\n #err { display: none; padding: 6px 12px; background: var(--err-bg); color: var(--err-fg);\n border-bottom: 1px solid var(--border); white-space: pre-wrap; }\n iframe { flex: 1; width: 100%; border: 0; background: #fff; }\n html[data-theme=\"dark\"] iframe { background: #111418; }\n </style>\n </head>\n <body>\n <header>\n <span class=\"name\">mcp-charts preview</span>\n <span class=\"source\" title=\"${esc(opts.source)}\">${esc(opts.source)}</span>\n <span id=\"theme\">\n <button data-theme-btn=\"light\">Light</button>\n <button data-theme-btn=\"dark\">Dark</button>\n </span>\n <button id=\"rerun\">${rerunLabel}</button>\n </header>\n <div id=\"err\"></div>\n <iframe id=\"stage\" src=\"/widget#harness\" title=\"chart widget\"></iframe>\n <script>\n const cfg = ${config};\n let theme = cfg.theme;\n let payload = null;\n const iframe = document.getElementById(\"stage\");\n const errEl = document.getElementById(\"err\");\n\n function feed() {\n if (!payload) return;\n iframe.contentWindow.postMessage(\n { type: \"bonnard:harness-render\", structuredContent: payload, theme },\n \"*\",\n );\n }\n\n async function refresh() {\n try {\n const state = await (await fetch(\"/spec\")).json();\n errEl.style.display = state.error ? \"block\" : \"none\";\n errEl.textContent = state.error ?? \"\";\n if (state.payload) {\n payload = state.payload;\n feed();\n }\n } catch (e) {\n errEl.style.display = \"block\";\n errEl.textContent = \"preview server unreachable: \" + e;\n }\n }\n\n // The widget posts harness-ready when (re)loaded; the load event is a fallback for embeds\n // that predate the handshake.\n window.addEventListener(\"message\", (e) => {\n if (e.data && e.data.type === \"bonnard:harness-ready\") feed();\n });\n iframe.addEventListener(\"load\", feed);\n\n new EventSource(\"/events\").addEventListener(\"render\", refresh);\n\n function setTheme(next) {\n theme = next;\n document.documentElement.dataset.theme = next;\n document.querySelectorAll(\"[data-theme-btn]\").forEach((b) => {\n b.setAttribute(\"aria-pressed\", String(b.dataset.themeBtn === next));\n });\n feed();\n }\n document.querySelectorAll(\"[data-theme-btn]\").forEach((b) => {\n b.addEventListener(\"click\", () => setTheme(b.dataset.themeBtn));\n });\n setTheme(theme);\n\n document.getElementById(\"rerun\").addEventListener(\"click\", () => {\n fetch(\"/rerun\", { method: \"POST\" }).catch(() => {});\n });\n\n refresh();\n </script>\n </body>\n</html>\n`;\n}\n"],"mappings":"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","names":["readFileSync","readFileSync"]}
|
package/dist/databricks.d.ts
CHANGED
package/dist/databricks.js
CHANGED
package/dist/duckdb.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DuckDBType, DuckDBConnection } from '@duckdb/node-api';
|
|
2
|
-
import { C as ChartData } from './types-
|
|
2
|
+
import { C as ChartData } from './types-DUKpye7m.js';
|
|
3
3
|
|
|
4
4
|
/** A DuckDB result column: its name + the DuckDBType (from reader.columnTypes()). */
|
|
5
5
|
interface DuckDbColumn {
|
package/dist/duckdb.js
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { S as SourceColumn } from './sql-BYkJnQOP.js';
|
|
2
|
+
import { R as ResolveOptions, K as KpiTile, T as TextBlock, D as DashboardSpec, a as ChartSpec } from './types-DUKpye7m.js';
|
|
3
|
+
|
|
4
|
+
/** Raw inputs for a chart cell: a SQL-shaped result (rows + typed columns) + resolve options. */
|
|
5
|
+
interface ChartCellInput {
|
|
6
|
+
kind: "chart";
|
|
7
|
+
rows: Record<string, unknown>[];
|
|
8
|
+
columns: SourceColumn[];
|
|
9
|
+
opts: ResolveOptions;
|
|
10
|
+
span?: number;
|
|
11
|
+
}
|
|
12
|
+
/** A dashboard-item input: either a chart to be built, or a final KPI/text item. */
|
|
13
|
+
type DashboardItemInput = ChartCellInput | KpiTile | TextBlock;
|
|
14
|
+
/** Inputs from which a DashboardSpec is (re)producible without hand-authoring chart specs. */
|
|
15
|
+
interface DashboardFixtureInputs {
|
|
16
|
+
title?: string;
|
|
17
|
+
columns?: number;
|
|
18
|
+
items: DashboardItemInput[];
|
|
19
|
+
notes?: string[];
|
|
20
|
+
}
|
|
21
|
+
interface DashboardFixture {
|
|
22
|
+
name: string;
|
|
23
|
+
inputs: DashboardFixtureInputs;
|
|
24
|
+
spec: DashboardSpec;
|
|
25
|
+
}
|
|
26
|
+
/** Build a ChartCell's ChartSpec through the library path: buildChartData -> resolve. */
|
|
27
|
+
declare function buildCellSpec(input: ChartCellInput): ChartSpec;
|
|
28
|
+
declare const dashboardFixtures: DashboardFixture[];
|
|
29
|
+
|
|
30
|
+
export { type ChartCellInput, type DashboardFixture, type DashboardFixtureInputs, type DashboardItemInput, buildCellSpec, dashboardFixtures };
|