adsa-cli 0.1.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/lib/mcp.mjs ADDED
@@ -0,0 +1,162 @@
1
+ /**
2
+ * MCP server over stdio: newline-delimited JSON-RPC, tools only. Hand-rolled, because
3
+ * five tools do not justify a dependency in something people run with npx.
4
+ *
5
+ * It answers from the repository it is started in, so the guides an agent reads are
6
+ * the ones in the checked-out version — not the newest ones on the internet.
7
+ */
8
+ import { read } from "./fsx.mjs";
9
+ import { join } from "node:path";
10
+ import { dense } from "./dense.mjs";
11
+ import { score } from "./score.mjs";
12
+
13
+ const PROTOCOL = "2025-06-18";
14
+
15
+ export const TOOLS = [
16
+ {
17
+ name: "list_components",
18
+ description: "List every documented component with its import path and guide.",
19
+ inputSchema: { type: "object", properties: {} },
20
+ },
21
+ {
22
+ name: "get_guide",
23
+ description: "The full guide for one component: imports, props, rules, examples.",
24
+ inputSchema: {
25
+ type: "object",
26
+ properties: {
27
+ name: { type: "string", description: "Component name, slug or exported symbol." },
28
+ dense: { type: "boolean", description: "Drop prose, keep headings, code and tables. Default true." },
29
+ },
30
+ required: ["name"],
31
+ },
32
+ },
33
+ {
34
+ name: "search",
35
+ description: "Find the component for a task, e.g. 'icon only button', 'date range', 'empty table'.",
36
+ inputSchema: { type: "object", properties: { query: { type: "string" }, limit: { type: "number" } }, required: ["query"] },
37
+ },
38
+ {
39
+ name: "list_gaps",
40
+ description: "What this design system deliberately does not have, and what to use instead. Read this before building anything the guides do not cover.",
41
+ inputSchema: { type: "object", properties: {} },
42
+ },
43
+ {
44
+ name: "readiness_score",
45
+ description: "The agent-readiness score of this design system, per dimension, with evidence.",
46
+ inputSchema: { type: "object", properties: {} },
47
+ },
48
+ ];
49
+
50
+ const STOP = new Set(["a", "an", "the", "for", "with", "and", "or", "of", "to", "in", "on", "is", "it", "my", "our", "how", "do", "i", "use", "using", "component"]);
51
+
52
+ export function searchGuides(facts, query, limit = 8) {
53
+ const terms = query.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1 && !STOP.has(t));
54
+ if (!terms.length) return [];
55
+ return facts.guides
56
+ .map((g) => {
57
+ const haystack = `${g.slug} ${g.title}`.toLowerCase();
58
+ const body = g.body.toLowerCase();
59
+ let points = 0;
60
+ for (const t of terms) {
61
+ if (haystack.includes(t)) points += 5;
62
+ if (g.slug.split("-").includes(t)) points += 3;
63
+ const hits = body.split(t).length - 1;
64
+ points += Math.min(hits, 6);
65
+ }
66
+ return { guide: g, points };
67
+ })
68
+ .filter((r) => r.points > 0)
69
+ .sort((a, b) => b.points - a.points)
70
+ .slice(0, limit)
71
+ .map((r) => ({ path: r.guide.path, title: r.guide.title || r.guide.slug, slug: r.guide.slug, score: r.points }));
72
+ }
73
+
74
+ export function findGuide(facts, name) {
75
+ const needle = name.toLowerCase().replace(/[\s_]+/g, "-");
76
+ const bare = needle.split(".")[0];
77
+ return (
78
+ facts.guides.find((g) => g.slug === needle || g.slug === bare) ||
79
+ facts.guides.find((g) => (g.title || "").toLowerCase() === name.toLowerCase()) ||
80
+ facts.guides.find((g) => g.tags.includes(name) || g.imports.some((i) => i.name === name)) ||
81
+ null
82
+ );
83
+ }
84
+
85
+ export function handleMessage(context, message) {
86
+ const { facts, config } = context;
87
+ const id = message.id;
88
+ const reply = (result) => ({ jsonrpc: "2.0", id, result });
89
+ const fail = (code, msg) => ({ jsonrpc: "2.0", id, error: { code, message: msg } });
90
+ const text = (s) => reply({ content: [{ type: "text", text: s }] });
91
+
92
+ switch (message.method) {
93
+ case "initialize":
94
+ return reply({ protocolVersion: PROTOCOL, capabilities: { tools: {} }, serverInfo: { name: "adsa", version: "0.1.0" } });
95
+ case "notifications/initialized":
96
+ case "notifications/cancelled":
97
+ return null;
98
+ case "ping":
99
+ return reply({});
100
+ case "tools/list":
101
+ return reply({ tools: TOOLS });
102
+ case "tools/call": {
103
+ const args = message.params?.arguments || {};
104
+ switch (message.params?.name) {
105
+ case "list_components": {
106
+ const rows = facts.guides.map((g) => `${g.title || g.slug} — ${g.path}`);
107
+ return text(`${facts.name}${facts.version ? " " + facts.version : ""}\n${rows.length} guides\n\n${rows.join("\n")}`);
108
+ }
109
+ case "get_guide": {
110
+ const guide = findGuide(facts, String(args.name || ""));
111
+ if (!guide) {
112
+ const near = searchGuides(facts, String(args.name || ""), 5).map((r) => r.slug);
113
+ return text(`No guide for "${args.name}".${near.length ? ` Closest: ${near.join(", ")}.` : ""} Use list_components to see everything, and list_gaps before assuming it exists.`);
114
+ }
115
+ const body = args.dense === false ? guide.body : dense(guide.body);
116
+ return text(`${guide.path}\n\n${body}`);
117
+ }
118
+ case "search": {
119
+ const results = searchGuides(facts, String(args.query || ""), Number(args.limit) || 8);
120
+ if (!results.length) return text(`Nothing matched "${args.query}". If this system has no such component, list_gaps says what to use instead — do not invent one.`);
121
+ return text(results.map((r) => `${r.slug} — ${r.title} (${r.path})`).join("\n"));
122
+ }
123
+ case "list_gaps": {
124
+ const file = facts.gaps.file;
125
+ const body = file ? read(join(facts.root, file)) : null;
126
+ if (!body) return text("This repository has no gap list. Nothing states what the system deliberately lacks, so treat anything you cannot find as unknown and ask rather than inventing it.");
127
+ return text(`${file}\n\n${body}`);
128
+ }
129
+ case "readiness_score": {
130
+ const scored = score(facts, config);
131
+ const rows = scored.dimensions.map((d) => `${d.skipped ? "—" : d.score + "/5"} ${d.title}${d.skipped ? " (skipped)" : ""}\n ${(d.evidence || []).join(" ")}`);
132
+ return text(`${facts.name}: ${scored.total}/${scored.max}\n\n${rows.join("\n")}`);
133
+ }
134
+ default:
135
+ return fail(-32601, `Unknown tool "${message.params?.name}"`);
136
+ }
137
+ }
138
+ default:
139
+ return fail(-32601, `Unknown method "${message.method}"`);
140
+ }
141
+ }
142
+
143
+ export async function serve(stdin, stdout, context) {
144
+ let buffer = "";
145
+ stdin.setEncoding("utf8");
146
+ for await (const chunk of stdin) {
147
+ buffer += chunk;
148
+ let index;
149
+ while ((index = buffer.indexOf("\n")) >= 0) {
150
+ const line = buffer.slice(0, index).trim();
151
+ buffer = buffer.slice(index + 1);
152
+ if (!line) continue;
153
+ let response;
154
+ try {
155
+ response = handleMessage(context, JSON.parse(line));
156
+ } catch (error) {
157
+ response = { jsonrpc: "2.0", id: null, error: { code: -32700, message: String(error.message || error) } };
158
+ }
159
+ if (response) stdout.write(JSON.stringify(response) + "\n");
160
+ }
161
+ }
162
+ }