@slickfast/mcp 0.7.2 → 0.7.4

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.
Files changed (3) hide show
  1. package/README.md +62 -33
  2. package/dist/index.js +113 -3
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -1,37 +1,58 @@
1
- # SlickFast MCP server
1
+ # SlickFast charts & dashboards for AI agents
2
2
 
3
- Exposes the render-core engine as an MCP tool so an AI agent can generate charts.
3
+ SlickFast turns a small JSON spec into a polished, retina-quality chart **47 chart and
4
+ information-design types** (bar, line, pie, KPI, cards, funnel, gauge, heatmap, calendar,
5
+ gantt, waterfall…), plus **multiple charts tiled into a single dashboard image in one call**.
6
+ It runs as an [MCP](https://modelcontextprotocol.io) tool, so an AI agent hands it a spec and
7
+ gets back a finished PNG (or SVG). Everything renders **locally** — nothing leaves your machine.
4
8
 
5
- - **Tool:** `render_chart(spec)` → returns the chart as an SVG string. Spec contract
6
- is in `../../packages/render-core/SPEC.md` (also served as the `chart-spec` resource).
7
- - **Transport:** stdio (local). Streamable HTTP (remote/hosted, e.g. Railway) comes later.
8
-
9
- ## Run / test
10
- ```bash
11
- npm install
12
- node test-client.mjs # spawns the server, calls render_chart, checks the SVG
9
+ ```txt
10
+ render_chart({ type: "bar", data: { labels: ["Q1","Q2","Q3"], series: [{ values: [12,19,8] }] } })
11
+ a retina PNG, rendered on your machine
13
12
  ```
14
13
 
15
- ## Use it in Claude Desktop
16
- Add to `claude_desktop_config.json` (Settings → Developer → Edit Config):
17
- ```json
18
- {
19
- "mcpServers": {
20
- "slickfast": {
21
- "command": "node",
22
- "args": ["/Users/alexmacbook/SlickFast/platform/apps/mcp/server.mjs"]
23
- }
24
- }
25
- }
26
- ```
27
- Restart Claude Desktop, then ask it to "render a bar chart of …" — it calls `render_chart`.
14
+ ## Why it's built for agents
15
+
16
+ - **Deterministic — same spec, same bytes, forever.** No randomness, no timestamps, no
17
+ headless-browser drift. Outputs are **cacheable, testable, and reproducible**, with zero
18
+ flaky pixel diffs. Almost no charting tool can promise this — and it's exactly what a
19
+ tool-calling agent needs.
20
+ - **Never throws garbage at the model.** Empty data, a bad tile, a filtered-to-nothing series →
21
+ a clean, graceful frame, not a crash or a stack trace. Reliability is a feature when the
22
+ caller is an agent, not a human who can eyeball the error.
23
+ - **Fails loud on real mistakes.** An unknown palette or enum is *rejected* with a clear error
24
+ listing the valid options — so the agent fixes it immediately instead of silently shipping the
25
+ wrong-looking chart.
26
+ - **Good-looking by default.** A spec of just `{type, data}` renders a complete, well-designed
27
+ chart; fonts, colors, palettes, and size are optional overrides.
28
+ - **100% local & private.** Rendered and rasterized on your machine. Nothing is sent anywhere.
29
+
30
+ ## Tools
31
+
32
+ - **`render_chart(spec)`** → the chart as a PNG image (default) or SVG (`format: "svg"`). For a
33
+ dashboard, pass `type: "dashboard"` with `tiles: [{ chart, span }]` — each tile is a full spec
34
+ of any other type, composited into one image in a single render.
35
+ - **`describe_type(type)`** → the exact data shape, a minimal working spec, and per-type
36
+ gotchas. Call it first when you're unsure how to structure a type.
37
+ - **`gallery()`** → a curated demo gallery of example charts and dashboards — each as a
38
+ rendered image plus its spec. Just ask *"show me a demo"* or *"what can you make?"*.
39
+ - **`list_palettes()`** → every valid `palette` name, grouped into flat palettes and nested
40
+ themes, with their colors. Ask *"what palettes are available?"*.
41
+
42
+ ## Install
43
+
44
+ Add to your MCP config and restart — it runs locally via `npx`, no clone or build needed:
28
45
 
29
- ## Use it in Claude Code
30
- Project-scoped config (`.mcp.json` at the workspace root):
31
46
  ```json
32
- { "mcpServers": { "slickfast": { "command": "node", "args": ["/Users/alexmacbook/SlickFast/platform/apps/mcp/server.mjs"] } } }
47
+ { "mcpServers": { "slickfast": { "command": "npx", "args": ["-y", "@slickfast/mcp"] } } }
33
48
  ```
34
- Restart the session; the `render_chart` tool becomes available.
49
+
50
+ - **Claude Desktop** — `claude_desktop_config.json` (Settings → Developer → Edit Config)
51
+ - **Claude Code** — project `.mcp.json` at the workspace root
52
+ - **claude.ai** — connector settings
53
+
54
+ Then ask it to *"render a bar chart of last quarter's revenue"* or *"build a dashboard with an
55
+ MRR tile, a signups funnel, and a usage heatmap."*
35
56
 
36
57
  ## Seeing your charts (why an image might not show inline)
37
58
 
@@ -64,14 +85,22 @@ embedding, posting to Slack/X/email, or getting a chart into a surface that can'
64
85
  local image (see the table above). The agent should **offer this only when you ask to share
65
86
  or post** — it never auto-inserts links. (The endpoint stays unadvertised during soft-launch.)
66
87
 
67
- ## Install via npm (published)
68
- ```json
69
- { "mcpServers": { "slickfast": { "command": "npx", "args": ["-y", "@slickfast/mcp"] } } }
70
- ```
71
- Restart Claude; everything runs locally on your machine — nothing leaves.
72
-
73
88
  ## License
89
+
74
90
  **AGPL-3.0-only.** Free to use, self-host, and embed under the AGPL's terms (your friends
75
91
  running it locally are completely unaffected). Building it into a **closed-source product
76
92
  or a hosted service**? That needs the AGPL'd source opened — or a **commercial license**
77
93
  from us instead. Reach out for commercial terms.
94
+
95
+ ## Developing locally
96
+
97
+ Clone the repo, then from the package directory:
98
+
99
+ ```bash
100
+ cd apps/mcp
101
+ npm install
102
+ node test-client.mjs # spawns the server, calls render_chart, checks the output
103
+ ```
104
+
105
+ To point an MCP client at a local checkout instead of the published package, use
106
+ `"command": "node", "args": ["<path-to-clone>/apps/mcp/server.mjs"]`.
package/dist/index.js CHANGED
@@ -786,7 +786,7 @@ function renderKpi(spec) {
786
786
  const fs = spec.fontSize ? Number(spec.fontSize) : Math.max(13, Math.round(13 * Math.sqrt(W * H) / 600));
787
787
  const font = resolveFont(spec);
788
788
  const watermark = spec.watermark !== false;
789
- const accent = resolveFlatPalette(spec.palette || "Clean Corporate", 1)[0];
789
+ const accent = spec.color || resolveFlatPalette(spec.palette || "Clean Corporate", 1)[0];
790
790
  const labelCol = txt(spec, isDark ? "#94a3b8" : "#64748b");
791
791
  const valueCol = txt(spec, isDark ? "#f1f5f9" : "#0f172a");
792
792
  const faint = isDark ? "#475569" : "#94a3b8";
@@ -2002,7 +2002,9 @@ function renderTable(spec) {
2002
2002
  const v = cells[ci];
2003
2003
  if (v === void 0 || v === null || v === "") continue;
2004
2004
  const num = ci > 0 && isNum(v);
2005
- const out = num ? fmt(Number(v)) : String(v);
2005
+ const nv = Number(v);
2006
+ const yearLike = num && Number.isInteger(nv) && nv >= 1900 && nv <= 2100;
2007
+ const out = num ? yearLike ? String(nv) : fmt(nv) : String(v);
2006
2008
  p.push(`<text x="${r(cellX(ci, num))}" y="${r(y + ROWH / 2 + 4)}" ${num ? 'text-anchor="end" ' : ""}font-size="${fs}"${wOpt(spec)} fill="${cellCol}">${esc(out)}</text>`);
2007
2009
  }
2008
2010
  });
@@ -2845,6 +2847,47 @@ var EXAMPLES = {
2845
2847
  swot: { type: "swot", cells: [{ title: "Strengths", items: ["Fast", "Cheap"] }, { title: "Weaknesses", items: ["New brand"] }, { title: "Opportunities", items: ["AI demand"] }, { title: "Threats", items: ["Incumbents"] }] },
2846
2848
  dashboard: { type: "dashboard", title: "Overview", layout: { cols: 2 }, tiles: [{ chart: { type: "kpi", label: "Revenue", value: 128400, valuePrefix: "$", delta: 12.4 } }, { chart: { type: "kpi", label: "Users", value: 8640, delta: 8.1 } }, { span: [2, 1], chart: { type: "bar", data: { labels: ["A", "B", "C"], series: [{ name: "Sales", values: [8, 5, 3] }] } } }] }
2847
2849
  };
2850
+ var GALLERY = [
2851
+ {
2852
+ name: "Dashboard \u2014 SaaS business review",
2853
+ blurb: "Nine charts tiled into one image in a single render.",
2854
+ spec: { type: "dashboard", title: "Acme \u2014 Monthly Business Review", palette: "Clean Corporate", layout: { cols: 3, tileHeight: 250 }, tiles: [
2855
+ { chart: { type: "kpi", label: "Monthly Recurring Revenue", value: 128400, valuePrefix: "$", delta: 12.4 } },
2856
+ { chart: { type: "kpi", label: "Active Users", value: 8640, delta: 8.1 } },
2857
+ { chart: { type: "kpi", label: "Churn", value: 2.3, valueUnit: "%", delta: -0.4, deltaUnit: "pt", deltaGoodWhen: "down" } },
2858
+ { span: [2, 1], chart: { type: "line", title: "Monthly active users", data: { labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"], series: [{ name: "Product A", values: [120, 190, 170, 250, 230, 310] }, { name: "Product B", values: [80, 110, 160, 140, 210, 260] }] } } },
2859
+ { chart: { type: "gauge", title: "Net revenue retention", value: 112, min: 0, max: 150, valueUnit: "%" } },
2860
+ { chart: { type: "funnel", title: "Signup funnel", stages: [{ label: "Visitors", value: 12e3 }, { label: "Signups", value: 4200 }, { label: "Trials", value: 1800 }, { label: "Paid", value: 640 }] } },
2861
+ { chart: { type: "waffle", title: "Revenue by segment", parts: [{ label: "Enterprise", value: 45 }, { label: "SMB", value: 30 }, { label: "Other", value: 15 }] } },
2862
+ { chart: { type: "bar", title: "Deals by stage", data: { labels: ["Lead", "Demo", "Won"], series: [{ name: "Deals", values: [42, 24, 11] }] } } }
2863
+ ] }
2864
+ },
2865
+ {
2866
+ name: "Line \u2014 trend over time",
2867
+ blurb: "Multi-series line with legend.",
2868
+ spec: { type: "line", title: "Monthly active users", data: { labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"], series: [{ name: "Product A", values: [120, 190, 170, 250, 230, 310] }, { name: "Product B", values: [80, 110, 160, 140, 210, 260] }] } }
2869
+ },
2870
+ {
2871
+ name: "Donut \u2014 part of whole",
2872
+ blurb: "Composition with a center total.",
2873
+ spec: { type: "donut", title: "Market share", data: { labels: ["Us", "Rival A", "Rival B", "Other"], series: [{ values: [38, 27, 20, 15] }] } }
2874
+ },
2875
+ {
2876
+ name: "Funnel \u2014 conversion",
2877
+ blurb: "Stages narrowing top \u2192 bottom with %.",
2878
+ spec: { type: "funnel", title: "Signup funnel", stages: [{ label: "Visitors", value: 12e3 }, { label: "Signups", value: 4200 }, { label: "Trials", value: 1800 }, { label: "Paid", value: 640 }] }
2879
+ },
2880
+ {
2881
+ name: "Heatmap \u2014 a grid of values",
2882
+ blurb: "Value \u2192 color intensity.",
2883
+ spec: { type: "heatmap", title: "Active users by day & time", rows: ["Mon", "Tue", "Wed", "Thu", "Fri"], columns: ["9a", "12p", "3p", "6p", "9p"], values: [[2, 5, 8, 3, 1], [1, 4, 9, 6, 2], [0, 3, 7, 8, 4], [2, 6, 9, 7, 3], [3, 7, 6, 4, 5]] }
2884
+ },
2885
+ {
2886
+ name: "Waterfall \u2014 running total",
2887
+ blurb: "Signed steps to a final total.",
2888
+ spec: { type: "waterfall", title: "Cash flow", start: 120, steps: [{ label: "Sales", value: 80 }, { label: "Refunds", value: -18 }, { label: "Costs", value: -42 }, { label: "Other", value: 12 }] }
2889
+ }
2890
+ ];
2848
2891
 
2849
2892
  // ../../packages/raster/raster.mjs
2850
2893
  import { Resvg } from "@resvg/resvg-js";
@@ -3055,7 +3098,23 @@ var inputSchema = {
3055
3098
  scale: z.number().optional().describe("png pixel-density multiplier (default 2 = retina)"),
3056
3099
  outputPath: z.string().optional().describe('LOCAL save: write the rendered chart to this file on disk AND still return the inline image. PNG by default, or SVG if the path ends in ".svg". Accepts absolute, ~, or relative paths (parent dirs auto-created). Works on a local stdio install only \u2014 a hosted/remote server has no access to your disk.')
3057
3100
  };
3058
- var server = new McpServer({ name: "slickfast", version: "0.7.2" });
3101
+ var server = new McpServer({ name: "slickfast", version: "0.7.4" });
3102
+ var GOTCHAS = {
3103
+ gantt: ["start/end are timeline positions; the axis spans min(start)\u2026max(end), so a non-zero-based timeline (years, offset quarters) fills the plot correctly."],
3104
+ diverging: ["needs 2 series \u2014 series A is forced positive (up), series B negative (down), regardless of stored sign."],
3105
+ difference: ["needs 2 series; the gap between them is shaded."],
3106
+ kpi: ["sparkline is landscape-only (omit it in portrait).", "the accent bar follows the palette \u2014 pass `color` to fix it."],
3107
+ waffle: ["values are cell counts out of a 10\xD710 grid (100 total); a single part renders as one % gauge."],
3108
+ heatmap: ["`values` is a 2D matrix values[row][col]; `rows` and `columns` are the axis labels."],
3109
+ stacked: ["each ROW is a stack segment shared across every bar; segment color comes from the row, not the series."],
3110
+ stacked100: ["each bar sums to 100%; each row is a shared segment."],
3111
+ pieofpie: ['uses `pies:[\u2026]`, not `data`; each pie\u2019s first slice ("bridge") drills into the next.'],
3112
+ gauge: ["`value` is clamped to [min,max]; set min/max to your scale (default 0\u2013100)."],
3113
+ bullet: ["`bands` are qualitative thresholds on the measure scale; `target` draws the comparison marker."],
3114
+ calendar: ['`days` maps "YYYY-MM-DD" \u2192 value; weekday is computed deterministically (no clock/Date).'],
3115
+ dashboard: ["each tile.chart is a full spec of any other type; tiles keep their OWN scale \u2014 for a shared-scale comparison use one grouped/multi-series chart, not side-by-side tiles."],
3116
+ table: ["year-like integers (1900\u20132100) are shown without thousands separators; other numbers get them."]
3117
+ };
3059
3118
  function typeContract(t) {
3060
3119
  const meta = TYPES.find((x) => x.type === t);
3061
3120
  if (!meta) return null;
@@ -3067,6 +3126,8 @@ function typeContract(t) {
3067
3126
  needsData: meta.needsData,
3068
3127
  honorsToggles: TYPE_TOGGLES[t] || [],
3069
3128
  // which of showValues/showTotal/showPoints this type uses (others are ignored)
3129
+ gotchas: GOTCHAS[t] || [],
3130
+ // non-obvious per-type rules (else discovered by rendering twice)
3070
3131
  example: EXAMPLES[t] || null
3071
3132
  };
3072
3133
  }
@@ -3121,6 +3182,55 @@ server.registerTool("describe_type", {
3121
3182
  const all = TYPES.map((t) => ({ type: t.type, family: t.family, summary: t.summary }));
3122
3183
  return { content: [{ type: "text", text: JSON.stringify({ count: all.length, types: all, hint: "call describe_type with a `type` for its fields + a minimal spec" }, null, 2) }] };
3123
3184
  });
3185
+ server.registerTool("gallery", {
3186
+ title: "Gallery / demo",
3187
+ description: 'Render a curated DEMO GALLERY of example charts and dashboards. Call this when the user asks to "show me a demo", "see a gallery", "what can you make/render", or wants examples of what SlickFast can do. Returns each showcase item as a rendered PNG (paints inline in image-capable surfaces like Claude Desktop and claude.ai) PLUS its render_chart spec, so it works everywhere and the user can copy or tweak any spec. Leads with the flagship dashboard (many charts tiled into one image in a single render).',
3188
+ inputSchema: {
3189
+ type: z.enum(TYPE_NAMES).optional().describe('render just this ONE type (its showcase spec, or its minimal example) \u2014 for "show me a <type>"; omit for the curated multi-type showcase'),
3190
+ limit: z.number().optional().describe("max items in the showcase (default all " + GALLERY.length + "); ignored when `type` is set")
3191
+ }
3192
+ }, async ({ type, limit }) => {
3193
+ let items;
3194
+ if (type) {
3195
+ const g = GALLERY.find((x) => x.spec.type === type);
3196
+ items = [{ name: type, blurb: g ? g.blurb : `Example ${type} chart.`, spec: g ? g.spec : EXAMPLES[type] }];
3197
+ } else {
3198
+ items = GALLERY.slice(0, limit && limit > 0 ? limit : GALLERY.length);
3199
+ }
3200
+ const content = [{ type: "text", text: `SlickFast gallery \u2014 ${items.length} example${items.length > 1 ? "s" : ""}. Each is one render_chart call; the spec is shown under it.` }];
3201
+ for (const it of items) {
3202
+ try {
3203
+ const png = svgToPng(renderSpec(it.spec), { scale: 2 });
3204
+ content.push({ type: "text", text: `### ${it.name}
3205
+ ${it.blurb}
3206
+ \`\`\`json
3207
+ ${JSON.stringify(it.spec)}
3208
+ \`\`\`` });
3209
+ content.push({ type: "image", data: png.toString("base64"), mimeType: "image/png" });
3210
+ } catch (e) {
3211
+ content.push({ type: "text", text: `(${it.name} failed to render: ${e?.message || e})` });
3212
+ }
3213
+ }
3214
+ return { content };
3215
+ });
3216
+ server.registerTool("list_palettes", {
3217
+ title: "List palettes",
3218
+ description: 'List every valid `palette` name, grouped into FLAT palettes (great for any chart) and NESTED themes (designed for pieofpie drill-downs, but accepted on any chart), each with its representative colors. Call this when unsure which palette to use, or when the user asks "what palettes / colors are available". Any name returned here is a valid `palette` value \u2014 anything else is rejected.',
3219
+ inputSchema: {}
3220
+ }, async () => {
3221
+ const flat = FLAT_PALETTES.map((p) => ({ name: p.name, colors: p.colors }));
3222
+ const nested = NESTED_THEMES.map((t) => ({
3223
+ name: t.name,
3224
+ anchor: t.tiers && t.tiers[0] && t.tiers[0][0] || t.pie1 && t.pie1[0] || null,
3225
+ colors: (t.tiers ? t.tiers.flat() : t.pie1) || [],
3226
+ dark: !!t.dark
3227
+ }));
3228
+ return { content: [{ type: "text", text: JSON.stringify({
3229
+ flat,
3230
+ nested,
3231
+ hint: 'pass any name as `palette`. Flat is the default for most charts; nested themes shine on pieofpie. "dark: true" themes assume a dark background.'
3232
+ }, null, 2) }] };
3233
+ });
3124
3234
  server.registerResource("chart-spec", "spec://chart-spec", {
3125
3235
  title: "ChartSpec contract",
3126
3236
  description: "Full field reference for render_chart.",
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@slickfast/mcp",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "SlickFast — render 47 chart & info-design types (bar, line, pie, KPI, cards, funnel, matrix, gauge, calendar…) plus multi-chart dashboards tiled into one image, as SVG/PNG via MCP. Local and deterministic; nothing leaves your machine.",
5
+ "keywords": ["mcp", "model-context-protocol", "chart", "charts", "dashboard", "data-visualization", "svg", "png", "ai", "agent", "claude", "llm", "deterministic"],
5
6
  "license": "AGPL-3.0-only",
6
7
  "type": "module",
7
8
  "bin": { "slickfast-mcp": "dist/index.js" },