@amcharts/amcharts5-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@amcharts/amcharts5-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for amCharts 5 — gives AI assistants on-demand access to amCharts docs, examples, and API reference",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "amcharts5-mcp": "src/index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node src/index.js",
12
+ "dev": "node --watch src/index.js"
13
+ },
14
+ "keywords": [
15
+ "amcharts",
16
+ "mcp",
17
+ "model-context-protocol",
18
+ "ai",
19
+ "charts",
20
+ "data-visualization"
21
+ ],
22
+ "author": "amCharts",
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "@modelcontextprotocol/sdk": "^1.12.1"
26
+ }
27
+ }
package/src/index.js ADDED
@@ -0,0 +1,329 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { z } from "zod";
6
+ import { readFileSync, readdirSync } from "fs";
7
+ import { join, dirname } from "path";
8
+ import { fileURLToPath } from "url";
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const SKILL_DIR = join(__dirname, "..", "amcharts5-skill", "amcharts5-skill");
12
+ const REFERENCES_DIR = join(SKILL_DIR, "references");
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Load and index all content at startup
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /** @type {Map<string, {title: string, content: string, sections: {heading: string, body: string}[]}>} */
19
+ const docs = new Map();
20
+
21
+ /** Chart type aliases → reference file mapping */
22
+ const CHART_TYPE_MAP = {
23
+ // XY family
24
+ "xy": "xy", "line": "xy", "area": "xy", "bar": "xy", "column": "xy",
25
+ "candlestick": "xy", "ohlc": "xy", "scatter": "xy", "bubble": "xy",
26
+ "stacked": "xy", "stacked-bar": "xy", "stacked-column": "xy",
27
+ "stacked-area": "xy", "range": "xy", "waterfall": "xy",
28
+ // Pie family
29
+ "pie": "pie", "donut": "pie", "funnel": "pie", "pyramid": "pie",
30
+ "semi-circle": "pie", "pictorial": "pie", "sliced": "pie",
31
+ // Map
32
+ "map": "map", "choropleth": "map", "geo": "map", "geographic": "map",
33
+ "world": "map", "country": "map", "bubble-map": "map", "point-map": "map",
34
+ // Hierarchy
35
+ "hierarchy": "hierarchy", "treemap": "hierarchy", "sunburst": "hierarchy",
36
+ "force-directed": "hierarchy", "pack": "hierarchy", "partition": "hierarchy",
37
+ "tree": "hierarchy", "org-chart": "hierarchy",
38
+ // Flow
39
+ "flow": "flow", "sankey": "flow", "chord": "flow", "arc": "flow",
40
+ "alluvial": "flow",
41
+ // Radar
42
+ "radar": "radar", "spider": "radar", "polar": "radar", "gauge": "radar",
43
+ "speedometer": "radar", "meter": "radar",
44
+ // Stock
45
+ "stock": "stock", "financial": "stock", "trading": "stock",
46
+ // Timeline
47
+ "timeline": "timeline", "serpentine": "timeline", "spiral": "timeline",
48
+ "curve": "timeline",
49
+ // Gantt
50
+ "gantt": "gantt", "project-timeline": "gantt",
51
+ // Word cloud
52
+ "wordcloud": "wordcloud", "word-cloud": "wordcloud", "tag-cloud": "wordcloud",
53
+ // Venn
54
+ "venn": "venn",
55
+ // UI
56
+ "ui": "ui-elements", "ui-elements": "ui-elements", "button": "ui-elements",
57
+ "slider": "ui-elements", "controls": "ui-elements",
58
+ };
59
+
60
+ function parseSections(content) {
61
+ const sections = [];
62
+ const lines = content.split("\n");
63
+ let currentHeading = "";
64
+ let currentLevel = 0;
65
+ let currentBody = [];
66
+
67
+ for (const line of lines) {
68
+ const headingMatch = line.match(/^(#{1,3})\s+(.+)/);
69
+ if (headingMatch) {
70
+ const level = headingMatch[1].length;
71
+ const title = headingMatch[2].trim();
72
+ if (level <= 2) {
73
+ // Major section boundary (# or ##)
74
+ if (currentHeading || currentBody.length > 0) {
75
+ sections.push({ heading: currentHeading, body: currentBody.join("\n") });
76
+ }
77
+ currentHeading = title;
78
+ currentLevel = level;
79
+ currentBody = [];
80
+ } else {
81
+ // Sub-heading (###) — include in current section body
82
+ currentBody.push(line);
83
+ }
84
+ } else {
85
+ currentBody.push(line);
86
+ }
87
+ }
88
+ if (currentHeading || currentBody.length > 0) {
89
+ sections.push({ heading: currentHeading, body: currentBody.join("\n") });
90
+ }
91
+ return sections;
92
+ }
93
+
94
+ function loadFile(filePath, name) {
95
+ const content = readFileSync(filePath, "utf-8");
96
+ const titleMatch = content.match(/^#\s+(.+)/m);
97
+ const title = titleMatch ? titleMatch[1] : name;
98
+ docs.set(name, { title, content, sections: parseSections(content) });
99
+ }
100
+
101
+ function loadContent() {
102
+ // Load core SKILL.md
103
+ loadFile(join(SKILL_DIR, "SKILL.md"), "SKILL");
104
+
105
+ // Load all reference files
106
+ const files = readdirSync(REFERENCES_DIR).filter(f => f.endsWith(".md"));
107
+ for (const file of files) {
108
+ loadFile(join(REFERENCES_DIR, file), file.replace(".md", ""));
109
+ }
110
+ }
111
+
112
+ loadContent();
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Search helpers
116
+ // ---------------------------------------------------------------------------
117
+
118
+ function searchDocs(query, maxResults = 10) {
119
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
120
+ const results = [];
121
+
122
+ for (const [name, doc] of docs) {
123
+ for (const section of doc.sections) {
124
+ const text = (section.heading + " " + section.body).toLowerCase();
125
+ const score = terms.reduce((s, t) => s + (text.includes(t) ? 1 : 0), 0);
126
+ if (score > 0) {
127
+ results.push({
128
+ file: name,
129
+ docTitle: doc.title,
130
+ heading: section.heading,
131
+ body: section.body.trim(),
132
+ score,
133
+ });
134
+ }
135
+ }
136
+ }
137
+
138
+ results.sort((a, b) => b.score - a.score);
139
+ return results.slice(0, maxResults);
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // MCP Server
144
+ // ---------------------------------------------------------------------------
145
+
146
+ const server = new McpServer({
147
+ name: "amcharts5",
148
+ version: "1.0.0",
149
+ });
150
+
151
+ // --- Tool: list_chart_types ---
152
+ server.tool(
153
+ "list_chart_types",
154
+ "List all amCharts 5 chart types with their categories and reference file names",
155
+ {},
156
+ async () => {
157
+ const types = {};
158
+ for (const [alias, ref] of Object.entries(CHART_TYPE_MAP)) {
159
+ if (!types[ref]) types[ref] = [];
160
+ types[ref].push(alias);
161
+ }
162
+
163
+ let text = "# amCharts 5 — Available Chart Types\n\n";
164
+ for (const [ref, aliases] of Object.entries(types)) {
165
+ const doc = docs.get(ref);
166
+ const title = doc ? doc.title : ref;
167
+ text += `## ${title}\n`;
168
+ text += `Reference: \`${ref}\`\n`;
169
+ text += `Keywords: ${aliases.join(", ")}\n\n`;
170
+ }
171
+ return { content: [{ type: "text", text }] };
172
+ }
173
+ );
174
+
175
+ // --- Tool: get_chart_reference ---
176
+ server.tool(
177
+ "get_chart_reference",
178
+ "Get the full reference documentation for a specific amCharts 5 chart type. Use list_chart_types first to see available types.",
179
+ { chartType: z.string().describe("Chart type keyword, e.g. 'pie', 'sankey', 'xy', 'treemap', 'map', 'radar', 'stock', 'gantt', 'venn', 'wordcloud', 'timeline', 'flow', 'hierarchy', 'ui-elements'") },
180
+ async ({ chartType }) => {
181
+ const key = CHART_TYPE_MAP[chartType.toLowerCase()] || chartType.toLowerCase();
182
+ const doc = docs.get(key);
183
+ if (!doc) {
184
+ const available = [...new Set(Object.values(CHART_TYPE_MAP))].sort().join(", ");
185
+ return {
186
+ content: [{ type: "text", text: `No reference found for "${chartType}". Available: ${available}` }],
187
+ };
188
+ }
189
+ return { content: [{ type: "text", text: doc.content }] };
190
+ }
191
+ );
192
+
193
+ // --- Tool: get_core_reference ---
194
+ server.tool(
195
+ "get_core_reference",
196
+ "Get the core amCharts 5 reference (SKILL.md) — covers setup, colors, themes, legends, tooltips, events, data, adapters, disposal, and common pitfalls. Read this first before any chart-specific reference.",
197
+ {},
198
+ async () => {
199
+ const doc = docs.get("SKILL");
200
+ if (!doc) {
201
+ return { content: [{ type: "text", text: "Core SKILL.md not found." }] };
202
+ }
203
+ return { content: [{ type: "text", text: doc.content }] };
204
+ }
205
+ );
206
+
207
+ // --- Tool: search_docs ---
208
+ server.tool(
209
+ "search_docs",
210
+ "Search across all amCharts 5 documentation for a keyword or topic. Returns matching sections ranked by relevance.",
211
+ {
212
+ query: z.string().describe("Search query, e.g. 'legend', 'axis label rotation', 'tooltip format', 'data processor'"),
213
+ maxResults: z.number().optional().default(5).describe("Maximum number of results to return (default 5)"),
214
+ },
215
+ async ({ query, maxResults }) => {
216
+ const results = searchDocs(query, maxResults);
217
+ if (results.length === 0) {
218
+ return { content: [{ type: "text", text: `No results found for "${query}".` }] };
219
+ }
220
+
221
+ let text = `# Search results for "${query}"\n\n`;
222
+ for (const r of results) {
223
+ text += `## ${r.docTitle} → ${r.heading}\n`;
224
+ text += `*(source: ${r.file}.md, relevance: ${r.score})*\n\n`;
225
+ // Truncate very long sections
226
+ const body = r.body.length > 2000 ? r.body.slice(0, 2000) + "\n\n...(truncated)" : r.body;
227
+ text += body + "\n\n---\n\n";
228
+ }
229
+ return { content: [{ type: "text", text }] };
230
+ }
231
+ );
232
+
233
+ // --- Tool: get_section ---
234
+ server.tool(
235
+ "get_section",
236
+ "Get a specific section from an amCharts 5 reference file by heading name. Use search_docs first to find the right section.",
237
+ {
238
+ file: z.string().describe("Reference file name without .md, e.g. 'xy', 'pie', 'SKILL', 'map'"),
239
+ heading: z.string().describe("Section heading to retrieve, e.g. 'Core setup pattern', 'Axis types', 'Common pitfalls'"),
240
+ },
241
+ async ({ file, heading }) => {
242
+ const doc = docs.get(file);
243
+ if (!doc) {
244
+ return { content: [{ type: "text", text: `File "${file}" not found.` }] };
245
+ }
246
+
247
+ const section = doc.sections.find(
248
+ s => s.heading.toLowerCase().includes(heading.toLowerCase())
249
+ );
250
+ if (!section) {
251
+ const available = doc.sections.map(s => s.heading).filter(Boolean).join("\n- ");
252
+ return {
253
+ content: [{ type: "text", text: `Section "${heading}" not found in ${file}.md.\n\nAvailable sections:\n- ${available}` }],
254
+ };
255
+ }
256
+ return {
257
+ content: [{ type: "text", text: `# ${section.heading}\n\n${section.body}` }],
258
+ };
259
+ }
260
+ );
261
+
262
+ // --- Tool: get_quick_start ---
263
+ server.tool(
264
+ "get_quick_start",
265
+ "Get a quick-start template for a specific chart type — a minimal working example ready to customize.",
266
+ {
267
+ chartType: z.string().describe("Chart type, e.g. 'pie', 'line', 'bar', 'map', 'sankey', 'treemap'"),
268
+ format: z.enum(["html", "esm"]).optional().default("html").describe("Output format: 'html' for a full HTML page with CDN scripts, 'esm' for ES module imports"),
269
+ },
270
+ async ({ chartType, format }) => {
271
+ const key = CHART_TYPE_MAP[chartType.toLowerCase()] || chartType.toLowerCase();
272
+ const doc = docs.get(key);
273
+ if (!doc) {
274
+ return {
275
+ content: [{ type: "text", text: `No reference found for "${chartType}". Use list_chart_types to see available types.` }],
276
+ };
277
+ }
278
+
279
+ // Find the setup/core pattern section
280
+ const setupSection = doc.sections.find(s =>
281
+ /setup|core.*pattern|basic|quick.*start|required.*import/i.test(s.heading)
282
+ );
283
+
284
+ if (!setupSection) {
285
+ // Fallback: return first few sections
286
+ const intro = doc.sections.slice(0, 4).map(s => `## ${s.heading}\n${s.body}`).join("\n\n");
287
+ return { content: [{ type: "text", text: intro }] };
288
+ }
289
+
290
+ // Also grab the imports section
291
+ const importSection = doc.sections.find(s =>
292
+ /import/i.test(s.heading)
293
+ );
294
+
295
+ let text = `# Quick Start: ${doc.title}\n\n`;
296
+ if (importSection && importSection !== setupSection) {
297
+ if (format === "esm") {
298
+ text += `## Imports (ES modules)\n${importSection.body}\n\n`;
299
+ } else {
300
+ text += `## Imports\n${importSection.body}\n\n`;
301
+ }
302
+ }
303
+ text += `## ${setupSection.heading}\n${setupSection.body}`;
304
+
305
+ return { content: [{ type: "text", text }] };
306
+ }
307
+ );
308
+
309
+ // ---------------------------------------------------------------------------
310
+ // Resources: expose each doc as an MCP resource
311
+ // ---------------------------------------------------------------------------
312
+
313
+ for (const [name, doc] of docs) {
314
+ server.resource(
315
+ name,
316
+ `amcharts://docs/${name}`,
317
+ { description: `amCharts 5 reference: ${doc.title}`, mimeType: "text/markdown" },
318
+ async () => ({
319
+ contents: [{ uri: `amcharts://docs/${name}`, text: doc.content, mimeType: "text/markdown" }],
320
+ })
321
+ );
322
+ }
323
+
324
+ // ---------------------------------------------------------------------------
325
+ // Start
326
+ // ---------------------------------------------------------------------------
327
+
328
+ const transport = new StdioServerTransport();
329
+ await server.connect(transport);
package/test.js ADDED
@@ -0,0 +1,153 @@
1
+ import { spawn } from "child_process";
2
+
3
+ const server = spawn("node", ["src/index.js"], {
4
+ cwd: "C:/projects/amcharts5-mcp",
5
+ stdio: ["pipe", "pipe", "pipe"],
6
+ });
7
+
8
+ let buffer = "";
9
+ server.stdout.on("data", (chunk) => {
10
+ buffer += chunk.toString();
11
+ // Each JSON-RPC response is a complete line
12
+ const lines = buffer.split("\n");
13
+ buffer = lines.pop(); // keep incomplete line in buffer
14
+ for (const line of lines) {
15
+ if (!line.trim()) continue;
16
+ try {
17
+ const msg = JSON.parse(line);
18
+ handleResponse(msg);
19
+ } catch {}
20
+ }
21
+ });
22
+
23
+ server.stderr.on("data", (chunk) => {
24
+ process.stderr.write("[stderr] " + chunk.toString());
25
+ });
26
+
27
+ let step = 0;
28
+ const tests = [
29
+ // Step 0: Initialize
30
+ {
31
+ send: { jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "test", version: "1.0.0" } } },
32
+ check: (r) => r.result?.serverInfo?.name === "amcharts5" ? "PASS" : "FAIL",
33
+ label: "Initialize server",
34
+ },
35
+ // Step 1: List tools
36
+ {
37
+ send: { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} },
38
+ check: (r) => r.result?.tools?.length === 6 ? `PASS (${r.result.tools.length} tools)` : `FAIL (${r.result?.tools?.length} tools)`,
39
+ label: "List tools (expect 6)",
40
+ },
41
+ // Step 2: list_chart_types
42
+ {
43
+ send: { jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "list_chart_types", arguments: {} } },
44
+ check: (r) => {
45
+ const text = r.result?.content?.[0]?.text || "";
46
+ return text.includes("XY") && text.includes("Pie") && text.includes("Sankey") ? "PASS" : "FAIL";
47
+ },
48
+ label: "list_chart_types — contains XY, Pie, Sankey",
49
+ },
50
+ // Step 3: get_core_reference
51
+ {
52
+ send: { jsonrpc: "2.0", id: 4, method: "tools/call", params: { name: "get_core_reference", arguments: {} } },
53
+ check: (r) => {
54
+ const text = r.result?.content?.[0]?.text || "";
55
+ return text.includes("Critical rules") && text.includes("Common pitfalls") ? `PASS (${text.length} chars)` : "FAIL";
56
+ },
57
+ label: "get_core_reference — returns SKILL.md content",
58
+ },
59
+ // Step 4: get_chart_reference for "sankey"
60
+ {
61
+ send: { jsonrpc: "2.0", id: 5, method: "tools/call", params: { name: "get_chart_reference", arguments: { chartType: "sankey" } } },
62
+ check: (r) => {
63
+ const text = r.result?.content?.[0]?.text || "";
64
+ return text.includes("Sankey") || text.includes("Flow") ? `PASS (${text.length} chars)` : "FAIL";
65
+ },
66
+ label: "get_chart_reference('sankey') — returns flow docs",
67
+ },
68
+ // Step 5: get_chart_reference for unknown type
69
+ {
70
+ send: { jsonrpc: "2.0", id: 6, method: "tools/call", params: { name: "get_chart_reference", arguments: { chartType: "banana" } } },
71
+ check: (r) => {
72
+ const text = r.result?.content?.[0]?.text || "";
73
+ return text.includes("No reference found") ? "PASS (graceful error)" : "FAIL";
74
+ },
75
+ label: "get_chart_reference('banana') — graceful error",
76
+ },
77
+ // Step 6: search_docs
78
+ {
79
+ send: { jsonrpc: "2.0", id: 7, method: "tools/call", params: { name: "search_docs", arguments: { query: "legend tooltip", maxResults: 3 } } },
80
+ check: (r) => {
81
+ const text = r.result?.content?.[0]?.text || "";
82
+ return text.includes("Search results") && text.includes("legend") ? `PASS (${(text.match(/---/g)||[]).length} results)` : "FAIL";
83
+ },
84
+ label: "search_docs('legend tooltip') — finds results",
85
+ },
86
+ // Step 7: get_section
87
+ {
88
+ send: { jsonrpc: "2.0", id: 8, method: "tools/call", params: { name: "get_section", arguments: { file: "SKILL", heading: "Common pitfalls" } } },
89
+ check: (r) => {
90
+ const text = r.result?.content?.[0]?.text || "";
91
+ return text.includes("Common pitfalls") && text.includes("CategoryAxis") ? "PASS" : "FAIL";
92
+ },
93
+ label: "get_section('SKILL', 'Common pitfalls') — returns section",
94
+ },
95
+ // Step 8: get_section not found
96
+ {
97
+ send: { jsonrpc: "2.0", id: 9, method: "tools/call", params: { name: "get_section", arguments: { file: "xy", heading: "nonexistent" } } },
98
+ check: (r) => {
99
+ const text = r.result?.content?.[0]?.text || "";
100
+ return text.includes("not found") && text.includes("Available sections") ? "PASS (lists available)" : "FAIL";
101
+ },
102
+ label: "get_section('xy', 'nonexistent') — lists available sections",
103
+ },
104
+ // Step 9: get_quick_start
105
+ {
106
+ send: { jsonrpc: "2.0", id: 10, method: "tools/call", params: { name: "get_quick_start", arguments: { chartType: "line", format: "esm" } } },
107
+ check: (r) => {
108
+ const text = r.result?.content?.[0]?.text || "";
109
+ return text.includes("Quick Start") && text.includes("import") ? "PASS" : "FAIL";
110
+ },
111
+ label: "get_quick_start('line', 'esm') — returns template",
112
+ },
113
+ // Step 10: List resources
114
+ {
115
+ send: { jsonrpc: "2.0", id: 11, method: "resources/list", params: {} },
116
+ check: (r) => {
117
+ const count = r.result?.resources?.length || 0;
118
+ return count >= 12 ? `PASS (${count} resources)` : `FAIL (${count} resources)`;
119
+ },
120
+ label: "List resources (expect 13 doc files)",
121
+ },
122
+ ];
123
+
124
+ function handleResponse(msg) {
125
+ if (step >= tests.length) return;
126
+ const test = tests[step];
127
+ const result = test.check(msg);
128
+ const icon = result.startsWith("PASS") ? "✓" : "✗";
129
+ console.log(`${icon} Test ${step + 1}/${tests.length}: ${test.label} → ${result}`);
130
+ step++;
131
+ if (step < tests.length) {
132
+ sendNext();
133
+ } else {
134
+ console.log("\nAll tests complete.");
135
+ server.kill();
136
+ process.exit(0);
137
+ }
138
+ }
139
+
140
+ function sendNext() {
141
+ const test = tests[step];
142
+ server.stdin.write(JSON.stringify(test.send) + "\n");
143
+ }
144
+
145
+ // Start
146
+ sendNext();
147
+
148
+ // Timeout
149
+ setTimeout(() => {
150
+ console.error(`\nTimeout — stuck at test ${step + 1}/${tests.length}: ${tests[step]?.label}`);
151
+ server.kill();
152
+ process.exit(1);
153
+ }, 15000);