@amcharts/amcharts5-mcp 1.2.0 → 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,10 +2,11 @@
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, "..");
@@ -14,639 +15,15 @@ const ROOT = join(__dirname, "..");
14
15
  const PKG = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
15
16
  const VERSION = PKG.version;
16
17
 
17
- // Submodule path (local dev) or content/ fallback (npm install)
18
- const SUBMODULE_DIR = join(ROOT, "amcharts5-skill", "amcharts5-skill");
19
- const CONTENT_DIR = join(ROOT, "content");
20
- const SKILL_DIR = existsSync(SUBMODULE_DIR) ? SUBMODULE_DIR : CONTENT_DIR;
21
- const REFERENCES_DIR = existsSync(join(SKILL_DIR, "references"))
22
- ? join(SKILL_DIR, "references")
23
- : SKILL_DIR;
24
- const EXTENDED_DIR = join(ROOT, "extended");
25
-
26
- // ---------------------------------------------------------------------------
27
- // Load and index all content at startup
28
- // ---------------------------------------------------------------------------
29
-
30
- /** @type {Map<string, {title: string, content: string, sections: {heading: string, body: string}[]}>} */
31
- const docs = new Map();
32
-
33
- /** @type {Map<string, {title: string, content: string, sections: {heading: string, body: string}[], path: string}>} */
34
- const extendedDocs = new Map();
35
-
36
- /** @type {Map<string, {title: string, category: string, content: string, source: string}>} */
37
- const examples = new Map();
38
-
39
- /** Chart type aliases → reference file mapping */
40
- const CHART_TYPE_MAP = {
41
- // XY family
42
- "xy": "xy", "line": "xy", "area": "xy", "bar": "xy", "column": "xy",
43
- "candlestick": "xy", "ohlc": "xy", "scatter": "xy", "bubble": "xy",
44
- "stacked": "xy", "stacked-bar": "xy", "stacked-column": "xy",
45
- "stacked-area": "xy", "range": "xy", "waterfall": "xy",
46
- // Pie family
47
- "pie": "pie", "donut": "pie", "funnel": "pie", "pyramid": "pie",
48
- "semi-circle": "pie", "pictorial": "pie", "sliced": "pie",
49
- // Map
50
- "map": "map", "choropleth": "map", "geo": "map", "geographic": "map",
51
- "world": "map", "country": "map", "bubble-map": "map", "point-map": "map",
52
- // Hierarchy
53
- "hierarchy": "hierarchy", "treemap": "hierarchy", "sunburst": "hierarchy",
54
- "force-directed": "hierarchy", "pack": "hierarchy", "partition": "hierarchy",
55
- "tree": "hierarchy", "org-chart": "hierarchy",
56
- // Flow
57
- "flow": "flow", "sankey": "flow", "chord": "flow", "arc": "flow",
58
- "alluvial": "flow",
59
- // Radar
60
- "radar": "radar", "spider": "radar", "polar": "radar", "gauge": "radar",
61
- "speedometer": "radar", "meter": "radar",
62
- // Stock
63
- "stock": "stock", "financial": "stock", "trading": "stock",
64
- // Timeline
65
- "timeline": "timeline", "serpentine": "timeline", "spiral": "timeline",
66
- "curve": "timeline",
67
- // Gantt
68
- "gantt": "gantt", "project-timeline": "gantt",
69
- // Word cloud
70
- "wordcloud": "wordcloud", "word-cloud": "wordcloud", "tag-cloud": "wordcloud",
71
- // Venn
72
- "venn": "venn",
73
- // UI
74
- "ui": "ui-elements", "ui-elements": "ui-elements", "button": "ui-elements",
75
- "slider": "ui-elements", "controls": "ui-elements",
76
- };
77
-
78
- function parseSections(content) {
79
- const sections = [];
80
- const lines = content.split("\n");
81
- let currentHeading = "";
82
- let currentLevel = 0;
83
- let currentBody = [];
84
-
85
- for (const line of lines) {
86
- const headingMatch = line.match(/^(#{1,3})\s+(.+)/);
87
- if (headingMatch) {
88
- const level = headingMatch[1].length;
89
- const title = headingMatch[2].trim();
90
- if (level <= 2) {
91
- // Major section boundary (# or ##)
92
- if (currentHeading || currentBody.length > 0) {
93
- sections.push({ heading: currentHeading, body: currentBody.join("\n") });
94
- }
95
- currentHeading = title;
96
- currentLevel = level;
97
- currentBody = [];
98
- } else {
99
- // Sub-heading (###) — include in current section body
100
- currentBody.push(line);
101
- }
102
- } else {
103
- currentBody.push(line);
104
- }
105
- }
106
- if (currentHeading || currentBody.length > 0) {
107
- sections.push({ heading: currentHeading, body: currentBody.join("\n") });
108
- }
109
- return sections;
110
- }
111
-
112
- function loadFile(filePath, name) {
113
- const content = readFileSync(filePath, "utf-8");
114
- const titleMatch = content.match(/^#\s+(.+)/m);
115
- const title = titleMatch ? titleMatch[1] : name;
116
- docs.set(name, { title, content, sections: parseSections(content) });
117
- }
118
-
119
- function loadContent() {
120
- // Load core SKILL.md
121
- const skillPath = join(SKILL_DIR, "SKILL.md");
122
- if (existsSync(skillPath)) {
123
- loadFile(skillPath, "SKILL");
124
- }
125
-
126
- // Load all reference files
127
- const files = readdirSync(REFERENCES_DIR).filter(f => f.endsWith(".md") && f !== "SKILL.md");
128
- for (const file of files) {
129
- loadFile(join(REFERENCES_DIR, file), file.replace(".md", ""));
130
- }
131
- }
132
-
133
- loadContent();
134
-
135
- // ---------------------------------------------------------------------------
136
- // Load extended docs and examples
137
- // ---------------------------------------------------------------------------
138
-
139
- function loadExtendedDir(dir, prefix = "") {
140
- if (!existsSync(dir)) return;
141
- const entries = readdirSync(dir, { withFileTypes: true });
142
- for (const entry of entries) {
143
- const fullPath = join(dir, entry.name);
144
- if (entry.isDirectory()) {
145
- loadExtendedDir(fullPath, prefix ? `${prefix}/${entry.name}` : entry.name);
146
- } else if (entry.name.endsWith(".md")) {
147
- const key = prefix ? `${prefix}/${entry.name.replace(".md", "")}` : entry.name.replace(".md", "");
148
- const content = readFileSync(fullPath, "utf-8");
149
-
150
- // Parse frontmatter
151
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
152
- const body = fmMatch ? fmMatch[2].trim() : content;
153
- const fm = fmMatch ? fmMatch[1] : "";
154
- const titleMatch = fm.match(/title:\s*"(.+?)"/);
155
- const categoryMatch = fm.match(/category:\s*"(.+?)"/);
156
- const sourceMatch = fm.match(/source:\s*"(.+?)"/);
157
- const title = titleMatch ? titleMatch[1] : entry.name.replace(".md", "");
158
-
159
- if (key.startsWith("examples/")) {
160
- examples.set(key, {
161
- title,
162
- category: categoryMatch ? categoryMatch[1] : "",
163
- content: body,
164
- source: sourceMatch ? sourceMatch[1] : "",
165
- });
166
- } else {
167
- extendedDocs.set(key, {
168
- title,
169
- content: body,
170
- sections: parseSections(body),
171
- path: key,
172
- });
173
- }
174
- }
175
- }
176
- }
177
-
178
- loadExtendedDir(EXTENDED_DIR);
179
-
180
- // ---------------------------------------------------------------------------
181
- // Search helpers
182
- // ---------------------------------------------------------------------------
183
-
184
- /**
185
- * Import sections carry two ### sub-blocks: one for ES modules, one for CDN /
186
- * script tags. Return only the requested one (esm | html), or "" if not found.
187
- */
188
- function extractImportBlock(body, format) {
189
- const lines = body.split("\n");
190
- const wantEsm = format === "esm";
191
- let capturing = false;
192
- let captured = [];
193
- let sawSub = false;
194
- for (const line of lines) {
195
- const sub = line.match(/^###\s+(.+)/);
196
- if (sub) {
197
- sawSub = true;
198
- const h = sub[1].toLowerCase();
199
- const isEsm = /es module|esm|typescript|import/.test(h);
200
- // Note: "typescript" contains "script", so match "script tag"/"cdn", not bare "script".
201
- const isCdn = /cdn|script tag|<script/.test(h);
202
- capturing = wantEsm ? isEsm : isCdn;
203
- continue;
204
- }
205
- if (capturing) captured.push(line);
206
- }
207
- if (!sawSub) return ""; // no sub-blocks → caller falls back to full body
208
- return captured.join("\n").trim();
209
- }
210
-
211
- function searchDocs(query, maxResults = 10, scope = "skill") {
212
- const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
213
- const results = [];
214
-
215
- // Skill docs (SKILL.md + references/*.md)
216
- for (const [name, doc] of docs) {
217
- for (const section of doc.sections) {
218
- const text = (section.heading + " " + section.body).toLowerCase();
219
- const score = terms.reduce((s, t) => s + (text.includes(t) ? 1 : 0), 0);
220
- if (score > 0) {
221
- results.push({
222
- file: name,
223
- source: "skill",
224
- docTitle: doc.title,
225
- heading: section.heading,
226
- body: section.body.trim(),
227
- // Nudge skill results slightly ahead of extended on ties — the skill
228
- // is the curated layer; extended is the exhaustive API reference.
229
- score: score + 0.1,
230
- });
231
- }
232
- }
233
- }
234
-
235
- // Extended docs (charts/, concepts/, getting-started/, reference/<class>) —
236
- // this is where the per-class API + defaults live. Only scanned when scope=all.
237
- if (scope === "all") {
238
- for (const [key, doc] of extendedDocs) {
239
- for (const section of doc.sections) {
240
- const text = (section.heading + " " + section.body).toLowerCase();
241
- const score = terms.reduce((s, t) => s + (text.includes(t) ? 1 : 0), 0);
242
- if (score > 0) {
243
- results.push({
244
- file: key,
245
- source: "extended",
246
- docTitle: doc.title,
247
- heading: section.heading,
248
- body: section.body.trim(),
249
- score,
250
- });
251
- }
252
- }
253
- }
254
- }
255
-
256
- results.sort((a, b) => b.score - a.score);
257
- return results.slice(0, maxResults);
258
- }
259
-
260
- // ---------------------------------------------------------------------------
261
- // MCP Server
262
- // ---------------------------------------------------------------------------
18
+ // Load and index all content from disk (submodule + extended/).
19
+ const content = loadContentFromDisk();
263
20
 
264
21
  const server = new McpServer({
265
22
  name: "amcharts5",
266
23
  version: VERSION,
267
24
  });
268
25
 
269
- // --- Tool: list_chart_types ---
270
- server.tool(
271
- "list_chart_types",
272
- "List all amCharts 5 chart types with their categories and reference file names",
273
- {},
274
- async () => {
275
- const types = {};
276
- for (const [alias, ref] of Object.entries(CHART_TYPE_MAP)) {
277
- if (!types[ref]) types[ref] = [];
278
- types[ref].push(alias);
279
- }
280
-
281
- let text = "# amCharts 5 — Available Chart Types\n\n";
282
- for (const [ref, aliases] of Object.entries(types)) {
283
- const doc = docs.get(ref);
284
- const title = doc ? doc.title : ref;
285
- text += `## ${title}\n`;
286
- text += `Reference: \`${ref}\`\n`;
287
- text += `Keywords: ${aliases.join(", ")}\n\n`;
288
- }
289
- return { content: [{ type: "text", text }] };
290
- }
291
- );
292
-
293
- // --- Tool: get_chart_reference ---
294
- server.tool(
295
- "get_chart_reference",
296
- "Get the full reference documentation for a specific amCharts 5 chart type. Use list_chart_types first to see available types.",
297
- { chartType: z.string().describe("Chart type keyword, e.g. 'pie', 'sankey', 'xy', 'treemap', 'map', 'radar', 'stock', 'gantt', 'venn', 'wordcloud', 'timeline', 'flow', 'hierarchy', 'ui-elements'") },
298
- async ({ chartType }) => {
299
- const key = CHART_TYPE_MAP[chartType.toLowerCase()] || chartType.toLowerCase();
300
- const doc = docs.get(key);
301
- if (!doc) {
302
- const available = [...new Set(Object.values(CHART_TYPE_MAP))].sort().join(", ");
303
- return {
304
- content: [{ type: "text", text: `No reference found for "${chartType}". Available: ${available}` }],
305
- };
306
- }
307
- return { content: [{ type: "text", text: doc.content }] };
308
- }
309
- );
310
-
311
- // --- Tool: get_core_reference ---
312
- server.tool(
313
- "get_core_reference",
314
- "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.",
315
- {},
316
- async () => {
317
- const doc = docs.get("SKILL");
318
- if (!doc) {
319
- return { content: [{ type: "text", text: "Core SKILL.md not found." }] };
320
- }
321
- return { content: [{ type: "text", text: doc.content }] };
322
- }
323
- );
324
-
325
- // --- Tool: search_docs ---
326
- server.tool(
327
- "search_docs",
328
- "Search the curated amCharts 5 skill docs (SKILL.md + chart references) for a keyword or topic, returning matching sections ranked by relevance. NOTE: by default this scans ONLY the skill layer. The per-class API reference and per-setting DEFAULTS (e.g. tooltip background cornerRadius, axis tick `visible` default, cursor line strokeDasharray) live in the extended docs — pass scope:'all' here, or use search_all / get_api_reference / get_doc, to reach them.",
329
- {
330
- query: z.string().describe("Search query, e.g. 'legend', 'axis label rotation', 'tooltip format', 'data processor'"),
331
- maxResults: z.number().optional().default(5).describe("Maximum number of results to return (default 5)"),
332
- scope: z.enum(["skill", "all"]).optional().default("skill").describe("'skill' (default) searches only the curated skill docs; 'all' also searches the extended docs / per-class API reference where settings and defaults live."),
333
- },
334
- async ({ query, maxResults, scope }) => {
335
- const results = searchDocs(query, maxResults, scope);
336
- if (results.length === 0) {
337
- const hint = scope === "skill"
338
- ? ` Nothing in the skill docs — retry with scope:'all' to include the API reference, or use search_all.`
339
- : "";
340
- return { content: [{ type: "text", text: `No results found for "${query}".${hint}` }] };
341
- }
342
-
343
- let text = `# Search results for "${query}" (scope: ${scope})\n\n`;
344
- for (const r of results) {
345
- text += `## ${r.docTitle} → ${r.heading}\n`;
346
- text += `*(source: ${r.source}/${r.file}.md, relevance: ${r.score})*\n\n`;
347
- // Truncate very long sections
348
- const body = r.body.length > 2000 ? r.body.slice(0, 2000) + "\n\n...(truncated)" : r.body;
349
- text += body + "\n\n---\n\n";
350
- }
351
- return { content: [{ type: "text", text }] };
352
- }
353
- );
354
-
355
- // --- Tool: get_section ---
356
- server.tool(
357
- "get_section",
358
- "Get a specific section from an amCharts 5 reference file by heading name. Use search_docs first to find the right section.",
359
- {
360
- file: z.string().describe("Reference file name without .md, e.g. 'xy', 'pie', 'SKILL', 'map'"),
361
- heading: z.string().describe("Section heading to retrieve, e.g. 'Core setup pattern', 'Axis types', 'Common pitfalls'"),
362
- },
363
- async ({ file, heading }) => {
364
- const doc = docs.get(file);
365
- if (!doc) {
366
- return { content: [{ type: "text", text: `File "${file}" not found.` }] };
367
- }
368
-
369
- const section = doc.sections.find(
370
- s => s.heading.toLowerCase().includes(heading.toLowerCase())
371
- );
372
- if (!section) {
373
- const available = doc.sections.map(s => s.heading).filter(Boolean).join("\n- ");
374
- return {
375
- content: [{ type: "text", text: `Section "${heading}" not found in ${file}.md.\n\nAvailable sections:\n- ${available}` }],
376
- };
377
- }
378
- return {
379
- content: [{ type: "text", text: `# ${section.heading}\n\n${section.body}` }],
380
- };
381
- }
382
- );
383
-
384
- // --- Tool: get_quick_start ---
385
- server.tool(
386
- "get_quick_start",
387
- "Get a quick-start template for a specific chart type — a minimal working example ready to customize.",
388
- {
389
- chartType: z.string().describe("Chart type, e.g. 'pie', 'line', 'bar', 'map', 'sankey', 'treemap'"),
390
- 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"),
391
- },
392
- async ({ chartType, format }) => {
393
- const key = CHART_TYPE_MAP[chartType.toLowerCase()] || chartType.toLowerCase();
394
- const doc = docs.get(key);
395
- if (!doc) {
396
- return {
397
- content: [{ type: "text", text: `No reference found for "${chartType}". Use list_chart_types to see available types.` }],
398
- };
399
- }
400
-
401
- // Grab the imports section first so we can exclude it from the setup match.
402
- const importSection = doc.sections.find(s =>
403
- /import/i.test(s.heading)
404
- );
405
-
406
- // Find the setup/core pattern section — must NOT be the imports section
407
- // (headings like "Required imports" otherwise win and the actual core
408
- // setup pattern is never shown, making esm/html identical).
409
- const setupSection = doc.sections.find(s =>
410
- s !== importSection && /setup|core.*pattern|basic|quick.*start/i.test(s.heading)
411
- );
412
-
413
- if (!setupSection) {
414
- // Fallback: return first few sections
415
- const intro = doc.sections.slice(0, 4).map(s => `## ${s.heading}\n${s.body}`).join("\n\n");
416
- return { content: [{ type: "text", text: intro }] };
417
- }
418
-
419
- let text = `# Quick Start: ${doc.title}\n\n`;
420
- if (importSection && importSection !== setupSection) {
421
- // Import sections contain both an "ES modules" and a "CDN / script tags"
422
- // sub-block. Emit only the one matching the requested format so esm and
423
- // html actually differ (instead of relabeling the same body).
424
- const esm = extractImportBlock(importSection.body, "esm");
425
- const cdn = extractImportBlock(importSection.body, "html");
426
- if (format === "esm") {
427
- text += `## Imports (ES modules)\n${esm || importSection.body}\n\n`;
428
- } else {
429
- text += `## Imports (CDN / script tags)\n${cdn || importSection.body}\n\n`;
430
- }
431
- }
432
- text += `## ${setupSection.heading}\n${setupSection.body}`;
433
-
434
- return { content: [{ type: "text", text }] };
435
- }
436
- );
437
-
438
- // --- Tool: search_all ---
439
- server.tool(
440
- "search_all",
441
- "Search across ALL amCharts 5 content: skill references, full documentation, and code examples. Use this for broad searches across everything.",
442
- {
443
- query: z.string().describe("Search query, e.g. 'react integration', 'date axis formatting', 'stacked bar example'"),
444
- maxResults: z.number().optional().default(10).describe("Maximum results (default 10)"),
445
- },
446
- async ({ query, maxResults }) => {
447
- const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
448
- const results = [];
449
-
450
- // Search skill docs
451
- for (const [name, doc] of docs) {
452
- for (const section of doc.sections) {
453
- const text = (section.heading + " " + section.body).toLowerCase();
454
- const score = terms.reduce((s, t) => s + (text.includes(t) ? 1 : 0), 0);
455
- if (score > 0) {
456
- results.push({ source: `skill/${name}`, title: doc.title, heading: section.heading, preview: section.body.slice(0, 300), score: score + 0.1, type: "skill" });
457
- }
458
- }
459
- }
460
-
461
- // Search extended docs
462
- for (const [key, doc] of extendedDocs) {
463
- for (const section of doc.sections) {
464
- const text = (section.heading + " " + section.body).toLowerCase();
465
- const score = terms.reduce((s, t) => s + (text.includes(t) ? 1 : 0), 0);
466
- if (score > 0) {
467
- results.push({ source: key, title: doc.title, heading: section.heading, preview: section.body.slice(0, 300), score, type: "docs" });
468
- }
469
- }
470
- }
471
-
472
- // Search examples
473
- for (const [key, ex] of examples) {
474
- const text = (ex.title + " " + ex.category + " " + ex.content).toLowerCase();
475
- const score = terms.reduce((s, t) => s + (text.includes(t) ? 1 : 0), 0);
476
- if (score > 0) {
477
- results.push({ source: key, title: ex.title, heading: ex.category, preview: ex.content.slice(0, 300), score, type: "example" });
478
- }
479
- }
480
-
481
- results.sort((a, b) => b.score - a.score);
482
- const top = results.slice(0, maxResults);
483
-
484
- if (top.length === 0) {
485
- return { content: [{ type: "text", text: `No results found for "${query}".` }] };
486
- }
487
-
488
- let text = `# Search results for "${query}" (${results.length} total, showing ${top.length})\n\n`;
489
- for (const r of top) {
490
- text += `## [${r.type}] ${r.title}${r.heading ? ` → ${r.heading}` : ""}\n`;
491
- text += `*Source: ${r.source} | Relevance: ${r.score}*\n\n`;
492
- text += r.preview + "\n\n---\n\n";
493
- }
494
- return { content: [{ type: "text", text }] };
495
- }
496
- );
497
-
498
- // --- Tool: get_doc ---
499
- server.tool(
500
- "get_doc",
501
- "Get a full documentation page from the extended amCharts 5 docs. Use search_all first to find the right path.",
502
- {
503
- path: z.string().describe("Doc path, e.g. 'charts/xy-chart/cursor', 'concepts/events', 'getting-started/integrations/react', 'reference/xycursor' (per-class API). For a class's full settings + defaults prefer get_api_reference."),
504
- },
505
- async ({ path: docPath }) => {
506
- const key = docPath.replace(/^\/|\/$/g, "").replace(/\.md$/, "");
507
- const doc = extendedDocs.get(key);
508
- if (!doc) {
509
- // List available top-level paths
510
- const paths = [...extendedDocs.keys()];
511
- const topLevel = [...new Set(paths.map(p => p.split("/")[0]))].sort();
512
- return {
513
- content: [{ type: "text", text: `Doc "${key}" not found.\n\nAvailable top-level sections: ${topLevel.join(", ")}\n\nUse search_all to find the right path.` }],
514
- };
515
- }
516
- return { content: [{ type: "text", text: `# ${doc.title}\n\n${doc.content}` }] };
517
- }
518
- );
519
-
520
- // --- Tool: get_api_reference ---
521
- server.tool(
522
- "get_api_reference",
523
- "Get the per-class API reference for an amCharts 5 class — the class page PLUS its settings table (with DEFAULTS) and, optionally, its private/read-only settings. This is the authoritative source for exact setting names and default values (e.g. tooltip background cornerRadius, axis tick `visible` default). Pass a class name in any casing, e.g. 'XYCursor', 'Tooltip', 'PieSeries', 'AxisRendererX'.",
524
- {
525
- className: z.string().describe("amCharts 5 class name, e.g. 'XYCursor', 'Tooltip', 'PieSeries', 'ColumnSeries', 'AxisRendererX'. Casing/punctuation are ignored."),
526
- includePrivate: z.boolean().optional().default(false).describe("Also include the private (read-only / internal) settings table. Default false."),
527
- },
528
- async ({ className, includePrivate }) => {
529
- // Normalize: "XYCursor" / "xy-cursor" / "XY Cursor" → "xycursor"
530
- const norm = className.toLowerCase().replace(/[^a-z0-9]/g, "");
531
- const classDoc = extendedDocs.get(`reference/${norm}`);
532
- const settingsDoc = extendedDocs.get(`reference/i${norm}settings`);
533
- const privateDoc = extendedDocs.get(`reference/i${norm}private`);
534
-
535
- if (!classDoc && !settingsDoc) {
536
- // Friendly not-found: suggest close matches among the class pages
537
- // (reference keys that are NOT i…settings / i…private interface pages).
538
- const classKeys = [...extendedDocs.keys()]
539
- .filter(k => k.startsWith("reference/"))
540
- .map(k => k.slice("reference/".length))
541
- .filter(k => !k.startsWith("i"));
542
- const suggestions = classKeys
543
- .filter(k => k.includes(norm) || norm.includes(k))
544
- .slice(0, 10);
545
- let msg = `No API reference found for "${className}" (normalized "${norm}").`;
546
- if (suggestions.length) {
547
- msg += `\n\nDid you mean: ${suggestions.join(", ")}?`;
548
- }
549
- msg += `\n\nTip: use search_all to find the right class, or list_chart_types for chart families.`;
550
- return { content: [{ type: "text", text: msg }] };
551
- }
552
-
553
- let text = "";
554
- if (classDoc) {
555
- text += `# ${classDoc.title}\n\n${classDoc.content}\n\n`;
556
- }
557
- if (settingsDoc) {
558
- text += `\n---\n\n# ${settingsDoc.title} (settings + defaults)\n\n${settingsDoc.content}\n\n`;
559
- }
560
- if (includePrivate && privateDoc) {
561
- text += `\n---\n\n# ${privateDoc.title} (private / read-only)\n\n${privateDoc.content}\n\n`;
562
- }
563
- if (!includePrivate && privateDoc) {
564
- text += `\n---\n\n*Private/read-only settings also exist — call get_api_reference("${className}", includePrivate: true) to include them.*\n`;
565
- }
566
- return { content: [{ type: "text", text: text.trim() }] };
567
- }
568
- );
569
-
570
- // --- Tool: list_examples ---
571
- server.tool(
572
- "list_examples",
573
- "List all available amCharts 5 code examples, optionally filtered by category.",
574
- {
575
- 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."),
576
- },
577
- async ({ category }) => {
578
- if (!category) {
579
- // List categories with counts
580
- const cats = {};
581
- for (const [, ex] of examples) {
582
- cats[ex.category] = (cats[ex.category] || 0) + 1;
583
- }
584
- let text = "# amCharts 5 — Example Categories\n\n";
585
- for (const [cat, count] of Object.entries(cats).sort((a, b) => a[0].localeCompare(b[0]))) {
586
- text += `- **${cat}** (${count} examples)\n`;
587
- }
588
- text += `\n**Total: ${examples.size} examples**\n`;
589
- text += "\nUse list_examples with a category to see all examples in that category.";
590
- return { content: [{ type: "text", text }] };
591
- }
592
-
593
- const matching = [...examples.entries()]
594
- .filter(([, ex]) => ex.category === category)
595
- .sort((a, b) => a[1].title.localeCompare(b[1].title));
596
-
597
- if (matching.length === 0) {
598
- const cats = [...new Set([...examples.values()].map(e => e.category))].sort();
599
- return { content: [{ type: "text", text: `No examples in category "${category}".\n\nAvailable: ${cats.join(", ")}` }] };
600
- }
601
-
602
- let text = `# ${category} examples (${matching.length})\n\n`;
603
- for (const [key, ex] of matching) {
604
- text += `- **${ex.title}** — \`${key}\`\n`;
605
- }
606
- text += "\nUse get_example with the path to get the full code.";
607
- return { content: [{ type: "text", text }] };
608
- }
609
- );
610
-
611
- // --- Tool: get_example ---
612
- server.tool(
613
- "get_example",
614
- "Get the full code for a specific amCharts 5 example/demo. Use list_examples or search_all to find the path.",
615
- {
616
- path: z.string().describe("Example path, e.g. 'examples/flow/sankey-diagram', 'examples/pie-donut/donut-chart', 'examples/maps/drill-down-map'"),
617
- },
618
- async ({ path: exPath }) => {
619
- const key = exPath.replace(/^\/|\/$/g, "").replace(/\.md$/, "");
620
- const ex = examples.get(key);
621
- if (!ex) {
622
- return { content: [{ type: "text", text: `Example "${key}" not found. Use list_examples to browse available examples.` }] };
623
- }
624
- let text = `# ${ex.title}\n`;
625
- text += `*Category: ${ex.category}*\n`;
626
- if (ex.source) text += `*Source: ${ex.source}*\n`;
627
- text += `\n${ex.content}`;
628
- return { content: [{ type: "text", text }] };
629
- }
630
- );
631
-
632
- // ---------------------------------------------------------------------------
633
- // Resources: expose each doc as an MCP resource
634
- // ---------------------------------------------------------------------------
635
-
636
- for (const [name, doc] of docs) {
637
- server.resource(
638
- name,
639
- `amcharts://docs/${name}`,
640
- { description: `amCharts 5 reference: ${doc.title}`, mimeType: "text/markdown" },
641
- async () => ({
642
- contents: [{ uri: `amcharts://docs/${name}`, text: doc.content, mimeType: "text/markdown" }],
643
- })
644
- );
645
- }
646
-
647
- // ---------------------------------------------------------------------------
648
- // Start
649
- // ---------------------------------------------------------------------------
26
+ registerTools(server, content);
650
27
 
651
28
  const transport = new StdioServerTransport();
652
29
  await server.connect(transport);