@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.
- package/README.md +62 -33
- package/dist/index.js +113 -3
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,37 +1,58 @@
|
|
|
1
|
-
# SlickFast
|
|
1
|
+
# SlickFast — charts & dashboards for AI agents
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
##
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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": "
|
|
47
|
+
{ "mcpServers": { "slickfast": { "command": "npx", "args": ["-y", "@slickfast/mcp"] } } }
|
|
33
48
|
```
|
|
34
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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" },
|