@amcharts/amcharts5-mcp 1.1.9 → 1.3.2

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/src/index.js CHANGED
@@ -2,534 +2,28 @@
2
2
 
3
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
- import { z } from "zod";
6
- import { readFileSync, readdirSync, existsSync } from "fs";
5
+ import { readFileSync } from "fs";
7
6
  import { join, dirname } from "path";
8
7
  import { fileURLToPath } from "url";
8
+ import { loadContentFromDisk } from "./content-fs.js";
9
+ import { registerTools } from "./tools.js";
9
10
 
10
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
12
  const ROOT = join(__dirname, "..");
12
13
 
13
- // Submodule path (local dev) or content/ fallback (npm install)
14
- const SUBMODULE_DIR = join(ROOT, "amcharts5-skill", "amcharts5-skill");
15
- const CONTENT_DIR = join(ROOT, "content");
16
- const SKILL_DIR = existsSync(SUBMODULE_DIR) ? SUBMODULE_DIR : CONTENT_DIR;
17
- const REFERENCES_DIR = existsSync(join(SKILL_DIR, "references"))
18
- ? join(SKILL_DIR, "references")
19
- : SKILL_DIR;
20
- const EXTENDED_DIR = join(ROOT, "extended");
14
+ // Single source of truth for the server version: package.json
15
+ const PKG = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
16
+ const VERSION = PKG.version;
21
17
 
22
- // ---------------------------------------------------------------------------
23
- // Load and index all content at startup
24
- // ---------------------------------------------------------------------------
25
-
26
- /** @type {Map<string, {title: string, content: string, sections: {heading: string, body: string}[]}>} */
27
- const docs = new Map();
28
-
29
- /** @type {Map<string, {title: string, content: string, sections: {heading: string, body: string}[], path: string}>} */
30
- const extendedDocs = new Map();
31
-
32
- /** @type {Map<string, {title: string, category: string, content: string, source: string}>} */
33
- const examples = new Map();
34
-
35
- /** Chart type aliases → reference file mapping */
36
- const CHART_TYPE_MAP = {
37
- // XY family
38
- "xy": "xy", "line": "xy", "area": "xy", "bar": "xy", "column": "xy",
39
- "candlestick": "xy", "ohlc": "xy", "scatter": "xy", "bubble": "xy",
40
- "stacked": "xy", "stacked-bar": "xy", "stacked-column": "xy",
41
- "stacked-area": "xy", "range": "xy", "waterfall": "xy",
42
- // Pie family
43
- "pie": "pie", "donut": "pie", "funnel": "pie", "pyramid": "pie",
44
- "semi-circle": "pie", "pictorial": "pie", "sliced": "pie",
45
- // Map
46
- "map": "map", "choropleth": "map", "geo": "map", "geographic": "map",
47
- "world": "map", "country": "map", "bubble-map": "map", "point-map": "map",
48
- // Hierarchy
49
- "hierarchy": "hierarchy", "treemap": "hierarchy", "sunburst": "hierarchy",
50
- "force-directed": "hierarchy", "pack": "hierarchy", "partition": "hierarchy",
51
- "tree": "hierarchy", "org-chart": "hierarchy",
52
- // Flow
53
- "flow": "flow", "sankey": "flow", "chord": "flow", "arc": "flow",
54
- "alluvial": "flow",
55
- // Radar
56
- "radar": "radar", "spider": "radar", "polar": "radar", "gauge": "radar",
57
- "speedometer": "radar", "meter": "radar",
58
- // Stock
59
- "stock": "stock", "financial": "stock", "trading": "stock",
60
- // Timeline
61
- "timeline": "timeline", "serpentine": "timeline", "spiral": "timeline",
62
- "curve": "timeline",
63
- // Gantt
64
- "gantt": "gantt", "project-timeline": "gantt",
65
- // Word cloud
66
- "wordcloud": "wordcloud", "word-cloud": "wordcloud", "tag-cloud": "wordcloud",
67
- // Venn
68
- "venn": "venn",
69
- // UI
70
- "ui": "ui-elements", "ui-elements": "ui-elements", "button": "ui-elements",
71
- "slider": "ui-elements", "controls": "ui-elements",
72
- };
73
-
74
- function parseSections(content) {
75
- const sections = [];
76
- const lines = content.split("\n");
77
- let currentHeading = "";
78
- let currentLevel = 0;
79
- let currentBody = [];
80
-
81
- for (const line of lines) {
82
- const headingMatch = line.match(/^(#{1,3})\s+(.+)/);
83
- if (headingMatch) {
84
- const level = headingMatch[1].length;
85
- const title = headingMatch[2].trim();
86
- if (level <= 2) {
87
- // Major section boundary (# or ##)
88
- if (currentHeading || currentBody.length > 0) {
89
- sections.push({ heading: currentHeading, body: currentBody.join("\n") });
90
- }
91
- currentHeading = title;
92
- currentLevel = level;
93
- currentBody = [];
94
- } else {
95
- // Sub-heading (###) — include in current section body
96
- currentBody.push(line);
97
- }
98
- } else {
99
- currentBody.push(line);
100
- }
101
- }
102
- if (currentHeading || currentBody.length > 0) {
103
- sections.push({ heading: currentHeading, body: currentBody.join("\n") });
104
- }
105
- return sections;
106
- }
107
-
108
- function loadFile(filePath, name) {
109
- const content = readFileSync(filePath, "utf-8");
110
- const titleMatch = content.match(/^#\s+(.+)/m);
111
- const title = titleMatch ? titleMatch[1] : name;
112
- docs.set(name, { title, content, sections: parseSections(content) });
113
- }
114
-
115
- function loadContent() {
116
- // Load core SKILL.md
117
- const skillPath = join(SKILL_DIR, "SKILL.md");
118
- if (existsSync(skillPath)) {
119
- loadFile(skillPath, "SKILL");
120
- }
121
-
122
- // Load all reference files
123
- const files = readdirSync(REFERENCES_DIR).filter(f => f.endsWith(".md") && f !== "SKILL.md");
124
- for (const file of files) {
125
- loadFile(join(REFERENCES_DIR, file), file.replace(".md", ""));
126
- }
127
- }
128
-
129
- loadContent();
130
-
131
- // ---------------------------------------------------------------------------
132
- // Load extended docs and examples
133
- // ---------------------------------------------------------------------------
134
-
135
- function loadExtendedDir(dir, prefix = "") {
136
- if (!existsSync(dir)) return;
137
- const entries = readdirSync(dir, { withFileTypes: true });
138
- for (const entry of entries) {
139
- const fullPath = join(dir, entry.name);
140
- if (entry.isDirectory()) {
141
- loadExtendedDir(fullPath, prefix ? `${prefix}/${entry.name}` : entry.name);
142
- } else if (entry.name.endsWith(".md")) {
143
- const key = prefix ? `${prefix}/${entry.name.replace(".md", "")}` : entry.name.replace(".md", "");
144
- const content = readFileSync(fullPath, "utf-8");
145
-
146
- // Parse frontmatter
147
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
148
- const body = fmMatch ? fmMatch[2].trim() : content;
149
- const fm = fmMatch ? fmMatch[1] : "";
150
- const titleMatch = fm.match(/title:\s*"(.+?)"/);
151
- const categoryMatch = fm.match(/category:\s*"(.+?)"/);
152
- const sourceMatch = fm.match(/source:\s*"(.+?)"/);
153
- const title = titleMatch ? titleMatch[1] : entry.name.replace(".md", "");
154
-
155
- if (key.startsWith("examples/")) {
156
- examples.set(key, {
157
- title,
158
- category: categoryMatch ? categoryMatch[1] : "",
159
- content: body,
160
- source: sourceMatch ? sourceMatch[1] : "",
161
- });
162
- } else {
163
- extendedDocs.set(key, {
164
- title,
165
- content: body,
166
- sections: parseSections(body),
167
- path: key,
168
- });
169
- }
170
- }
171
- }
172
- }
173
-
174
- loadExtendedDir(EXTENDED_DIR);
175
-
176
- // ---------------------------------------------------------------------------
177
- // Search helpers
178
- // ---------------------------------------------------------------------------
179
-
180
- function searchDocs(query, maxResults = 10) {
181
- const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
182
- const results = [];
183
-
184
- for (const [name, doc] of docs) {
185
- for (const section of doc.sections) {
186
- const text = (section.heading + " " + section.body).toLowerCase();
187
- const score = terms.reduce((s, t) => s + (text.includes(t) ? 1 : 0), 0);
188
- if (score > 0) {
189
- results.push({
190
- file: name,
191
- docTitle: doc.title,
192
- heading: section.heading,
193
- body: section.body.trim(),
194
- score,
195
- });
196
- }
197
- }
198
- }
199
-
200
- results.sort((a, b) => b.score - a.score);
201
- return results.slice(0, maxResults);
202
- }
203
-
204
- // ---------------------------------------------------------------------------
205
- // MCP Server
206
- // ---------------------------------------------------------------------------
18
+ // Load and index all content from disk (submodule + extended/).
19
+ const content = loadContentFromDisk();
207
20
 
208
21
  const server = new McpServer({
209
22
  name: "amcharts5",
210
- version: "1.0.0",
23
+ version: VERSION,
211
24
  });
212
25
 
213
- // --- Tool: list_chart_types ---
214
- server.tool(
215
- "list_chart_types",
216
- "List all amCharts 5 chart types with their categories and reference file names",
217
- {},
218
- async () => {
219
- const types = {};
220
- for (const [alias, ref] of Object.entries(CHART_TYPE_MAP)) {
221
- if (!types[ref]) types[ref] = [];
222
- types[ref].push(alias);
223
- }
224
-
225
- let text = "# amCharts 5 — Available Chart Types\n\n";
226
- for (const [ref, aliases] of Object.entries(types)) {
227
- const doc = docs.get(ref);
228
- const title = doc ? doc.title : ref;
229
- text += `## ${title}\n`;
230
- text += `Reference: \`${ref}\`\n`;
231
- text += `Keywords: ${aliases.join(", ")}\n\n`;
232
- }
233
- return { content: [{ type: "text", text }] };
234
- }
235
- );
236
-
237
- // --- Tool: get_chart_reference ---
238
- server.tool(
239
- "get_chart_reference",
240
- "Get the full reference documentation for a specific amCharts 5 chart type. Use list_chart_types first to see available types.",
241
- { chartType: z.string().describe("Chart type keyword, e.g. 'pie', 'sankey', 'xy', 'treemap', 'map', 'radar', 'stock', 'gantt', 'venn', 'wordcloud', 'timeline', 'flow', 'hierarchy', 'ui-elements'") },
242
- async ({ chartType }) => {
243
- const key = CHART_TYPE_MAP[chartType.toLowerCase()] || chartType.toLowerCase();
244
- const doc = docs.get(key);
245
- if (!doc) {
246
- const available = [...new Set(Object.values(CHART_TYPE_MAP))].sort().join(", ");
247
- return {
248
- content: [{ type: "text", text: `No reference found for "${chartType}". Available: ${available}` }],
249
- };
250
- }
251
- return { content: [{ type: "text", text: doc.content }] };
252
- }
253
- );
254
-
255
- // --- Tool: get_core_reference ---
256
- server.tool(
257
- "get_core_reference",
258
- "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.",
259
- {},
260
- async () => {
261
- const doc = docs.get("SKILL");
262
- if (!doc) {
263
- return { content: [{ type: "text", text: "Core SKILL.md not found." }] };
264
- }
265
- return { content: [{ type: "text", text: doc.content }] };
266
- }
267
- );
268
-
269
- // --- Tool: search_docs ---
270
- server.tool(
271
- "search_docs",
272
- "Search across all amCharts 5 documentation for a keyword or topic. Returns matching sections ranked by relevance.",
273
- {
274
- query: z.string().describe("Search query, e.g. 'legend', 'axis label rotation', 'tooltip format', 'data processor'"),
275
- maxResults: z.number().optional().default(5).describe("Maximum number of results to return (default 5)"),
276
- },
277
- async ({ query, maxResults }) => {
278
- const results = searchDocs(query, maxResults);
279
- if (results.length === 0) {
280
- return { content: [{ type: "text", text: `No results found for "${query}".` }] };
281
- }
282
-
283
- let text = `# Search results for "${query}"\n\n`;
284
- for (const r of results) {
285
- text += `## ${r.docTitle} → ${r.heading}\n`;
286
- text += `*(source: ${r.file}.md, relevance: ${r.score})*\n\n`;
287
- // Truncate very long sections
288
- const body = r.body.length > 2000 ? r.body.slice(0, 2000) + "\n\n...(truncated)" : r.body;
289
- text += body + "\n\n---\n\n";
290
- }
291
- return { content: [{ type: "text", text }] };
292
- }
293
- );
294
-
295
- // --- Tool: get_section ---
296
- server.tool(
297
- "get_section",
298
- "Get a specific section from an amCharts 5 reference file by heading name. Use search_docs first to find the right section.",
299
- {
300
- file: z.string().describe("Reference file name without .md, e.g. 'xy', 'pie', 'SKILL', 'map'"),
301
- heading: z.string().describe("Section heading to retrieve, e.g. 'Core setup pattern', 'Axis types', 'Common pitfalls'"),
302
- },
303
- async ({ file, heading }) => {
304
- const doc = docs.get(file);
305
- if (!doc) {
306
- return { content: [{ type: "text", text: `File "${file}" not found.` }] };
307
- }
308
-
309
- const section = doc.sections.find(
310
- s => s.heading.toLowerCase().includes(heading.toLowerCase())
311
- );
312
- if (!section) {
313
- const available = doc.sections.map(s => s.heading).filter(Boolean).join("\n- ");
314
- return {
315
- content: [{ type: "text", text: `Section "${heading}" not found in ${file}.md.\n\nAvailable sections:\n- ${available}` }],
316
- };
317
- }
318
- return {
319
- content: [{ type: "text", text: `# ${section.heading}\n\n${section.body}` }],
320
- };
321
- }
322
- );
323
-
324
- // --- Tool: get_quick_start ---
325
- server.tool(
326
- "get_quick_start",
327
- "Get a quick-start template for a specific chart type — a minimal working example ready to customize.",
328
- {
329
- chartType: z.string().describe("Chart type, e.g. 'pie', 'line', 'bar', 'map', 'sankey', 'treemap'"),
330
- 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"),
331
- },
332
- async ({ chartType, format }) => {
333
- const key = CHART_TYPE_MAP[chartType.toLowerCase()] || chartType.toLowerCase();
334
- const doc = docs.get(key);
335
- if (!doc) {
336
- return {
337
- content: [{ type: "text", text: `No reference found for "${chartType}". Use list_chart_types to see available types.` }],
338
- };
339
- }
340
-
341
- // Find the setup/core pattern section
342
- const setupSection = doc.sections.find(s =>
343
- /setup|core.*pattern|basic|quick.*start|required.*import/i.test(s.heading)
344
- );
345
-
346
- if (!setupSection) {
347
- // Fallback: return first few sections
348
- const intro = doc.sections.slice(0, 4).map(s => `## ${s.heading}\n${s.body}`).join("\n\n");
349
- return { content: [{ type: "text", text: intro }] };
350
- }
351
-
352
- // Also grab the imports section
353
- const importSection = doc.sections.find(s =>
354
- /import/i.test(s.heading)
355
- );
356
-
357
- let text = `# Quick Start: ${doc.title}\n\n`;
358
- if (importSection && importSection !== setupSection) {
359
- if (format === "esm") {
360
- text += `## Imports (ES modules)\n${importSection.body}\n\n`;
361
- } else {
362
- text += `## Imports\n${importSection.body}\n\n`;
363
- }
364
- }
365
- text += `## ${setupSection.heading}\n${setupSection.body}`;
366
-
367
- return { content: [{ type: "text", text }] };
368
- }
369
- );
370
-
371
- // --- Tool: search_all ---
372
- server.tool(
373
- "search_all",
374
- "Search across ALL amCharts 5 content: skill references, full documentation, and code examples. Use this for broad searches across everything.",
375
- {
376
- query: z.string().describe("Search query, e.g. 'react integration', 'date axis formatting', 'stacked bar example'"),
377
- maxResults: z.number().optional().default(10).describe("Maximum results (default 10)"),
378
- },
379
- async ({ query, maxResults }) => {
380
- const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
381
- const results = [];
382
-
383
- // Search skill docs
384
- for (const [name, doc] of docs) {
385
- for (const section of doc.sections) {
386
- const text = (section.heading + " " + section.body).toLowerCase();
387
- const score = terms.reduce((s, t) => s + (text.includes(t) ? 1 : 0), 0);
388
- if (score > 0) {
389
- results.push({ source: `skill/${name}`, title: doc.title, heading: section.heading, preview: section.body.slice(0, 300), score: score + 0.1, type: "skill" });
390
- }
391
- }
392
- }
393
-
394
- // Search extended docs
395
- for (const [key, doc] of extendedDocs) {
396
- for (const section of doc.sections) {
397
- const text = (section.heading + " " + section.body).toLowerCase();
398
- const score = terms.reduce((s, t) => s + (text.includes(t) ? 1 : 0), 0);
399
- if (score > 0) {
400
- results.push({ source: key, title: doc.title, heading: section.heading, preview: section.body.slice(0, 300), score, type: "docs" });
401
- }
402
- }
403
- }
404
-
405
- // Search examples
406
- for (const [key, ex] of examples) {
407
- const text = (ex.title + " " + ex.category + " " + ex.content).toLowerCase();
408
- const score = terms.reduce((s, t) => s + (text.includes(t) ? 1 : 0), 0);
409
- if (score > 0) {
410
- results.push({ source: key, title: ex.title, heading: ex.category, preview: ex.content.slice(0, 300), score, type: "example" });
411
- }
412
- }
413
-
414
- results.sort((a, b) => b.score - a.score);
415
- const top = results.slice(0, maxResults);
416
-
417
- if (top.length === 0) {
418
- return { content: [{ type: "text", text: `No results found for "${query}".` }] };
419
- }
420
-
421
- let text = `# Search results for "${query}" (${results.length} total, showing ${top.length})\n\n`;
422
- for (const r of top) {
423
- text += `## [${r.type}] ${r.title}${r.heading ? ` → ${r.heading}` : ""}\n`;
424
- text += `*Source: ${r.source} | Relevance: ${r.score}*\n\n`;
425
- text += r.preview + "\n\n---\n\n";
426
- }
427
- return { content: [{ type: "text", text }] };
428
- }
429
- );
430
-
431
- // --- Tool: get_doc ---
432
- server.tool(
433
- "get_doc",
434
- "Get a full documentation page from the extended amCharts 5 docs. Use search_all first to find the right path.",
435
- {
436
- path: z.string().describe("Doc path, e.g. 'charts/xy-chart/cursor', 'concepts/events', 'getting-started/integrations/react'"),
437
- },
438
- async ({ path: docPath }) => {
439
- const key = docPath.replace(/^\/|\/$/g, "").replace(/\.md$/, "");
440
- const doc = extendedDocs.get(key);
441
- if (!doc) {
442
- // List available top-level paths
443
- const paths = [...extendedDocs.keys()];
444
- const topLevel = [...new Set(paths.map(p => p.split("/")[0]))].sort();
445
- return {
446
- content: [{ type: "text", text: `Doc "${key}" not found.\n\nAvailable top-level sections: ${topLevel.join(", ")}\n\nUse search_all to find the right path.` }],
447
- };
448
- }
449
- return { content: [{ type: "text", text: `# ${doc.title}\n\n${doc.content}` }] };
450
- }
451
- );
452
-
453
- // --- Tool: list_examples ---
454
- server.tool(
455
- "list_examples",
456
- "List all available amCharts 5 code examples, optionally filtered by category.",
457
- {
458
- category: z.string().optional().describe("Filter by category, e.g. 'column-bar', 'line-area', 'pie-donut', 'maps', 'flow', 'hierarchy', 'stock', 'gantt', 'gauges', 'radar-polar', 'timeline', 'funnel-pyramid'. Omit to list all categories."),
459
- },
460
- async ({ category }) => {
461
- if (!category) {
462
- // List categories with counts
463
- const cats = {};
464
- for (const [, ex] of examples) {
465
- cats[ex.category] = (cats[ex.category] || 0) + 1;
466
- }
467
- let text = "# amCharts 5 — Example Categories\n\n";
468
- for (const [cat, count] of Object.entries(cats).sort((a, b) => a[0].localeCompare(b[0]))) {
469
- text += `- **${cat}** (${count} examples)\n`;
470
- }
471
- text += `\n**Total: ${examples.size} examples**\n`;
472
- text += "\nUse list_examples with a category to see all examples in that category.";
473
- return { content: [{ type: "text", text }] };
474
- }
475
-
476
- const matching = [...examples.entries()]
477
- .filter(([, ex]) => ex.category === category)
478
- .sort((a, b) => a[1].title.localeCompare(b[1].title));
479
-
480
- if (matching.length === 0) {
481
- const cats = [...new Set([...examples.values()].map(e => e.category))].sort();
482
- return { content: [{ type: "text", text: `No examples in category "${category}".\n\nAvailable: ${cats.join(", ")}` }] };
483
- }
484
-
485
- let text = `# ${category} examples (${matching.length})\n\n`;
486
- for (const [key, ex] of matching) {
487
- text += `- **${ex.title}** — \`${key}\`\n`;
488
- }
489
- text += "\nUse get_example with the path to get the full code.";
490
- return { content: [{ type: "text", text }] };
491
- }
492
- );
493
-
494
- // --- Tool: get_example ---
495
- server.tool(
496
- "get_example",
497
- "Get the full code for a specific amCharts 5 example/demo. Use list_examples or search_all to find the path.",
498
- {
499
- path: z.string().describe("Example path, e.g. 'examples/flow/sankey-diagram', 'examples/pie-donut/donut-chart', 'examples/maps/drill-down-map'"),
500
- },
501
- async ({ path: exPath }) => {
502
- const key = exPath.replace(/^\/|\/$/g, "").replace(/\.md$/, "");
503
- const ex = examples.get(key);
504
- if (!ex) {
505
- return { content: [{ type: "text", text: `Example "${key}" not found. Use list_examples to browse available examples.` }] };
506
- }
507
- let text = `# ${ex.title}\n`;
508
- text += `*Category: ${ex.category}*\n`;
509
- if (ex.source) text += `*Source: ${ex.source}*\n`;
510
- text += `\n${ex.content}`;
511
- return { content: [{ type: "text", text }] };
512
- }
513
- );
514
-
515
- // ---------------------------------------------------------------------------
516
- // Resources: expose each doc as an MCP resource
517
- // ---------------------------------------------------------------------------
518
-
519
- for (const [name, doc] of docs) {
520
- server.resource(
521
- name,
522
- `amcharts://docs/${name}`,
523
- { description: `amCharts 5 reference: ${doc.title}`, mimeType: "text/markdown" },
524
- async () => ({
525
- contents: [{ uri: `amcharts://docs/${name}`, text: doc.content, mimeType: "text/markdown" }],
526
- })
527
- );
528
- }
529
-
530
- // ---------------------------------------------------------------------------
531
- // Start
532
- // ---------------------------------------------------------------------------
26
+ registerTools(server, content);
533
27
 
534
28
  const transport = new StdioServerTransport();
535
29
  await server.connect(transport);