@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/.gitmodules +3 -0
- package/README.md +82 -0
- package/amcharts5-skill/README.md +50 -0
- package/amcharts5-skill/amcharts5-skill/SKILL.md +667 -0
- package/amcharts5-skill/amcharts5-skill/references/flow.md +523 -0
- package/amcharts5-skill/amcharts5-skill/references/gantt.md +410 -0
- package/amcharts5-skill/amcharts5-skill/references/hierarchy.md +568 -0
- package/amcharts5-skill/amcharts5-skill/references/map.md +574 -0
- package/amcharts5-skill/amcharts5-skill/references/pie.md +663 -0
- package/amcharts5-skill/amcharts5-skill/references/radar.md +584 -0
- package/amcharts5-skill/amcharts5-skill/references/stock.md +664 -0
- package/amcharts5-skill/amcharts5-skill/references/timeline.md +449 -0
- package/amcharts5-skill/amcharts5-skill/references/ui-elements.md +393 -0
- package/amcharts5-skill/amcharts5-skill/references/venn.md +368 -0
- package/amcharts5-skill/amcharts5-skill/references/wordcloud.md +339 -0
- package/amcharts5-skill/amcharts5-skill/references/xy.md +1150 -0
- package/amcharts5-skill/cursorrules +820 -0
- package/package.json +27 -0
- package/src/index.js +329 -0
- package/test.js +153 -0
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);
|