@bonnard/mcp-charts 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Bonnard (bonnard-data)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # @bonnard/mcp-charts
2
+
3
+ Add agent-ready, interactive charts to your MCP server in a few lines. `addCharts` registers a
4
+ `visualize` tool plus an embedded chart widget; your agent writes SQL, you run it, and the result
5
+ renders as an interactive chart inside the MCP host (Claude, ChatGPT, and other MCP Apps clients).
6
+
7
+ ```bash
8
+ npm install @bonnard/mcp-charts
9
+ ```
10
+
11
+ ```ts
12
+ import { addCharts } from "@bonnard/mcp-charts";
13
+
14
+ addCharts(server, {
15
+ // your read-only query callback — return { rows } (and optionally typed `fields`)
16
+ runSql: async (sql) => ({ rows: await db.query(sql) }),
17
+ discovery: { toolName: "explore_schema" }, // your schema-discovery tool
18
+ });
19
+ ```
20
+
21
+ You own the database connection; the SDK only turns the rows into a chart. Bring your own SQL, or
22
+ use a bundled warehouse adapter (BigQuery, Postgres, DuckDB, Snowflake, Databricks):
23
+
24
+ ```ts
25
+ import { postgresRunSql } from "@bonnard/mcp-charts/postgres";
26
+ addCharts(server, { runSql: postgresRunSql(pool) });
27
+ ```
28
+
29
+ ## Read-only access
30
+
31
+ The agent writes the SQL, so run it through a **read-only database role** — that is the security
32
+ boundary. The bundled adapters also apply a lightweight SELECT-only check (`assertReadOnlySql`), but
33
+ treat that as a guardrail, not a substitute for least-privilege credentials.
34
+
35
+ Full docs and examples: https://github.com/bonnard-data/mcp-charts
36
+
37
+ ## License
38
+
39
+ MIT
@@ -0,0 +1,37 @@
1
+ import { TableField, BigQuery } from '@google-cloud/bigquery';
2
+ import { C as ChartData } from './types-CHiUU9mj.js';
3
+
4
+ /** A BigQuery schema field. Anchored to the client's own `TableField` type so it tracks the driver:
5
+ * if @google-cloud/bigquery changes the schema shape, this and its consumers fail to compile.
6
+ * Relevant fields: `type` (INT64/FLOAT64/NUMERIC/BOOL/STRING/DATE/DATETIME/TIMESTAMP/TIME/RECORD/…),
7
+ * `mode` (NULLABLE | REQUIRED | REPEATED, where REPEATED is a non-scalar ARRAY). */
8
+ type BigQueryField = TableField;
9
+ /**
10
+ * Convert a BigQuery query result into ChartData.
11
+ * @param rows Result rows (record objects), as returned by `@google-cloud/bigquery` or the REST API.
12
+ * @param schema The result's `schema.fields` (BigQuery's standard TableFieldSchema list).
13
+ */
14
+ declare function bigQueryToChartData(rows: Record<string, unknown>[], schema: BigQueryField[]): ChartData;
15
+
16
+ interface BigQueryRunSqlOptions {
17
+ /** BigQuery processing location (must match the dataset region), e.g. "europe-west2". */
18
+ location?: string;
19
+ /** Cost guard: max bytes a single query may bill (BigQuery's string API shape). */
20
+ maximumBytesBilled?: string;
21
+ /** Per-query timeout in milliseconds. */
22
+ jobTimeoutMs?: number;
23
+ /** Default dataset so the agent can write unqualified table names (e.g. `weekly_sales`). */
24
+ defaultDataset?: string | {
25
+ datasetId: string;
26
+ projectId?: string;
27
+ };
28
+ /** Reject non-SELECT (DML/DDL) statements. Default: true. A read-only IAM role is the real boundary. */
29
+ readOnly?: boolean;
30
+ }
31
+ /**
32
+ * Build the `runSql` callback for `addCharts` from a BigQuery client.
33
+ * Runs the query with the configured guards and returns schema-typed ChartData.
34
+ */
35
+ declare function bigQueryRunSql(bq: BigQuery, opts?: BigQueryRunSqlOptions): (sql: string) => Promise<ChartData>;
36
+
37
+ export { type BigQueryField, type BigQueryRunSqlOptions, bigQueryRunSql, bigQueryToChartData };
@@ -0,0 +1,74 @@
1
+ import {
2
+ assertReadOnlySql,
3
+ buildChartData
4
+ } from "./chunk-NGXV4I2L.js";
5
+
6
+ // src/adapters/bigquery.ts
7
+ var NUMERIC = /* @__PURE__ */ new Set(["INT64", "INTEGER", "FLOAT64", "FLOAT", "NUMERIC", "BIGNUMERIC"]);
8
+ var TEMPORAL = /* @__PURE__ */ new Set(["DATE", "DATETIME", "TIMESTAMP"]);
9
+ var BOOLEAN = /* @__PURE__ */ new Set(["BOOL", "BOOLEAN"]);
10
+ function bqKind(field) {
11
+ if (field.mode === "REPEATED") return "string";
12
+ const t = (field.type || "").toUpperCase();
13
+ if (t === "RECORD" || t === "STRUCT") return "string";
14
+ if (NUMERIC.has(t)) return "number";
15
+ if (TEMPORAL.has(t)) return "time";
16
+ if (BOOLEAN.has(t)) return "boolean";
17
+ return "string";
18
+ }
19
+ function bqNormalize(value, kind) {
20
+ if (value == null) return null;
21
+ const obj = typeof value === "object" ? value : null;
22
+ if (kind === "number") {
23
+ const raw = obj ? "value" in obj ? obj.value : value.toString() : value;
24
+ const n = Number(raw);
25
+ return Number.isFinite(n) ? n : null;
26
+ }
27
+ if (kind === "time") {
28
+ if (obj && "value" in obj) return obj.value;
29
+ return typeof value === "string" ? value : String(value);
30
+ }
31
+ if (kind === "boolean") {
32
+ if (typeof value === "boolean") return value;
33
+ const s = String(value).toLowerCase();
34
+ return s === "true" || s === "1" ? true : s === "false" || s === "0" ? false : null;
35
+ }
36
+ if (obj) {
37
+ if (Array.isArray(value)) return JSON.stringify(value);
38
+ if ("value" in obj) return String(obj.value);
39
+ return JSON.stringify(value);
40
+ }
41
+ return String(value);
42
+ }
43
+ function bigQueryToChartData(rows, schema) {
44
+ const columns = schema.map((f) => ({ name: f.name ?? "", type: f }));
45
+ return buildChartData({
46
+ rows,
47
+ columns,
48
+ mapKind: (type) => bqKind(type),
49
+ normalizeCell: bqNormalize
50
+ });
51
+ }
52
+
53
+ // src/bigquery.ts
54
+ function bigQueryRunSql(bq, opts = {}) {
55
+ const defaultDataset = typeof opts.defaultDataset === "string" ? { datasetId: opts.defaultDataset } : opts.defaultDataset;
56
+ return async (sql) => {
57
+ if (opts.readOnly !== false) assertReadOnlySql(sql);
58
+ const [job] = await bq.createQueryJob({
59
+ query: sql,
60
+ ...opts.location && { location: opts.location },
61
+ ...opts.maximumBytesBilled && { maximumBytesBilled: opts.maximumBytesBilled },
62
+ ...opts.jobTimeoutMs && { jobTimeoutMs: opts.jobTimeoutMs },
63
+ ...defaultDataset && { defaultDataset }
64
+ });
65
+ const [rows, , apiResponse] = await job.getQueryResults();
66
+ const schema = apiResponse?.schema?.fields ?? [];
67
+ return bigQueryToChartData(rows, schema);
68
+ };
69
+ }
70
+ export {
71
+ bigQueryRunSql,
72
+ bigQueryToChartData
73
+ };
74
+ //# sourceMappingURL=bigquery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/bigquery.ts","../src/bigquery.ts"],"sourcesContent":["// BigQuery -> ChartData adapter. The ONLY engine-specific pieces are here: the type map and the\n// cell normalizer (BigQuery returns typed strings — \"7.19E-4\", \"1389\" — or, via the Node client,\n// wrapper objects like BigQueryDate/Int + Big). The FieldMeta assembly, name hints, ChartData\n// shape and read-only backstop come from ./sql (shared by every adapter).\n//\n// Usage (the dev owns the client + safety):\n// const [job] = await bq.createQueryJob({ query, location, maximumBytesBilled, jobTimeoutMs });\n// const [rows, , apiResponse] = await job.getQueryResults();\n// const data = bigQueryToChartData(rows, apiResponse.schema.fields); // -> ChartData\nimport type { TableField } from \"@google-cloud/bigquery\";\nimport type { ChartData, FieldKind } from \"../types.js\";\nimport { buildChartData, type SourceColumn } from \"./sql.js\";\n\n/** A BigQuery schema field. Anchored to the client's own `TableField` type so it tracks the driver:\n * if @google-cloud/bigquery changes the schema shape, this and its consumers fail to compile.\n * Relevant fields: `type` (INT64/FLOAT64/NUMERIC/BOOL/STRING/DATE/DATETIME/TIMESTAMP/TIME/RECORD/…),\n * `mode` (NULLABLE | REQUIRED | REPEATED, where REPEATED is a non-scalar ARRAY). */\nexport type BigQueryField = TableField;\n\n// BigQuery reports both GoogleSQL and legacy names depending on the API surface — handle both.\nconst NUMERIC = new Set([\"INT64\", \"INTEGER\", \"FLOAT64\", \"FLOAT\", \"NUMERIC\", \"BIGNUMERIC\"]);\nconst TEMPORAL = new Set([\"DATE\", \"DATETIME\", \"TIMESTAMP\"]);\nconst BOOLEAN = new Set([\"BOOL\", \"BOOLEAN\"]);\n\nfunction bqKind(field: BigQueryField): FieldKind {\n if (field.mode === \"REPEATED\") return \"string\"; // ARRAY -> not scalar-plottable, stringify\n const t = (field.type || \"\").toUpperCase();\n if (t === \"RECORD\" || t === \"STRUCT\") return \"string\"; // nested -> stringify\n if (NUMERIC.has(t)) return \"number\";\n if (TEMPORAL.has(t)) return \"time\";\n if (BOOLEAN.has(t)) return \"boolean\";\n return \"string\"; // STRING, TIME, BYTES, GEOGRAPHY, JSON, ...\n}\n\n// Coerce one BigQuery cell to a flat scalar, handling raw-string (REST) and wrapper-object (Node\n// client) representations, guided by the column's declared kind.\nfunction bqNormalize(value: unknown, kind: FieldKind): unknown {\n if (value == null) return null;\n const obj = typeof value === \"object\" ? (value as Record<string, unknown>) : null;\n if (kind === \"number\") {\n const raw = obj ? (\"value\" in obj ? obj.value : (value as { toString(): string }).toString()) : value;\n const n = Number(raw);\n return Number.isFinite(n) ? n : null;\n }\n if (kind === \"time\") {\n // BigQueryDate/Datetime/Timestamp -> { value } (ISO string); raw -> ISO string already.\n if (obj && \"value\" in obj) return obj.value;\n return typeof value === \"string\" ? value : String(value);\n }\n if (kind === \"boolean\") {\n if (typeof value === \"boolean\") return value;\n const s = String(value).toLowerCase();\n return s === \"true\" || s === \"1\" ? true : s === \"false\" || s === \"0\" ? false : null;\n }\n // string-ish: stringify arrays/structs so the scalar guard passes; unwrap { value }; else String.\n if (obj) {\n if (Array.isArray(value)) return JSON.stringify(value);\n if (\"value\" in obj) return String(obj.value);\n return JSON.stringify(value);\n }\n return String(value);\n}\n\n/**\n * Convert a BigQuery query result into ChartData.\n * @param rows Result rows (record objects), as returned by `@google-cloud/bigquery` or the REST API.\n * @param schema The result's `schema.fields` (BigQuery's standard TableFieldSchema list).\n */\nexport function bigQueryToChartData(rows: Record<string, unknown>[], schema: BigQueryField[]): ChartData {\n const columns: SourceColumn[] = schema.map((f) => ({ name: f.name ?? \"\", type: f }));\n return buildChartData({\n rows,\n columns,\n mapKind: (type) => bqKind(type as BigQueryField),\n normalizeCell: bqNormalize,\n });\n}\n","// @bonnard/mcp-charts/bigquery — drop-in BigQuery adapter for addCharts.\n//\n// The caller owns the client, auth (ADC / service account) and IAM (read-only); this packages the\n// rest of the dev-side `runSql`: a read-only guard, a cost cap + timeout, an optional default\n// dataset, and the schema-driven result -> ChartData transform.\n//\n// import { addCharts } from \"@bonnard/mcp-charts\";\n// import { bigQueryRunSql } from \"@bonnard/mcp-charts/bigquery\";\n// import { BigQuery } from \"@google-cloud/bigquery\";\n//\n// const bq = new BigQuery(); // ADC: local = gcloud login; prod = Workload Identity service account\n// addCharts(server, {\n// runSql: bigQueryRunSql(bq, {\n// location: \"europe-west2\",\n// maximumBytesBilled: \"200000000\", // cost guard\n// defaultDataset: \"analytics_internal\", // unqualified table names resolve here\n// }),\n// discovery: { toolName: \"explore_schema\" },\n// });\n//\n// `@google-cloud/bigquery` is an OPTIONAL peer (only needed if you import this subpath). The type\n// import below is erased at build, so this module adds no runtime dependency — it just calls methods\n// on the client you pass in.\nimport type { BigQuery } from \"@google-cloud/bigquery\";\nimport type { ChartData } from \"./types.js\";\nimport { assertReadOnlySql } from \"./adapters/sql.js\";\nimport { bigQueryToChartData, type BigQueryField } from \"./adapters/bigquery.js\";\n\nexport { bigQueryToChartData, type BigQueryField };\n\nexport interface BigQueryRunSqlOptions {\n /** BigQuery processing location (must match the dataset region), e.g. \"europe-west2\". */\n location?: string;\n /** Cost guard: max bytes a single query may bill (BigQuery's string API shape). */\n maximumBytesBilled?: string;\n /** Per-query timeout in milliseconds. */\n jobTimeoutMs?: number;\n /** Default dataset so the agent can write unqualified table names (e.g. `weekly_sales`). */\n defaultDataset?: string | { datasetId: string; projectId?: string };\n /** Reject non-SELECT (DML/DDL) statements. Default: true. A read-only IAM role is the real boundary. */\n readOnly?: boolean;\n}\n\n/**\n * Build the `runSql` callback for `addCharts` from a BigQuery client.\n * Runs the query with the configured guards and returns schema-typed ChartData.\n */\nexport function bigQueryRunSql(bq: BigQuery, opts: BigQueryRunSqlOptions = {}): (sql: string) => Promise<ChartData> {\n const defaultDataset =\n typeof opts.defaultDataset === \"string\" ? { datasetId: opts.defaultDataset } : opts.defaultDataset;\n return async (sql: string): Promise<ChartData> => {\n if (opts.readOnly !== false) assertReadOnlySql(sql);\n const [job] = await bq.createQueryJob({\n query: sql,\n ...(opts.location && { location: opts.location }),\n ...(opts.maximumBytesBilled && { maximumBytesBilled: opts.maximumBytesBilled }),\n ...(opts.jobTimeoutMs && { jobTimeoutMs: opts.jobTimeoutMs }),\n ...(defaultDataset && { defaultDataset }),\n });\n const [rows, , apiResponse] = await job.getQueryResults();\n const schema: BigQueryField[] = apiResponse?.schema?.fields ?? [];\n return bigQueryToChartData(rows as Record<string, unknown>[], schema);\n };\n}\n"],"mappings":";;;;;;AAoBA,IAAM,UAAU,oBAAI,IAAI,CAAC,SAAS,WAAW,WAAW,SAAS,WAAW,YAAY,CAAC;AACzF,IAAM,WAAW,oBAAI,IAAI,CAAC,QAAQ,YAAY,WAAW,CAAC;AAC1D,IAAM,UAAU,oBAAI,IAAI,CAAC,QAAQ,SAAS,CAAC;AAE3C,SAAS,OAAO,OAAiC;AAC/C,MAAI,MAAM,SAAS,WAAY,QAAO;AACtC,QAAM,KAAK,MAAM,QAAQ,IAAI,YAAY;AACzC,MAAI,MAAM,YAAY,MAAM,SAAU,QAAO;AAC7C,MAAI,QAAQ,IAAI,CAAC,EAAG,QAAO;AAC3B,MAAI,SAAS,IAAI,CAAC,EAAG,QAAO;AAC5B,MAAI,QAAQ,IAAI,CAAC,EAAG,QAAO;AAC3B,SAAO;AACT;AAIA,SAAS,YAAY,OAAgB,MAA0B;AAC7D,MAAI,SAAS,KAAM,QAAO;AAC1B,QAAM,MAAM,OAAO,UAAU,WAAY,QAAoC;AAC7E,MAAI,SAAS,UAAU;AACrB,UAAM,MAAM,MAAO,WAAW,MAAM,IAAI,QAAS,MAAiC,SAAS,IAAK;AAChG,UAAM,IAAI,OAAO,GAAG;AACpB,WAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAAA,EAClC;AACA,MAAI,SAAS,QAAQ;AAEnB,QAAI,OAAO,WAAW,IAAK,QAAO,IAAI;AACtC,WAAO,OAAO,UAAU,WAAW,QAAQ,OAAO,KAAK;AAAA,EACzD;AACA,MAAI,SAAS,WAAW;AACtB,QAAI,OAAO,UAAU,UAAW,QAAO;AACvC,UAAM,IAAI,OAAO,KAAK,EAAE,YAAY;AACpC,WAAO,MAAM,UAAU,MAAM,MAAM,OAAO,MAAM,WAAW,MAAM,MAAM,QAAQ;AAAA,EACjF;AAEA,MAAI,KAAK;AACP,QAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,KAAK,UAAU,KAAK;AACrD,QAAI,WAAW,IAAK,QAAO,OAAO,IAAI,KAAK;AAC3C,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B;AACA,SAAO,OAAO,KAAK;AACrB;AAOO,SAAS,oBAAoB,MAAiC,QAAoC;AACvG,QAAM,UAA0B,OAAO,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,IAAI,MAAM,EAAE,EAAE;AACnF,SAAO,eAAe;AAAA,IACpB;AAAA,IACA;AAAA,IACA,SAAS,CAAC,SAAS,OAAO,IAAqB;AAAA,IAC/C,eAAe;AAAA,EACjB,CAAC;AACH;;;AC7BO,SAAS,eAAe,IAAc,OAA8B,CAAC,GAAwC;AAClH,QAAM,iBACJ,OAAO,KAAK,mBAAmB,WAAW,EAAE,WAAW,KAAK,eAAe,IAAI,KAAK;AACtF,SAAO,OAAO,QAAoC;AAChD,QAAI,KAAK,aAAa,MAAO,mBAAkB,GAAG;AAClD,UAAM,CAAC,GAAG,IAAI,MAAM,GAAG,eAAe;AAAA,MACpC,OAAO;AAAA,MACP,GAAI,KAAK,YAAY,EAAE,UAAU,KAAK,SAAS;AAAA,MAC/C,GAAI,KAAK,sBAAsB,EAAE,oBAAoB,KAAK,mBAAmB;AAAA,MAC7E,GAAI,KAAK,gBAAgB,EAAE,cAAc,KAAK,aAAa;AAAA,MAC3D,GAAI,kBAAkB,EAAE,eAAe;AAAA,IACzC,CAAC;AACD,UAAM,CAAC,MAAM,EAAE,WAAW,IAAI,MAAM,IAAI,gBAAgB;AACxD,UAAM,SAA0B,aAAa,QAAQ,UAAU,CAAC;AAChE,WAAO,oBAAoB,MAAmC,MAAM;AAAA,EACtE;AACF;","names":[]}
@@ -0,0 +1,136 @@
1
+ // src/resolve/infer.ts
2
+ function sniffTimeGranularity(value) {
3
+ if (typeof value !== "string") return null;
4
+ if (/^\d{4}-\d{2}-\d{2}([T ]|$)/.test(value)) return "day";
5
+ if (/^\d{4}-\d{2}$/.test(value)) return "month";
6
+ if (/^\d{4}-?Q[1-4]$/i.test(value)) return "quarter";
7
+ if (/^\d{4}-W\d{2}$/i.test(value)) return "week";
8
+ if (/^\d{4}$/.test(value)) {
9
+ const y = Number(value);
10
+ if (y >= 1900 && y <= 2100) return "year";
11
+ }
12
+ return null;
13
+ }
14
+ function sniffKind(rows, name) {
15
+ const sample = rows.find((r) => r[name] != null)?.[name];
16
+ if (sample == null) return "string";
17
+ if (typeof sample === "number" || typeof sample === "bigint") return "number";
18
+ if (typeof sample === "boolean") return "boolean";
19
+ if (typeof sample === "string" && sniffTimeGranularity(sample) !== null) return "time";
20
+ return "string";
21
+ }
22
+ function roleFromKind(kind) {
23
+ if (kind === "time") return "time";
24
+ if (kind === "number") return "measure";
25
+ return "dimension";
26
+ }
27
+ function titleCase(s) {
28
+ return s.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
29
+ }
30
+ function formatHint(name) {
31
+ const n = name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
32
+ if (/(^|_)(rate|pct|percent|percentage|ratio)(_|$)/.test(n)) return "percent";
33
+ return void 0;
34
+ }
35
+ function granularityHint(name) {
36
+ const n = name.toLowerCase();
37
+ if (n.includes("year")) return "year";
38
+ if (n.includes("quarter")) return "quarter";
39
+ if (n.includes("month")) return "month";
40
+ if (n.includes("week")) return "week";
41
+ return "day";
42
+ }
43
+ function inferFields(data) {
44
+ const { rows, fields } = data;
45
+ const declared = new Map((fields ?? []).map((f) => [f.name, f]));
46
+ const rowKeys = rows[0] ? Object.keys(rows[0]) : [];
47
+ const names = [...rowKeys, ...[...declared.keys()].filter((n) => !rowKeys.includes(n))];
48
+ return names.map((name) => {
49
+ const d = declared.get(name);
50
+ const kind = d?.kind ?? sniffKind(rows, name);
51
+ const role = d?.role ?? roleFromKind(kind);
52
+ const granularity = d?.granularity ?? (kind === "time" ? sniffTimeGranularity(rows.find((r) => r[name] != null)?.[name]) ?? granularityHint(name) : void 0);
53
+ const format = d?.format ?? (kind === "number" ? formatHint(name) : void 0);
54
+ return {
55
+ name,
56
+ kind,
57
+ role,
58
+ label: d?.label ?? titleCase(name),
59
+ ...format && { format },
60
+ ...granularity && { granularity },
61
+ ...d?.additive != null && { additive: d.additive },
62
+ ...d?.currency && { currency: d.currency }
63
+ };
64
+ });
65
+ }
66
+
67
+ // src/adapters/sql.ts
68
+ function defaultNormalizeCell(value, kind) {
69
+ if (value == null) return null;
70
+ const obj = typeof value === "object" ? value : null;
71
+ if (kind === "number") {
72
+ const raw = obj ? "value" in obj ? obj.value : value.toString() : value;
73
+ const n = Number(raw);
74
+ return Number.isFinite(n) ? n : null;
75
+ }
76
+ if (kind === "boolean") {
77
+ if (typeof value === "boolean") return value;
78
+ const s = String(obj && "value" in obj ? obj.value : value).toLowerCase();
79
+ return s === "true" || s === "1" ? true : s === "false" || s === "0" ? false : null;
80
+ }
81
+ if (kind === "time") {
82
+ if (obj && "value" in obj) return String(obj.value);
83
+ return value instanceof Date ? value.toISOString() : typeof value === "string" ? value : String(value);
84
+ }
85
+ if (obj) {
86
+ if (Array.isArray(value)) return JSON.stringify(value);
87
+ if ("value" in obj) return String(obj.value);
88
+ return JSON.stringify(value);
89
+ }
90
+ return value;
91
+ }
92
+ function buildChartData({
93
+ rows,
94
+ columns,
95
+ mapKind,
96
+ normalizeCell = defaultNormalizeCell
97
+ }) {
98
+ const fields = columns.map((col) => {
99
+ const kind = mapKind(col.type, col);
100
+ const format = kind === "number" ? formatHint(col.name) : void 0;
101
+ const granularity = kind === "time" ? granularityHint(col.name) : void 0;
102
+ return {
103
+ name: col.name,
104
+ kind,
105
+ role: roleFromKind(kind),
106
+ label: titleCase(col.name),
107
+ ...format && { format },
108
+ ...granularity && { granularity }
109
+ };
110
+ });
111
+ const kindByName = new Map(fields.map((f) => [f.name, f.kind]));
112
+ const out = rows.map((row) => {
113
+ const r = {};
114
+ for (const col of columns) r[col.name] = normalizeCell(row[col.name], kindByName.get(col.name), col);
115
+ return r;
116
+ });
117
+ return { rows: out, fields };
118
+ }
119
+ function assertReadOnlySql(sql) {
120
+ const t = sql.trim().replace(/;\s*$/, "");
121
+ if (/;/.test(t)) throw new Error("Only a single statement is allowed.");
122
+ if (!/^(select|with)\b/i.test(t)) throw new Error("Only read-only SELECT queries are allowed.");
123
+ if (/\b(insert|update|delete|merge|drop|alter|create|truncate|grant|revoke|call|export|load|copy|execute|attach|vacuum|lock)\b/i.test(
124
+ t
125
+ )) {
126
+ throw new Error("Write/DDL statements are not allowed.");
127
+ }
128
+ }
129
+
130
+ export {
131
+ inferFields,
132
+ defaultNormalizeCell,
133
+ buildChartData,
134
+ assertReadOnlySql
135
+ };
136
+ //# sourceMappingURL=chunk-NGXV4I2L.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/resolve/infer.ts","../src/adapters/sql.ts"],"sourcesContent":["// Typing inference: turn a ChartData into a complete, role-tagged FieldMeta list.\n// Priority: declared fields win; anything missing is sniffed from the row values.\n// This is what lets resolve() decide x / y / series when the data source did not\n// supply full typing (e.g. raw SQL rows).\nimport type { ChartData, FieldFormat, FieldKind, FieldMeta, FieldRole, TimeGranularity } from \"../types.js\";\n\n// Period-string -> granularity. Lets the raw-SQL path recognize time buckets that aren't full ISO\n// dates (e.g. strftime('%Y-%m') -> \"2025-01\") so time series are typed TEMPORAL — ordered, drawn\n// vertically, nicely labelled — instead of being mistaken for high-cardinality categories. Used\n// when the data source declares no column types, so the kind is inferred from the value shape.\nexport function sniffTimeGranularity(value: unknown): TimeGranularity | null {\n if (typeof value !== \"string\") return null;\n if (/^\\d{4}-\\d{2}-\\d{2}([T ]|$)/.test(value)) return \"day\"; // 2025-01-15 or a full datetime\n if (/^\\d{4}-\\d{2}$/.test(value)) return \"month\"; // 2025-01\n if (/^\\d{4}-?Q[1-4]$/i.test(value)) return \"quarter\"; // 2025-Q1 / 2025Q1\n if (/^\\d{4}-W\\d{2}$/i.test(value)) return \"week\"; // 2025-W03 (ISO week)\n if (/^\\d{4}$/.test(value)) {\n const y = Number(value); // bare year as a string (\"2025\"); guard the range to avoid 4-digit codes\n if (y >= 1900 && y <= 2100) return \"year\";\n }\n return null;\n}\n\nexport function sniffKind(rows: Record<string, unknown>[], name: string): FieldKind {\n const sample = rows.find((r) => r[name] != null)?.[name];\n if (sample == null) return \"string\";\n if (typeof sample === \"number\" || typeof sample === \"bigint\") return \"number\";\n if (typeof sample === \"boolean\") return \"boolean\";\n if (typeof sample === \"string\" && sniffTimeGranularity(sample) !== null) return \"time\";\n return \"string\";\n}\n\nexport function roleFromKind(kind: FieldKind): FieldRole {\n if (kind === \"time\") return \"time\";\n if (kind === \"number\") return \"measure\";\n return \"dimension\";\n}\n\nexport function titleCase(s: string): string {\n return s.replace(/_/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\n// Name-based hints — a purely conventional fallback used only when the data source declares no\n// format/granularity. Centralized here so the adapter path (buildChartData) and the raw-rows path\n// (inferFields) apply identical rules.\n\n// A numeric column whose name reads as a rate/ratio renders as a percent. Currency is deliberately\n// NOT guessed from names (the code is unknowable) — declare it. camelCase is normalized to snake.\nexport function formatHint(name: string): FieldFormat | undefined {\n const n = name.replace(/([a-z0-9])([A-Z])/g, \"$1_$2\").toLowerCase();\n if (/(^|_)(rate|pct|percent|percentage|ratio)(_|$)/.test(n)) return \"percent\";\n return undefined;\n}\n\n// Granularity from the column name, for time columns whose values don't reveal it (e.g. a DATE\n// named \"week_start\" holding full dates). Defaults to day.\nexport function granularityHint(name: string): TimeGranularity {\n const n = name.toLowerCase();\n if (n.includes(\"year\")) return \"year\";\n if (n.includes(\"quarter\")) return \"quarter\";\n if (n.includes(\"month\")) return \"month\";\n if (n.includes(\"week\")) return \"week\";\n return \"day\";\n}\n\n/** Complete field metadata for every column, declared values taking precedence. */\nexport function inferFields(data: ChartData): FieldMeta[] {\n const { rows, fields } = data;\n const declared = new Map((fields ?? []).map((f) => [f.name, f]));\n // Union of returned columns + any declared-only names: declaring fields for SOME columns\n // augments their typing, it does not drop the columns you didn't declare.\n const rowKeys = rows[0] ? Object.keys(rows[0]) : [];\n const names = [...rowKeys, ...[...declared.keys()].filter((n) => !rowKeys.includes(n))];\n\n return names.map((name) => {\n const d = declared.get(name);\n const kind: FieldKind = d?.kind ?? sniffKind(rows, name);\n const role: FieldRole = d?.role ?? roleFromKind(kind);\n // Time columns with no declared granularity: infer it from the value shape (YYYY-MM -> month).\n // declared > value-sniff > name-hint\n const granularity =\n d?.granularity ??\n (kind === \"time\"\n ? (sniffTimeGranularity(rows.find((r) => r[name] != null)?.[name]) ?? granularityHint(name))\n : undefined);\n // declared > name-hint (rate -> percent); only for numeric columns\n const format = d?.format ?? (kind === \"number\" ? formatHint(name) : undefined);\n return {\n name,\n kind,\n role,\n label: d?.label ?? titleCase(name),\n ...(format && { format }),\n ...(granularity && { granularity }),\n ...(d?.additive != null && { additive: d.additive }),\n ...(d?.currency && { currency: d.currency }),\n };\n });\n}\n","// Shared adapter core. An adapter's only engine-specific jobs are (a) report each column's engine\n// type, (b) map that type to a FieldKind, and (c) normalize a cell to a flat scalar. The rest —\n// assembling typed FieldMeta (with name-based format/granularity hints), the ChartData shape, and\n// the read-only check — lives here so every adapter shares it.\n//\n// Connection, auth, read-only enforcement, cost caps and timeouts belong to the caller's database\n// role; assertReadOnlySql is a cheap extra check on LLM-authored SQL, not a substitute for one.\nimport type { ChartData, FieldKind, FieldMeta } from \"../types.js\";\nimport { roleFromKind, titleCase, formatHint, granularityHint } from \"../resolve/infer.js\";\n\n/** A source column: its name + the engine's declared type, in whatever shape the driver reports. */\nexport interface SourceColumn {\n name: string;\n /** Engine type, passed verbatim to mapKind (e.g. \"INT64\", a Postgres OID, \"VARCHAR\", a field object). */\n type: unknown;\n}\n\n/** Map an engine's column type to our FieldKind. The one piece of real per-engine work. */\nexport type KindMapper = (type: unknown, column: SourceColumn) => FieldKind;\n\n/** Coerce one cell value to a flat scalar, guided by the resolved kind. */\nexport type CellNormalizer = (value: unknown, kind: FieldKind, column: SourceColumn) => unknown;\n\nexport interface BuildChartDataOptions {\n rows: Record<string, unknown>[];\n columns: SourceColumn[];\n mapKind: KindMapper;\n /** Optional; defaults to {@link defaultNormalizeCell}, which suits drivers that return JS-native values. */\n normalizeCell?: CellNormalizer;\n}\n\n/** Default cell normalizer: pass scalars through, coerce numbers/booleans, stringify non-scalars.\n * Handles `{ value }` wrapper objects (some drivers wrap typed values). Adapters whose driver\n * needs special handling (e.g. BigQuery's typed-string + wrapper mix) supply their own. */\nexport function defaultNormalizeCell(value: unknown, kind: FieldKind): unknown {\n if (value == null) return null;\n const obj = typeof value === \"object\" ? (value as Record<string, unknown>) : null;\n if (kind === \"number\") {\n const raw = obj ? (\"value\" in obj ? obj.value : (value as { toString(): string }).toString()) : value;\n const n = Number(raw);\n return Number.isFinite(n) ? n : null;\n }\n if (kind === \"boolean\") {\n if (typeof value === \"boolean\") return value;\n const s = String(obj && \"value\" in obj ? obj.value : value).toLowerCase();\n return s === \"true\" || s === \"1\" ? true : s === \"false\" || s === \"0\" ? false : null;\n }\n if (kind === \"time\") {\n if (obj && \"value\" in obj) return String(obj.value);\n return value instanceof Date ? value.toISOString() : typeof value === \"string\" ? value : String(value);\n }\n // string-ish: unwrap { value }, stringify arrays/objects so the scalar guard passes, else pass through.\n if (obj) {\n if (Array.isArray(value)) return JSON.stringify(value);\n if (\"value\" in obj) return String(obj.value);\n return JSON.stringify(value);\n }\n return value;\n}\n\n/**\n * Turn a SQL result (rows + column types) into typed ChartData. Builds a complete FieldMeta per\n * column from the engine kind + name-based format/granularity hints, then normalizes every cell.\n */\nexport function buildChartData({\n rows,\n columns,\n mapKind,\n normalizeCell = defaultNormalizeCell,\n}: BuildChartDataOptions): ChartData {\n const fields: FieldMeta[] = columns.map((col) => {\n const kind = mapKind(col.type, col);\n const format = kind === \"number\" ? formatHint(col.name) : undefined;\n const granularity = kind === \"time\" ? granularityHint(col.name) : undefined;\n return {\n name: col.name,\n kind,\n role: roleFromKind(kind),\n label: titleCase(col.name),\n ...(format && { format }),\n ...(granularity && { granularity }),\n };\n });\n const kindByName = new Map(fields.map((f) => [f.name, f.kind!]));\n const out = rows.map((row) => {\n const r: Record<string, unknown> = {};\n for (const col of columns) r[col.name] = normalizeCell(row[col.name], kindByName.get(col.name)!, col);\n return r;\n });\n return { rows: out, fields };\n}\n\n/**\n * Reject anything that isn't a single SELECT/WITH statement. A cheap check on LLM-authored SQL;\n * enforce real read-only access with a database role.\n */\nexport function assertReadOnlySql(sql: string): void {\n const t = sql.trim().replace(/;\\s*$/, \"\");\n if (/;/.test(t)) throw new Error(\"Only a single statement is allowed.\");\n if (!/^(select|with)\\b/i.test(t)) throw new Error(\"Only read-only SELECT queries are allowed.\");\n if (\n /\\b(insert|update|delete|merge|drop|alter|create|truncate|grant|revoke|call|export|load|copy|execute|attach|vacuum|lock)\\b/i.test(\n t,\n )\n ) {\n throw new Error(\"Write/DDL statements are not allowed.\");\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,23 @@
1
+ import { DBSQLSession } from '@databricks/sql';
2
+ import { C as ChartData } from './types-CHiUU9mj.js';
3
+
4
+ /** A Databricks result column: name + the Thrift TTypeId code. */
5
+ interface DatabricksColumn {
6
+ name: string;
7
+ typeId: number;
8
+ }
9
+ /**
10
+ * Convert a Databricks result into ChartData.
11
+ * @param rows Row objects from `operation.fetchAll()`.
12
+ * @param columns Column name + TTypeId, extracted from `operation.getSchema()` (see databricksRunSql).
13
+ */
14
+ declare function databricksToChartData(rows: Record<string, unknown>[], columns: DatabricksColumn[]): ChartData;
15
+
16
+ interface DatabricksRunSqlOptions {
17
+ /** Reject non-SELECT statements. Default: true. A read-only warehouse/grant is the real boundary. */
18
+ readOnly?: boolean;
19
+ }
20
+ /** Build the `runSql` callback for `addCharts` from an open Databricks SQL session. */
21
+ declare function databricksRunSql(session: DBSQLSession, opts?: DatabricksRunSqlOptions): (sql: string) => Promise<ChartData>;
22
+
23
+ export { type DatabricksColumn, type DatabricksRunSqlOptions, databricksRunSql, databricksToChartData };
@@ -0,0 +1,65 @@
1
+ import {
2
+ assertReadOnlySql,
3
+ buildChartData
4
+ } from "./chunk-NGXV4I2L.js";
5
+
6
+ // src/adapters/databricks.ts
7
+ var TTypeId = {
8
+ BOOLEAN: 0,
9
+ TINYINT: 1,
10
+ SMALLINT: 2,
11
+ INT: 3,
12
+ BIGINT: 4,
13
+ FLOAT: 5,
14
+ DOUBLE: 6,
15
+ STRING: 7,
16
+ TIMESTAMP: 8,
17
+ DECIMAL: 15,
18
+ DATE: 17,
19
+ VARCHAR: 18,
20
+ CHAR: 19
21
+ };
22
+ var NUMERIC = /* @__PURE__ */ new Set([
23
+ TTypeId.TINYINT,
24
+ TTypeId.SMALLINT,
25
+ TTypeId.INT,
26
+ TTypeId.BIGINT,
27
+ TTypeId.FLOAT,
28
+ TTypeId.DOUBLE,
29
+ TTypeId.DECIMAL
30
+ ]);
31
+ var TEMPORAL = /* @__PURE__ */ new Set([TTypeId.TIMESTAMP, TTypeId.DATE]);
32
+ function databricksKind(typeId) {
33
+ if (NUMERIC.has(typeId)) return "number";
34
+ if (TEMPORAL.has(typeId)) return "time";
35
+ if (typeId === TTypeId.BOOLEAN) return "boolean";
36
+ return "string";
37
+ }
38
+ function databricksToChartData(rows, columns) {
39
+ const cols = columns.map((c) => ({ name: c.name, type: c.typeId }));
40
+ return buildChartData({ rows, columns: cols, mapKind: (typeId) => databricksKind(typeId) });
41
+ }
42
+
43
+ // src/databricks.ts
44
+ function databricksRunSql(session, opts = {}) {
45
+ const readOnly = opts.readOnly !== false;
46
+ return async (sql) => {
47
+ if (readOnly) assertReadOnlySql(sql);
48
+ const operation = await session.executeStatement(sql);
49
+ try {
50
+ const [rows, schema] = await Promise.all([operation.fetchAll(), operation.getSchema()]);
51
+ const columns = (schema?.columns ?? []).map((c) => ({
52
+ name: c.columnName,
53
+ typeId: c.typeDesc?.types?.[0]?.primitiveEntry?.type ?? -1
54
+ }));
55
+ return databricksToChartData(rows, columns);
56
+ } finally {
57
+ await operation.close();
58
+ }
59
+ };
60
+ }
61
+ export {
62
+ databricksRunSql,
63
+ databricksToChartData
64
+ };
65
+ //# sourceMappingURL=databricks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/databricks.ts","../src/databricks.ts"],"sourcesContent":["// Databricks -> ChartData. Engine-specific only: map Thrift TTypeId codes (from the result schema)\n// to FieldKind. FieldMeta assembly + ChartData shape come from ./sql.\nimport type { ChartData, FieldKind } from \"../types.js\";\nimport { buildChartData, type SourceColumn } from \"./sql.js\";\n\n// Thrift TTypeId codes (@databricks/sql TCLIService_types). These are wire-protocol constants.\nconst TTypeId = {\n BOOLEAN: 0,\n TINYINT: 1,\n SMALLINT: 2,\n INT: 3,\n BIGINT: 4,\n FLOAT: 5,\n DOUBLE: 6,\n STRING: 7,\n TIMESTAMP: 8,\n DECIMAL: 15,\n DATE: 17,\n VARCHAR: 18,\n CHAR: 19,\n} as const;\n\nconst NUMERIC = new Set<number>([\n TTypeId.TINYINT,\n TTypeId.SMALLINT,\n TTypeId.INT,\n TTypeId.BIGINT,\n TTypeId.FLOAT,\n TTypeId.DOUBLE,\n TTypeId.DECIMAL,\n]);\nconst TEMPORAL = new Set<number>([TTypeId.TIMESTAMP, TTypeId.DATE]);\n\nfunction databricksKind(typeId: number): FieldKind {\n if (NUMERIC.has(typeId)) return \"number\";\n if (TEMPORAL.has(typeId)) return \"time\";\n if (typeId === TTypeId.BOOLEAN) return \"boolean\";\n return \"string\"; // string/varchar/char/binary/array/map/struct (stringified) ...\n}\n\n/** A Databricks result column: name + the Thrift TTypeId code. */\nexport interface DatabricksColumn {\n name: string;\n typeId: number;\n}\n\n/**\n * Convert a Databricks result into ChartData.\n * @param rows Row objects from `operation.fetchAll()`.\n * @param columns Column name + TTypeId, extracted from `operation.getSchema()` (see databricksRunSql).\n */\nexport function databricksToChartData(rows: Record<string, unknown>[], columns: DatabricksColumn[]): ChartData {\n const cols: SourceColumn[] = columns.map((c) => ({ name: c.name, type: c.typeId }));\n return buildChartData({ rows, columns: cols, mapKind: (typeId) => databricksKind(typeId as number) });\n}\n","// @bonnard/mcp-charts/databricks — drop-in Databricks SQL adapter for addCharts.\n//\n// The caller owns the client/session + auth; this packages the dev-side `runSql`: a read-only check\n// plus the result -> ChartData transform. Use a read-only SQL warehouse / Unity Catalog grants as\n// the real boundary.\n//\n// import { addCharts } from \"@bonnard/mcp-charts\";\n// import { databricksRunSql } from \"@bonnard/mcp-charts/databricks\";\n// import { DBSQLClient } from \"@databricks/sql\";\n//\n// const client = new DBSQLClient();\n// await client.connect({ host, path, token });\n// const session = await client.openSession();\n// addCharts(server, { runSql: databricksRunSql(session), discovery: { toolName: \"explore_schema\" } });\n//\n// `@databricks/sql` is an OPTIONAL peer — only needed if you import this subpath.\nimport type { DBSQLSession } from \"@databricks/sql\";\nimport type { ChartData } from \"./types.js\";\nimport { assertReadOnlySql } from \"./adapters/sql.js\";\nimport { databricksToChartData } from \"./adapters/databricks.js\";\n\nexport { databricksToChartData, type DatabricksColumn } from \"./adapters/databricks.js\";\n\nexport interface DatabricksRunSqlOptions {\n /** Reject non-SELECT statements. Default: true. A read-only warehouse/grant is the real boundary. */\n readOnly?: boolean;\n}\n\n/** Build the `runSql` callback for `addCharts` from an open Databricks SQL session. */\nexport function databricksRunSql(\n session: DBSQLSession,\n opts: DatabricksRunSqlOptions = {},\n): (sql: string) => Promise<ChartData> {\n const readOnly = opts.readOnly !== false;\n return async (sql: string): Promise<ChartData> => {\n if (readOnly) assertReadOnlySql(sql);\n const operation = await session.executeStatement(sql);\n try {\n const [rows, schema] = await Promise.all([operation.fetchAll(), operation.getSchema()]);\n const columns = (schema?.columns ?? []).map((c) => ({\n name: c.columnName,\n typeId: c.typeDesc?.types?.[0]?.primitiveEntry?.type ?? -1,\n }));\n return databricksToChartData(rows as Record<string, unknown>[], columns);\n } finally {\n await operation.close();\n }\n };\n}\n"],"mappings":";;;;;;AAMA,IAAM,UAAU;AAAA,EACd,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AACR;AAEA,IAAM,UAAU,oBAAI,IAAY;AAAA,EAC9B,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AACV,CAAC;AACD,IAAM,WAAW,oBAAI,IAAY,CAAC,QAAQ,WAAW,QAAQ,IAAI,CAAC;AAElE,SAAS,eAAe,QAA2B;AACjD,MAAI,QAAQ,IAAI,MAAM,EAAG,QAAO;AAChC,MAAI,SAAS,IAAI,MAAM,EAAG,QAAO;AACjC,MAAI,WAAW,QAAQ,QAAS,QAAO;AACvC,SAAO;AACT;AAaO,SAAS,sBAAsB,MAAiC,SAAwC;AAC7G,QAAM,OAAuB,QAAQ,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,EAAE,OAAO,EAAE;AAClF,SAAO,eAAe,EAAE,MAAM,SAAS,MAAM,SAAS,CAAC,WAAW,eAAe,MAAgB,EAAE,CAAC;AACtG;;;ACzBO,SAAS,iBACd,SACA,OAAgC,CAAC,GACI;AACrC,QAAM,WAAW,KAAK,aAAa;AACnC,SAAO,OAAO,QAAoC;AAChD,QAAI,SAAU,mBAAkB,GAAG;AACnC,UAAM,YAAY,MAAM,QAAQ,iBAAiB,GAAG;AACpD,QAAI;AACF,YAAM,CAAC,MAAM,MAAM,IAAI,MAAM,QAAQ,IAAI,CAAC,UAAU,SAAS,GAAG,UAAU,UAAU,CAAC,CAAC;AACtF,YAAM,WAAW,QAAQ,WAAW,CAAC,GAAG,IAAI,CAAC,OAAO;AAAA,QAClD,MAAM,EAAE;AAAA,QACR,QAAQ,EAAE,UAAU,QAAQ,CAAC,GAAG,gBAAgB,QAAQ;AAAA,MAC1D,EAAE;AACF,aAAO,sBAAsB,MAAmC,OAAO;AAAA,IACzE,UAAE;AACA,YAAM,UAAU,MAAM;AAAA,IACxB;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,23 @@
1
+ import { DuckDBType, DuckDBConnection } from '@duckdb/node-api';
2
+ import { C as ChartData } from './types-CHiUU9mj.js';
3
+
4
+ /** A DuckDB result column: its name + the DuckDBType (from reader.columnTypes()). */
5
+ interface DuckDbColumn {
6
+ name: string;
7
+ type: DuckDBType;
8
+ }
9
+ /**
10
+ * Convert a DuckDB result into ChartData.
11
+ * @param rows Row objects from `reader.getRowObjectsJson()` (JSON-safe scalar values).
12
+ * @param columns Column name + DuckDBType pairs (from `reader.columnNames()` + `reader.columnTypes()`).
13
+ */
14
+ declare function duckDbToChartData(rows: Record<string, unknown>[], columns: DuckDbColumn[]): ChartData;
15
+
16
+ interface DuckDbRunSqlOptions {
17
+ /** Reject non-SELECT statements. Default: true. A read-only instance is the real boundary. */
18
+ readOnly?: boolean;
19
+ }
20
+ /** Build the `runSql` callback for `addCharts` from a DuckDB connection. */
21
+ declare function duckDbRunSql(connection: DuckDBConnection, opts?: DuckDbRunSqlOptions): (sql: string) => Promise<ChartData>;
22
+
23
+ export { type DuckDbColumn, type DuckDbRunSqlOptions, duckDbRunSql, duckDbToChartData };
package/dist/duckdb.js ADDED
@@ -0,0 +1,58 @@
1
+ import {
2
+ assertReadOnlySql,
3
+ buildChartData
4
+ } from "./chunk-NGXV4I2L.js";
5
+
6
+ // src/adapters/duckdb.ts
7
+ import { DuckDBTypeId } from "@duckdb/node-api";
8
+ var NUMERIC = /* @__PURE__ */ new Set([
9
+ DuckDBTypeId.TINYINT,
10
+ DuckDBTypeId.SMALLINT,
11
+ DuckDBTypeId.INTEGER,
12
+ DuckDBTypeId.BIGINT,
13
+ DuckDBTypeId.HUGEINT,
14
+ DuckDBTypeId.UTINYINT,
15
+ DuckDBTypeId.USMALLINT,
16
+ DuckDBTypeId.UINTEGER,
17
+ DuckDBTypeId.UBIGINT,
18
+ DuckDBTypeId.UHUGEINT,
19
+ DuckDBTypeId.FLOAT,
20
+ DuckDBTypeId.DOUBLE,
21
+ DuckDBTypeId.DECIMAL
22
+ ]);
23
+ var TEMPORAL = /* @__PURE__ */ new Set([
24
+ DuckDBTypeId.DATE,
25
+ DuckDBTypeId.TIMESTAMP,
26
+ DuckDBTypeId.TIMESTAMP_S,
27
+ DuckDBTypeId.TIMESTAMP_MS,
28
+ DuckDBTypeId.TIMESTAMP_NS,
29
+ DuckDBTypeId.TIMESTAMP_TZ
30
+ ]);
31
+ function duckKind(typeId) {
32
+ if (NUMERIC.has(typeId)) return "number";
33
+ if (TEMPORAL.has(typeId)) return "time";
34
+ if (typeId === DuckDBTypeId.BOOLEAN) return "boolean";
35
+ return "string";
36
+ }
37
+ function duckDbToChartData(rows, columns) {
38
+ const cols = columns.map((c) => ({ name: c.name, type: c.type.typeId }));
39
+ return buildChartData({ rows, columns: cols, mapKind: (typeId) => duckKind(typeId) });
40
+ }
41
+
42
+ // src/duckdb.ts
43
+ function duckDbRunSql(connection, opts = {}) {
44
+ return async (sql) => {
45
+ if (opts.readOnly !== false) assertReadOnlySql(sql);
46
+ const reader = await connection.runAndReadAll(sql);
47
+ const names = reader.columnNames();
48
+ const types = reader.columnTypes();
49
+ const columns = names.map((name, i) => ({ name, type: types[i] }));
50
+ const rows = reader.getRowObjectsJson();
51
+ return duckDbToChartData(rows, columns);
52
+ };
53
+ }
54
+ export {
55
+ duckDbRunSql,
56
+ duckDbToChartData
57
+ };
58
+ //# sourceMappingURL=duckdb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/duckdb.ts","../src/duckdb.ts"],"sourcesContent":["// DuckDB -> ChartData adapter. Engine-specific pieces only: the type map (DuckDBTypeId -> FieldKind)\n// and the column shape. Rows come from reader.getRowObjectsJson() (JSON-safe values), so the shared\n// defaultNormalizeCell handles coercion; the FieldMeta assembly and ChartData shape come from ./sql.\nimport { DuckDBTypeId } from \"@duckdb/node-api\";\nimport type { DuckDBType } from \"@duckdb/node-api\";\nimport type { ChartData, FieldKind } from \"../types.js\";\nimport { buildChartData, type SourceColumn } from \"./sql.js\";\n\nconst NUMERIC = new Set<DuckDBTypeId>([\n DuckDBTypeId.TINYINT,\n DuckDBTypeId.SMALLINT,\n DuckDBTypeId.INTEGER,\n DuckDBTypeId.BIGINT,\n DuckDBTypeId.HUGEINT,\n DuckDBTypeId.UTINYINT,\n DuckDBTypeId.USMALLINT,\n DuckDBTypeId.UINTEGER,\n DuckDBTypeId.UBIGINT,\n DuckDBTypeId.UHUGEINT,\n DuckDBTypeId.FLOAT,\n DuckDBTypeId.DOUBLE,\n DuckDBTypeId.DECIMAL,\n]);\nconst TEMPORAL = new Set<DuckDBTypeId>([\n DuckDBTypeId.DATE,\n DuckDBTypeId.TIMESTAMP,\n DuckDBTypeId.TIMESTAMP_S,\n DuckDBTypeId.TIMESTAMP_MS,\n DuckDBTypeId.TIMESTAMP_NS,\n DuckDBTypeId.TIMESTAMP_TZ,\n]);\n\nfunction duckKind(typeId: DuckDBTypeId): FieldKind {\n if (NUMERIC.has(typeId)) return \"number\";\n if (TEMPORAL.has(typeId)) return \"time\";\n if (typeId === DuckDBTypeId.BOOLEAN) return \"boolean\";\n return \"string\"; // VARCHAR, TIME, BLOB, UUID, LIST/STRUCT/MAP (stringified), ...\n}\n\n/** A DuckDB result column: its name + the DuckDBType (from reader.columnTypes()). */\nexport interface DuckDbColumn {\n name: string;\n type: DuckDBType;\n}\n\n/**\n * Convert a DuckDB result into ChartData.\n * @param rows Row objects from `reader.getRowObjectsJson()` (JSON-safe scalar values).\n * @param columns Column name + DuckDBType pairs (from `reader.columnNames()` + `reader.columnTypes()`).\n */\nexport function duckDbToChartData(rows: Record<string, unknown>[], columns: DuckDbColumn[]): ChartData {\n const cols: SourceColumn[] = columns.map((c) => ({ name: c.name, type: c.type.typeId }));\n return buildChartData({ rows, columns: cols, mapKind: (typeId) => duckKind(typeId as DuckDBTypeId) });\n}\n","// @bonnard/mcp-charts/duckdb — drop-in DuckDB adapter for addCharts.\n//\n// The caller owns the instance + connection; this packages the dev-side `runSql`: a read-only\n// check plus the result -> ChartData transform. For engine-enforced read-only, open a file-backed\n// instance with `{ access_mode: \"READ_ONLY\" }`.\n//\n// import { addCharts } from \"@bonnard/mcp-charts\";\n// import { duckDbRunSql } from \"@bonnard/mcp-charts/duckdb\";\n// import { DuckDBInstance } from \"@duckdb/node-api\";\n//\n// const instance = await DuckDBInstance.create(\"analytics.duckdb\", { access_mode: \"READ_ONLY\" });\n// const connection = await instance.connect();\n// addCharts(server, { runSql: duckDbRunSql(connection), discovery: { toolName: \"explore_schema\" } });\n//\n// `@duckdb/node-api` is an OPTIONAL peer — only needed if you import this subpath.\nimport type { DuckDBConnection } from \"@duckdb/node-api\";\nimport type { ChartData } from \"./types.js\";\nimport { assertReadOnlySql } from \"./adapters/sql.js\";\nimport { duckDbToChartData } from \"./adapters/duckdb.js\";\n\nexport { duckDbToChartData, type DuckDbColumn } from \"./adapters/duckdb.js\";\n\nexport interface DuckDbRunSqlOptions {\n /** Reject non-SELECT statements. Default: true. A read-only instance is the real boundary. */\n readOnly?: boolean;\n}\n\n/** Build the `runSql` callback for `addCharts` from a DuckDB connection. */\nexport function duckDbRunSql(\n connection: DuckDBConnection,\n opts: DuckDbRunSqlOptions = {},\n): (sql: string) => Promise<ChartData> {\n return async (sql: string): Promise<ChartData> => {\n if (opts.readOnly !== false) assertReadOnlySql(sql);\n const reader = await connection.runAndReadAll(sql);\n const names = reader.columnNames();\n const types = reader.columnTypes();\n const columns = names.map((name, i) => ({ name, type: types[i]! }));\n const rows = reader.getRowObjectsJson() as Record<string, unknown>[];\n return duckDbToChartData(rows, columns);\n };\n}\n"],"mappings":";;;;;;AAGA,SAAS,oBAAoB;AAK7B,IAAM,UAAU,oBAAI,IAAkB;AAAA,EACpC,aAAa;AAAA,EACb,aAAa;AAAA,EACb,aAAa;AAAA,EACb,aAAa;AAAA,EACb,aAAa;AAAA,EACb,aAAa;AAAA,EACb,aAAa;AAAA,EACb,aAAa;AAAA,EACb,aAAa;AAAA,EACb,aAAa;AAAA,EACb,aAAa;AAAA,EACb,aAAa;AAAA,EACb,aAAa;AACf,CAAC;AACD,IAAM,WAAW,oBAAI,IAAkB;AAAA,EACrC,aAAa;AAAA,EACb,aAAa;AAAA,EACb,aAAa;AAAA,EACb,aAAa;AAAA,EACb,aAAa;AAAA,EACb,aAAa;AACf,CAAC;AAED,SAAS,SAAS,QAAiC;AACjD,MAAI,QAAQ,IAAI,MAAM,EAAG,QAAO;AAChC,MAAI,SAAS,IAAI,MAAM,EAAG,QAAO;AACjC,MAAI,WAAW,aAAa,QAAS,QAAO;AAC5C,SAAO;AACT;AAaO,SAAS,kBAAkB,MAAiC,SAAoC;AACrG,QAAM,OAAuB,QAAQ,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,EAAE,KAAK,OAAO,EAAE;AACvF,SAAO,eAAe,EAAE,MAAM,SAAS,MAAM,SAAS,CAAC,WAAW,SAAS,MAAsB,EAAE,CAAC;AACtG;;;ACzBO,SAAS,aACd,YACA,OAA4B,CAAC,GACQ;AACrC,SAAO,OAAO,QAAoC;AAChD,QAAI,KAAK,aAAa,MAAO,mBAAkB,GAAG;AAClD,UAAM,SAAS,MAAM,WAAW,cAAc,GAAG;AACjD,UAAM,QAAQ,OAAO,YAAY;AACjC,UAAM,QAAQ,OAAO,YAAY;AACjC,UAAM,UAAU,MAAM,IAAI,CAAC,MAAM,OAAO,EAAE,MAAM,MAAM,MAAM,CAAC,EAAG,EAAE;AAClE,UAAM,OAAO,OAAO,kBAAkB;AACtC,WAAO,kBAAkB,MAAM,OAAO;AAAA,EACxC;AACF;","names":[]}
@@ -0,0 +1,58 @@
1
+ import { C as ChartData, R as ResolveOptions, a as ChartSpec, F as FieldMeta, b as ChartContext, c as ChartType, d as FieldKind } from './types-CHiUU9mj.js';
2
+ export { A as AxisSpec, e as ColumnSpec, E as Encode, f as FieldFormat, g as FieldRole, h as ReferenceLine, S as SeriesSpec, i as Stacking, T as TimeGranularity } from './types-CHiUU9mj.js';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+
5
+ declare function resolve(data: ChartData, opts?: ResolveOptions): ChartSpec;
6
+
7
+ /** Complete field metadata for every column, declared values taking precedence. */
8
+ declare function inferFields(data: ChartData): FieldMeta[];
9
+
10
+ /** Options for addCharts. */
11
+ interface AddChartsOptions {
12
+ /** SQL mode: the agent writes SQL; you execute it read-only and return rows. */
13
+ runSql?: (sql: string, ctx: ChartContext) => Promise<ChartData>;
14
+ /** Which chart types the agent may use (default: all). */
15
+ allow?: ChartType[];
16
+ /** Names the dev's schema-discovery tool so the agent is told to call it first. */
17
+ discovery?: {
18
+ toolName: string;
19
+ };
20
+ /** Override the tool name (default: "visualize"). */
21
+ toolName?: string;
22
+ }
23
+ /** Register the generic `visualize` tool on an MCP server. */
24
+ declare function addCharts(server: McpServer, options: AddChartsOptions): void;
25
+
26
+ /** A source column: its name + the engine's declared type, in whatever shape the driver reports. */
27
+ interface SourceColumn {
28
+ name: string;
29
+ /** Engine type, passed verbatim to mapKind (e.g. "INT64", a Postgres OID, "VARCHAR", a field object). */
30
+ type: unknown;
31
+ }
32
+ /** Map an engine's column type to our FieldKind. The one piece of real per-engine work. */
33
+ type KindMapper = (type: unknown, column: SourceColumn) => FieldKind;
34
+ /** Coerce one cell value to a flat scalar, guided by the resolved kind. */
35
+ type CellNormalizer = (value: unknown, kind: FieldKind, column: SourceColumn) => unknown;
36
+ interface BuildChartDataOptions {
37
+ rows: Record<string, unknown>[];
38
+ columns: SourceColumn[];
39
+ mapKind: KindMapper;
40
+ /** Optional; defaults to {@link defaultNormalizeCell}, which suits drivers that return JS-native values. */
41
+ normalizeCell?: CellNormalizer;
42
+ }
43
+ /** Default cell normalizer: pass scalars through, coerce numbers/booleans, stringify non-scalars.
44
+ * Handles `{ value }` wrapper objects (some drivers wrap typed values). Adapters whose driver
45
+ * needs special handling (e.g. BigQuery's typed-string + wrapper mix) supply their own. */
46
+ declare function defaultNormalizeCell(value: unknown, kind: FieldKind): unknown;
47
+ /**
48
+ * Turn a SQL result (rows + column types) into typed ChartData. Builds a complete FieldMeta per
49
+ * column from the engine kind + name-based format/granularity hints, then normalizes every cell.
50
+ */
51
+ declare function buildChartData({ rows, columns, mapKind, normalizeCell, }: BuildChartDataOptions): ChartData;
52
+ /**
53
+ * Reject anything that isn't a single SELECT/WITH statement. A cheap check on LLM-authored SQL;
54
+ * enforce real read-only access with a database role.
55
+ */
56
+ declare function assertReadOnlySql(sql: string): void;
57
+
58
+ export { type AddChartsOptions, type BuildChartDataOptions, type CellNormalizer, ChartContext, ChartData, ChartSpec, ChartType, FieldKind, FieldMeta, type KindMapper, ResolveOptions, type SourceColumn, addCharts, assertReadOnlySql, buildChartData, defaultNormalizeCell, inferFields, resolve };