@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.
@@ -0,0 +1,23 @@
1
+ import { Pool } from 'pg';
2
+ import { C as ChartData } from './types-CHiUU9mj.js';
3
+
4
+ /** A Postgres result column: name + the type OID (`dataTypeID` from pg/PGlite result `fields`). */
5
+ interface PostgresField {
6
+ name: string;
7
+ dataTypeID: number;
8
+ }
9
+ /**
10
+ * Convert a Postgres result into ChartData.
11
+ * @param rows Result rows (objects), as returned by node-postgres or PGlite (`result.rows`).
12
+ * @param fields Result columns (`result.fields`: name + dataTypeID).
13
+ */
14
+ declare function postgresToChartData(rows: Record<string, unknown>[], fields: PostgresField[]): ChartData;
15
+
16
+ interface PostgresRunSqlOptions {
17
+ /** Reject non-SELECT statements + run inside a READ ONLY transaction. Default: true. */
18
+ readOnly?: boolean;
19
+ }
20
+ /** Build the `runSql` callback for `addCharts` from a pg Pool. */
21
+ declare function postgresRunSql(pool: Pool, opts?: PostgresRunSqlOptions): (sql: string) => Promise<ChartData>;
22
+
23
+ export { type PostgresField, type PostgresRunSqlOptions, postgresRunSql, postgresToChartData };
@@ -0,0 +1,70 @@
1
+ import {
2
+ assertReadOnlySql,
3
+ buildChartData,
4
+ defaultNormalizeCell
5
+ } from "./chunk-NGXV4I2L.js";
6
+
7
+ // src/adapters/postgres.ts
8
+ var OID = {
9
+ BOOL: 16,
10
+ INT8: 20,
11
+ INT2: 21,
12
+ INT4: 23,
13
+ OID: 26,
14
+ FLOAT4: 700,
15
+ FLOAT8: 701,
16
+ NUMERIC: 1700,
17
+ DATE: 1082,
18
+ TIMESTAMP: 1114,
19
+ TIMESTAMPTZ: 1184
20
+ };
21
+ var NUMERIC = /* @__PURE__ */ new Set([OID.INT8, OID.INT2, OID.INT4, OID.OID, OID.FLOAT4, OID.FLOAT8, OID.NUMERIC]);
22
+ var TEMPORAL = /* @__PURE__ */ new Set([OID.DATE, OID.TIMESTAMP, OID.TIMESTAMPTZ]);
23
+ function pgKind(oid) {
24
+ if (NUMERIC.has(oid)) return "number";
25
+ if (TEMPORAL.has(oid)) return "time";
26
+ if (oid === OID.BOOL) return "boolean";
27
+ return "string";
28
+ }
29
+ function pgNormalize(value, kind, column) {
30
+ if (kind === "time" && value instanceof Date) {
31
+ if (column.type === OID.DATE) {
32
+ const y = value.getFullYear();
33
+ const m = String(value.getMonth() + 1).padStart(2, "0");
34
+ const d = String(value.getDate()).padStart(2, "0");
35
+ return `${y}-${m}-${d}`;
36
+ }
37
+ return value.toISOString();
38
+ }
39
+ return defaultNormalizeCell(value, kind);
40
+ }
41
+ function postgresToChartData(rows, fields) {
42
+ const columns = fields.map((f) => ({ name: f.name, type: f.dataTypeID }));
43
+ return buildChartData({ rows, columns, mapKind: (oid) => pgKind(oid), normalizeCell: pgNormalize });
44
+ }
45
+
46
+ // src/postgres.ts
47
+ function postgresRunSql(pool, opts = {}) {
48
+ const readOnly = opts.readOnly !== false;
49
+ return async (sql) => {
50
+ if (readOnly) assertReadOnlySql(sql);
51
+ const client = await pool.connect();
52
+ try {
53
+ if (readOnly) await client.query("BEGIN TRANSACTION READ ONLY");
54
+ const res = await client.query(sql);
55
+ if (readOnly) await client.query("ROLLBACK");
56
+ return postgresToChartData(res.rows, res.fields);
57
+ } catch (err) {
58
+ if (readOnly) await client.query("ROLLBACK").catch(() => {
59
+ });
60
+ throw err;
61
+ } finally {
62
+ client.release();
63
+ }
64
+ };
65
+ }
66
+ export {
67
+ postgresRunSql,
68
+ postgresToChartData
69
+ };
70
+ //# sourceMappingURL=postgres.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/postgres.ts","../src/postgres.ts"],"sourcesContent":["// Postgres -> ChartData adapter. Engine-specific pieces only: the OID -> FieldKind map and a\n// date-safe cell normalizer. Works for any driver whose result exposes { name, dataTypeID } columns\n// (node-postgres and PGlite both do). FieldMeta assembly + ChartData shape come from ./sql.\nimport type { ChartData, FieldKind } from \"../types.js\";\nimport { buildChartData, defaultNormalizeCell, type SourceColumn } from \"./sql.js\";\n\n// Postgres builtin type OIDs. These are fixed system-catalog identifiers (part of the wire\n// protocol), stable across versions — the same constants pg-types ships as `builtins`.\nconst OID = {\n BOOL: 16,\n INT8: 20,\n INT2: 21,\n INT4: 23,\n OID: 26,\n FLOAT4: 700,\n FLOAT8: 701,\n NUMERIC: 1700,\n DATE: 1082,\n TIMESTAMP: 1114,\n TIMESTAMPTZ: 1184,\n} as const;\n\nconst NUMERIC = new Set<number>([OID.INT8, OID.INT2, OID.INT4, OID.OID, OID.FLOAT4, OID.FLOAT8, OID.NUMERIC]);\nconst TEMPORAL = new Set<number>([OID.DATE, OID.TIMESTAMP, OID.TIMESTAMPTZ]);\n\nfunction pgKind(oid: number): FieldKind {\n if (NUMERIC.has(oid)) return \"number\"; // pg returns int8/numeric as strings; buildChartData coerces\n if (TEMPORAL.has(oid)) return \"time\";\n if (oid === OID.BOOL) return \"boolean\";\n return \"string\"; // text/varchar/json/uuid/time/arrays (stringified) ...\n}\n\n// pg parses date/timestamp columns to JS Date. For DATE, format from local components (pg builds the\n// Date at local midnight) to avoid a UTC day-shift; timestamps keep full ISO. Everything else uses\n// the shared default (numeric-string -> number, arrays/objects -> stringified).\nfunction pgNormalize(value: unknown, kind: FieldKind, column: SourceColumn): unknown {\n if (kind === \"time\" && value instanceof Date) {\n if (column.type === OID.DATE) {\n const y = value.getFullYear();\n const m = String(value.getMonth() + 1).padStart(2, \"0\");\n const d = String(value.getDate()).padStart(2, \"0\");\n return `${y}-${m}-${d}`;\n }\n return value.toISOString();\n }\n return defaultNormalizeCell(value, kind);\n}\n\n/** A Postgres result column: name + the type OID (`dataTypeID` from pg/PGlite result `fields`). */\nexport interface PostgresField {\n name: string;\n dataTypeID: number;\n}\n\n/**\n * Convert a Postgres result into ChartData.\n * @param rows Result rows (objects), as returned by node-postgres or PGlite (`result.rows`).\n * @param fields Result columns (`result.fields`: name + dataTypeID).\n */\nexport function postgresToChartData(rows: Record<string, unknown>[], fields: PostgresField[]): ChartData {\n const columns: SourceColumn[] = fields.map((f) => ({ name: f.name, type: f.dataTypeID }));\n return buildChartData({ rows, columns, mapKind: (oid) => pgKind(oid as number), normalizeCell: pgNormalize });\n}\n","// @bonnard/mcp-charts/postgres — drop-in Postgres adapter for addCharts. Works with any\n// Postgres-wire endpoint, including a Cube SQL API or a Redshift cluster.\n//\n// The caller owns the pool + credentials; this packages the dev-side `runSql`: each query runs\n// inside a `READ ONLY` transaction so Postgres itself rejects writes, plus the result -> ChartData\n// transform. A read-only database role is still the real boundary.\n//\n// import { addCharts } from \"@bonnard/mcp-charts\";\n// import { postgresRunSql } from \"@bonnard/mcp-charts/postgres\";\n// import { Pool } from \"pg\";\n//\n// const pool = new Pool({ connectionString: process.env.DATABASE_URL });\n// addCharts(server, { runSql: postgresRunSql(pool), discovery: { toolName: \"explore_schema\" } });\n//\n// `pg` is an OPTIONAL peer — only needed if you import this subpath. The type import below is erased\n// at build, and the OID map is self-contained, so this module adds no runtime dependency.\nimport type { Pool } from \"pg\";\nimport type { ChartData } from \"./types.js\";\nimport { assertReadOnlySql } from \"./adapters/sql.js\";\nimport { postgresToChartData } from \"./adapters/postgres.js\";\n\nexport { postgresToChartData, type PostgresField } from \"./adapters/postgres.js\";\n\nexport interface PostgresRunSqlOptions {\n /** Reject non-SELECT statements + run inside a READ ONLY transaction. Default: true. */\n readOnly?: boolean;\n}\n\n/** Build the `runSql` callback for `addCharts` from a pg Pool. */\nexport function postgresRunSql(pool: Pool, opts: PostgresRunSqlOptions = {}): (sql: string) => Promise<ChartData> {\n const readOnly = opts.readOnly !== false;\n return async (sql: string): Promise<ChartData> => {\n if (readOnly) assertReadOnlySql(sql);\n // A dedicated connection so BEGIN/ROLLBACK bracket the same session (a Pool may round-robin).\n const client = await pool.connect();\n try {\n if (readOnly) await client.query(\"BEGIN TRANSACTION READ ONLY\");\n const res = await client.query(sql);\n if (readOnly) await client.query(\"ROLLBACK\");\n return postgresToChartData(res.rows, res.fields);\n } catch (err) {\n if (readOnly) await client.query(\"ROLLBACK\").catch(() => {});\n throw err;\n } finally {\n client.release();\n }\n };\n}\n"],"mappings":";;;;;;;AAQA,IAAM,MAAM;AAAA,EACV,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AAAA,EACN,WAAW;AAAA,EACX,aAAa;AACf;AAEA,IAAM,UAAU,oBAAI,IAAY,CAAC,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,IAAI,KAAK,IAAI,QAAQ,IAAI,QAAQ,IAAI,OAAO,CAAC;AAC5G,IAAM,WAAW,oBAAI,IAAY,CAAC,IAAI,MAAM,IAAI,WAAW,IAAI,WAAW,CAAC;AAE3E,SAAS,OAAO,KAAwB;AACtC,MAAI,QAAQ,IAAI,GAAG,EAAG,QAAO;AAC7B,MAAI,SAAS,IAAI,GAAG,EAAG,QAAO;AAC9B,MAAI,QAAQ,IAAI,KAAM,QAAO;AAC7B,SAAO;AACT;AAKA,SAAS,YAAY,OAAgB,MAAiB,QAA+B;AACnF,MAAI,SAAS,UAAU,iBAAiB,MAAM;AAC5C,QAAI,OAAO,SAAS,IAAI,MAAM;AAC5B,YAAM,IAAI,MAAM,YAAY;AAC5B,YAAM,IAAI,OAAO,MAAM,SAAS,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG;AACtD,YAAM,IAAI,OAAO,MAAM,QAAQ,CAAC,EAAE,SAAS,GAAG,GAAG;AACjD,aAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;AAAA,IACvB;AACA,WAAO,MAAM,YAAY;AAAA,EAC3B;AACA,SAAO,qBAAqB,OAAO,IAAI;AACzC;AAaO,SAAS,oBAAoB,MAAiC,QAAoC;AACvG,QAAM,UAA0B,OAAO,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,EAAE,WAAW,EAAE;AACxF,SAAO,eAAe,EAAE,MAAM,SAAS,SAAS,CAAC,QAAQ,OAAO,GAAa,GAAG,eAAe,YAAY,CAAC;AAC9G;;;ACjCO,SAAS,eAAe,MAAY,OAA8B,CAAC,GAAwC;AAChH,QAAM,WAAW,KAAK,aAAa;AACnC,SAAO,OAAO,QAAoC;AAChD,QAAI,SAAU,mBAAkB,GAAG;AAEnC,UAAM,SAAS,MAAM,KAAK,QAAQ;AAClC,QAAI;AACF,UAAI,SAAU,OAAM,OAAO,MAAM,6BAA6B;AAC9D,YAAM,MAAM,MAAM,OAAO,MAAM,GAAG;AAClC,UAAI,SAAU,OAAM,OAAO,MAAM,UAAU;AAC3C,aAAO,oBAAoB,IAAI,MAAM,IAAI,MAAM;AAAA,IACjD,SAAS,KAAK;AACZ,UAAI,SAAU,OAAM,OAAO,MAAM,UAAU,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAC3D,YAAM;AAAA,IACR,UAAE;AACA,aAAO,QAAQ;AAAA,IACjB;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,23 @@
1
+ import { Connection } from 'snowflake-sdk';
2
+ import { C as ChartData } from './types-CHiUU9mj.js';
3
+
4
+ /** A Snowflake result column: name + the logical type from `Column.getType()`. */
5
+ interface SnowflakeColumn {
6
+ name: string;
7
+ type: string;
8
+ }
9
+ /**
10
+ * Convert a Snowflake result into ChartData.
11
+ * @param rows Row objects from the `complete(err, stmt, rows)` callback (rowMode "object").
12
+ * @param columns Column name + logical type, from `stmt.getColumns().map(c => ({ name: c.getName(), type: c.getType() }))`.
13
+ */
14
+ declare function snowflakeToChartData(rows: Record<string, unknown>[], columns: SnowflakeColumn[]): ChartData;
15
+
16
+ interface SnowflakeRunSqlOptions {
17
+ /** Reject non-SELECT statements. Default: true. A read-only role is the real boundary. */
18
+ readOnly?: boolean;
19
+ }
20
+ /** Build the `runSql` callback for `addCharts` from a connected snowflake-sdk Connection. */
21
+ declare function snowflakeRunSql(connection: Connection, opts?: SnowflakeRunSqlOptions): (sql: string) => Promise<ChartData>;
22
+
23
+ export { type SnowflakeColumn, type SnowflakeRunSqlOptions, snowflakeRunSql, snowflakeToChartData };
@@ -0,0 +1,65 @@
1
+ import {
2
+ assertReadOnlySql,
3
+ buildChartData,
4
+ defaultNormalizeCell
5
+ } from "./chunk-NGXV4I2L.js";
6
+
7
+ // src/adapters/snowflake.ts
8
+ var NUMERIC = /* @__PURE__ */ new Set(["fixed", "real"]);
9
+ var TEMPORAL = /* @__PURE__ */ new Set(["date", "timestamp_ltz", "timestamp_ntz", "timestamp_tz"]);
10
+ function snowflakeKind(type) {
11
+ const t = type.toLowerCase();
12
+ if (NUMERIC.has(t)) return "number";
13
+ if (TEMPORAL.has(t)) return "time";
14
+ if (t === "boolean") return "boolean";
15
+ return "string";
16
+ }
17
+ function snowflakeNormalize(value, kind, column) {
18
+ if (kind === "time" && value instanceof Date && String(column.type).toLowerCase() === "date") {
19
+ const y = value.getUTCFullYear();
20
+ const m = String(value.getUTCMonth() + 1).padStart(2, "0");
21
+ const d = String(value.getUTCDate()).padStart(2, "0");
22
+ return `${y}-${m}-${d}`;
23
+ }
24
+ return defaultNormalizeCell(value, kind);
25
+ }
26
+ function snowflakeToChartData(rows, columns) {
27
+ const cols = columns.map((c) => ({ name: c.name, type: c.type }));
28
+ return buildChartData({
29
+ rows,
30
+ columns: cols,
31
+ mapKind: (type) => snowflakeKind(type),
32
+ normalizeCell: snowflakeNormalize
33
+ });
34
+ }
35
+
36
+ // src/snowflake.ts
37
+ function snowflakeRunSql(connection, opts = {}) {
38
+ const readOnly = opts.readOnly !== false;
39
+ return (sql) => new Promise((resolve, reject) => {
40
+ if (readOnly) {
41
+ try {
42
+ assertReadOnlySql(sql);
43
+ } catch (err) {
44
+ reject(err instanceof Error ? err : new Error(String(err)));
45
+ return;
46
+ }
47
+ }
48
+ connection.execute({
49
+ sqlText: sql,
50
+ complete: (err, stmt, rows) => {
51
+ if (err) {
52
+ reject(err);
53
+ return;
54
+ }
55
+ const columns = (stmt.getColumns() ?? []).map((c) => ({ name: c.getName(), type: c.getType() }));
56
+ resolve(snowflakeToChartData(rows ?? [], columns));
57
+ }
58
+ });
59
+ });
60
+ }
61
+ export {
62
+ snowflakeRunSql,
63
+ snowflakeToChartData
64
+ };
65
+ //# sourceMappingURL=snowflake.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/snowflake.ts","../src/snowflake.ts"],"sourcesContent":["// Snowflake -> ChartData. Engine-specific only: map Snowflake's logical column types (as reported\n// by snowflake-sdk's Column.getType()) to FieldKind, plus date-safe normalization. FieldMeta\n// assembly + ChartData shape come from ./sql.\nimport type { ChartData, FieldKind } from \"../types.js\";\nimport { buildChartData, defaultNormalizeCell, type SourceColumn } from \"./sql.js\";\n\n// snowflake-sdk Column.getType() returns lowercase logical types:\n// NUMBER/DECIMAL/INT -> \"fixed\", FLOAT/DOUBLE -> \"real\", VARCHAR/STRING -> \"text\", etc.\nconst NUMERIC = new Set([\"fixed\", \"real\"]);\nconst TEMPORAL = new Set([\"date\", \"timestamp_ltz\", \"timestamp_ntz\", \"timestamp_tz\"]);\n\nfunction snowflakeKind(type: string): FieldKind {\n const t = type.toLowerCase();\n if (NUMERIC.has(t)) return \"number\";\n if (TEMPORAL.has(t)) return \"time\";\n if (t === \"boolean\") return \"boolean\";\n return \"string\"; // text, time, variant, object, array, binary, ...\n}\n\n// Snowflake DATE columns come back as JS Date (UTC midnight); keep the calendar day. Timestamps\n// keep full ISO. Everything else uses the shared default (numeric coercion, non-scalar stringify).\nfunction snowflakeNormalize(value: unknown, kind: FieldKind, column: SourceColumn): unknown {\n if (kind === \"time\" && value instanceof Date && String(column.type).toLowerCase() === \"date\") {\n const y = value.getUTCFullYear();\n const m = String(value.getUTCMonth() + 1).padStart(2, \"0\");\n const d = String(value.getUTCDate()).padStart(2, \"0\");\n return `${y}-${m}-${d}`;\n }\n return defaultNormalizeCell(value, kind);\n}\n\n/** A Snowflake result column: name + the logical type from `Column.getType()`. */\nexport interface SnowflakeColumn {\n name: string;\n type: string;\n}\n\n/**\n * Convert a Snowflake result into ChartData.\n * @param rows Row objects from the `complete(err, stmt, rows)` callback (rowMode \"object\").\n * @param columns Column name + logical type, from `stmt.getColumns().map(c => ({ name: c.getName(), type: c.getType() }))`.\n */\nexport function snowflakeToChartData(rows: Record<string, unknown>[], columns: SnowflakeColumn[]): ChartData {\n const cols: SourceColumn[] = columns.map((c) => ({ name: c.name, type: c.type }));\n return buildChartData({\n rows,\n columns: cols,\n mapKind: (type) => snowflakeKind(type as string),\n normalizeCell: snowflakeNormalize,\n });\n}\n","// @bonnard/mcp-charts/snowflake — drop-in Snowflake adapter for addCharts.\n//\n// The caller owns the connection + auth; this packages the dev-side `runSql`: a read-only check\n// plus the result -> ChartData transform. Use a read-only role / warehouse as the real boundary.\n//\n// import { addCharts } from \"@bonnard/mcp-charts\";\n// import { snowflakeRunSql } from \"@bonnard/mcp-charts/snowflake\";\n// import snowflake from \"snowflake-sdk\";\n//\n// const connection = snowflake.createConnection({ account, username, ... });\n// await new Promise((res, rej) => connection.connect((e) => (e ? rej(e) : res(null))));\n// addCharts(server, { runSql: snowflakeRunSql(connection), discovery: { toolName: \"explore_schema\" } });\n//\n// `snowflake-sdk` is an OPTIONAL peer — only needed if you import this subpath.\nimport type { Connection } from \"snowflake-sdk\";\nimport type { ChartData } from \"./types.js\";\nimport { assertReadOnlySql } from \"./adapters/sql.js\";\nimport { snowflakeToChartData } from \"./adapters/snowflake.js\";\n\nexport { snowflakeToChartData, type SnowflakeColumn } from \"./adapters/snowflake.js\";\n\nexport interface SnowflakeRunSqlOptions {\n /** Reject non-SELECT statements. Default: true. A read-only role is the real boundary. */\n readOnly?: boolean;\n}\n\n/** Build the `runSql` callback for `addCharts` from a connected snowflake-sdk Connection. */\nexport function snowflakeRunSql(\n connection: Connection,\n opts: SnowflakeRunSqlOptions = {},\n): (sql: string) => Promise<ChartData> {\n const readOnly = opts.readOnly !== false;\n return (sql: string): Promise<ChartData> =>\n new Promise<ChartData>((resolve, reject) => {\n if (readOnly) {\n try {\n assertReadOnlySql(sql);\n } catch (err) {\n reject(err instanceof Error ? err : new Error(String(err)));\n return;\n }\n }\n connection.execute({\n sqlText: sql,\n complete: (err, stmt, rows) => {\n if (err) {\n reject(err);\n return;\n }\n const columns = (stmt.getColumns() ?? []).map((c) => ({ name: c.getName(), type: c.getType() }));\n resolve(snowflakeToChartData((rows ?? []) as Record<string, unknown>[], columns));\n },\n });\n });\n}\n"],"mappings":";;;;;;;AAQA,IAAM,UAAU,oBAAI,IAAI,CAAC,SAAS,MAAM,CAAC;AACzC,IAAM,WAAW,oBAAI,IAAI,CAAC,QAAQ,iBAAiB,iBAAiB,cAAc,CAAC;AAEnF,SAAS,cAAc,MAAyB;AAC9C,QAAM,IAAI,KAAK,YAAY;AAC3B,MAAI,QAAQ,IAAI,CAAC,EAAG,QAAO;AAC3B,MAAI,SAAS,IAAI,CAAC,EAAG,QAAO;AAC5B,MAAI,MAAM,UAAW,QAAO;AAC5B,SAAO;AACT;AAIA,SAAS,mBAAmB,OAAgB,MAAiB,QAA+B;AAC1F,MAAI,SAAS,UAAU,iBAAiB,QAAQ,OAAO,OAAO,IAAI,EAAE,YAAY,MAAM,QAAQ;AAC5F,UAAM,IAAI,MAAM,eAAe;AAC/B,UAAM,IAAI,OAAO,MAAM,YAAY,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG;AACzD,UAAM,IAAI,OAAO,MAAM,WAAW,CAAC,EAAE,SAAS,GAAG,GAAG;AACpD,WAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;AAAA,EACvB;AACA,SAAO,qBAAqB,OAAO,IAAI;AACzC;AAaO,SAAS,qBAAqB,MAAiC,SAAuC;AAC3G,QAAM,OAAuB,QAAQ,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,EAAE,KAAK,EAAE;AAChF,SAAO,eAAe;AAAA,IACpB;AAAA,IACA,SAAS;AAAA,IACT,SAAS,CAAC,SAAS,cAAc,IAAc;AAAA,IAC/C,eAAe;AAAA,EACjB,CAAC;AACH;;;ACvBO,SAAS,gBACd,YACA,OAA+B,CAAC,GACK;AACrC,QAAM,WAAW,KAAK,aAAa;AACnC,SAAO,CAAC,QACN,IAAI,QAAmB,CAAC,SAAS,WAAW;AAC1C,QAAI,UAAU;AACZ,UAAI;AACF,0BAAkB,GAAG;AAAA,MACvB,SAAS,KAAK;AACZ,eAAO,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAC1D;AAAA,MACF;AAAA,IACF;AACA,eAAW,QAAQ;AAAA,MACjB,SAAS;AAAA,MACT,UAAU,CAAC,KAAK,MAAM,SAAS;AAC7B,YAAI,KAAK;AACP,iBAAO,GAAG;AACV;AAAA,QACF;AACA,cAAM,WAAW,KAAK,WAAW,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,GAAG,MAAM,EAAE,QAAQ,EAAE,EAAE;AAC/F,gBAAQ,qBAAsB,QAAQ,CAAC,GAAiC,OAAO,CAAC;AAAA,MAClF;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACL;","names":[]}
@@ -0,0 +1,115 @@
1
+ type ChartType = "line" | "bar" | "area" | "pie" | "scatter" | "funnel" | "waterfall" | "table";
2
+ /** What a field IS, for charting purposes. */
3
+ type FieldRole = "measure" | "dimension" | "time";
4
+ type FieldKind = "number" | "string" | "time" | "boolean";
5
+ type FieldFormat = "number" | "currency" | "percent";
6
+ type TimeGranularity = "day" | "week" | "month" | "quarter" | "year";
7
+ /** Per-field typing — the metadata `resolve()` needs. Semantic layers supply this for
8
+ * free; SQL drivers supply name + kind; ORM/custom devs declare it once. */
9
+ interface FieldMeta {
10
+ name: string;
11
+ role?: FieldRole;
12
+ kind?: FieldKind;
13
+ format?: FieldFormat;
14
+ label?: string;
15
+ granularity?: TimeGranularity;
16
+ additive?: boolean;
17
+ currency?: string;
18
+ }
19
+ /** Explicit encoding override, when field names don't make x/y/series obvious. */
20
+ interface Encode {
21
+ x?: string;
22
+ y?: string | string[];
23
+ series?: string;
24
+ /** Measure(s) to plot on a secondary (right) y-axis, drawn as a line over the primary chart. */
25
+ y2?: string | string[];
26
+ /** Measure(s) to draw as a line instead of bars on the SAME axis (a combo, e.g. actual vs target). */
27
+ line?: string | string[];
28
+ /** Scatter only: a 3rd measure mapped to point size (turns a scatter into a bubble chart). */
29
+ size?: string;
30
+ }
31
+ /** The normalized result every data callback returns. */
32
+ interface ChartData {
33
+ rows: Record<string, unknown>[];
34
+ fields?: FieldMeta[];
35
+ encode?: Encode;
36
+ }
37
+ /** Context passed to data callbacks — extensible bag so the signature never breaks. */
38
+ interface ChartContext {
39
+ tenant?: string;
40
+ roles?: string[];
41
+ userId?: string;
42
+ requestId?: string;
43
+ signal?: AbortSignal;
44
+ }
45
+ type Stacking = "stacked" | "grouped" | "stacked100";
46
+ interface SeriesSpec {
47
+ key: string;
48
+ label: string;
49
+ /** Which y-axis this series belongs to. "right" series render as a line (dual-axis combo). */
50
+ axis?: "left" | "right";
51
+ /** Render this series as a line instead of the chart's base type (same-axis combo). */
52
+ type?: "bar" | "line";
53
+ }
54
+ interface AxisSpec {
55
+ label?: string;
56
+ format?: FieldFormat;
57
+ granularity?: TimeGranularity;
58
+ currency?: string;
59
+ /** x-axis only: values are numeric, so line/area render on a linear (value) scale, not categories. */
60
+ numeric?: boolean;
61
+ }
62
+ interface ColumnSpec {
63
+ key: string;
64
+ label: string;
65
+ format?: FieldFormat;
66
+ granularity?: TimeGranularity;
67
+ currency?: string;
68
+ }
69
+ /** A horizontal reference line on the value axis (target, average, threshold). */
70
+ interface ReferenceLine {
71
+ value: number;
72
+ label: string;
73
+ }
74
+ /** Output of resolve() — everything a renderer needs, fully serializable. */
75
+ interface ChartSpec {
76
+ chartType: ChartType;
77
+ /** Cleaned + (if needed) pivoted/time-filled rows, ready to plot. */
78
+ data: Record<string, unknown>[];
79
+ /** x-axis key ("" for a single-measure table). */
80
+ x: string;
81
+ series: SeriesSpec[];
82
+ xAxis?: AxisSpec;
83
+ yAxis?: AxisSpec;
84
+ /** Secondary (right) y-axis descriptor, present only for dual-axis combo charts. */
85
+ yAxisRight?: AxisSpec;
86
+ legend: boolean;
87
+ stacking?: Stacking;
88
+ horizontal?: boolean;
89
+ title?: string;
90
+ columns?: ColumnSpec[];
91
+ /** Horizontal reference lines on the value axis (target / average). */
92
+ reference?: ReferenceLine[];
93
+ /** Scatter only: column mapped to point size (bubble chart). */
94
+ size?: string;
95
+ /** Scatter only: a dimension column used to label/identify each point (tooltip). */
96
+ pointLabel?: string;
97
+ /** Waterfall only: step labels that are totals (full bars anchored at 0), not floating deltas. */
98
+ totals?: string[];
99
+ /** Non-fatal advisories about how the data was massaged (e.g. summed unaggregated rows). */
100
+ notes?: string[];
101
+ }
102
+ /** Options to resolve(), typically sourced from the agent's tool args. */
103
+ interface ResolveOptions {
104
+ chartType?: ChartType | "auto";
105
+ title?: string;
106
+ stacking?: Stacking;
107
+ horizontal?: boolean;
108
+ /** Add reference lines: an average (computed) and/or a target (a value you pass). */
109
+ reference?: {
110
+ target?: number;
111
+ average?: boolean;
112
+ };
113
+ }
114
+
115
+ export type { AxisSpec as A, ChartData as C, Encode as E, FieldMeta as F, ResolveOptions as R, SeriesSpec as S, TimeGranularity as T, ChartSpec as a, ChartContext as b, ChartType as c, FieldKind as d, ColumnSpec as e, FieldFormat as f, FieldRole as g, ReferenceLine as h, Stacking as i };
package/package.json ADDED
@@ -0,0 +1,109 @@
1
+ {
2
+ "name": "@bonnard/mcp-charts",
3
+ "version": "0.1.1",
4
+ "description": "Add beautiful, agent-ready charts to your MCP server in a few lines.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Bonnard (bonnard-data)",
8
+ "homepage": "https://github.com/bonnard-data/mcp-charts#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/bonnard-data/mcp-charts.git",
12
+ "directory": "packages/core"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/bonnard-data/mcp-charts/issues"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js"
27
+ },
28
+ "./bigquery": {
29
+ "types": "./dist/bigquery.d.ts",
30
+ "import": "./dist/bigquery.js"
31
+ },
32
+ "./duckdb": {
33
+ "types": "./dist/duckdb.d.ts",
34
+ "import": "./dist/duckdb.js"
35
+ },
36
+ "./postgres": {
37
+ "types": "./dist/postgres.d.ts",
38
+ "import": "./dist/postgres.js"
39
+ },
40
+ "./snowflake": {
41
+ "types": "./dist/snowflake.d.ts",
42
+ "import": "./dist/snowflake.js"
43
+ },
44
+ "./databricks": {
45
+ "types": "./dist/databricks.d.ts",
46
+ "import": "./dist/databricks.js"
47
+ }
48
+ },
49
+ "scripts": {
50
+ "embed": "node scripts/embed-widget.mjs",
51
+ "prebuild": "node scripts/embed-widget.mjs",
52
+ "build": "tsup",
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "vitest run",
55
+ "check:exports": "publint && attw --pack . --profile esm-only"
56
+ },
57
+ "keywords": [
58
+ "mcp",
59
+ "model-context-protocol",
60
+ "charts",
61
+ "visualization",
62
+ "mcp-apps",
63
+ "ai"
64
+ ],
65
+ "engines": {
66
+ "node": ">=20"
67
+ },
68
+ "peerDependencies": {
69
+ "@databricks/sql": "^1.0.0",
70
+ "@duckdb/node-api": "^1.0.0",
71
+ "@google-cloud/bigquery": "^7.0.0 || ^8.0.0",
72
+ "@modelcontextprotocol/sdk": ">=1.27.0",
73
+ "pg": "^8.0.0",
74
+ "snowflake-sdk": "^1.9.0 || ^2.0.0 || ^3.0.0",
75
+ "zod": "^3.25.0 || ^4.0.0"
76
+ },
77
+ "peerDependenciesMeta": {
78
+ "@databricks/sql": {
79
+ "optional": true
80
+ },
81
+ "@duckdb/node-api": {
82
+ "optional": true
83
+ },
84
+ "@google-cloud/bigquery": {
85
+ "optional": true
86
+ },
87
+ "pg": {
88
+ "optional": true
89
+ },
90
+ "snowflake-sdk": {
91
+ "optional": true
92
+ }
93
+ },
94
+ "devDependencies": {
95
+ "@arethetypeswrong/cli": "^0.18.3",
96
+ "@databricks/sql": "^1.16.0",
97
+ "@duckdb/node-api": "1.5.4-r.1",
98
+ "@electric-sql/pglite": "^0.5.3",
99
+ "@google-cloud/bigquery": "^8.3.1",
100
+ "@modelcontextprotocol/sdk": "^1.29.0",
101
+ "@types/node": "^20.0.0",
102
+ "@types/pg": "^8.20.0",
103
+ "publint": "^0.3.21",
104
+ "snowflake-sdk": "^3.0.0",
105
+ "tsup": "^8.5.0",
106
+ "vitest": "^3.2.0",
107
+ "zod": "^4.4.0"
108
+ }
109
+ }