@twobirds/microcomponents 0.10.3 → 0.11.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/dist/MC.min.js +1 -1
- package/dist/autoload.min.js +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/core/MC.d.ts +1 -1
- package/dist/core/MC.d.ts.map +1 -1
- package/dist/core/elements.d.ts.map +1 -1
- package/dist/core/elements.js +12 -5
- package/dist/core/mcGuards.js +6 -1
- package/dist/docs/cli.d.ts +1 -1
- package/dist/elements.min.js +1 -1
- package/dist/formValues.min.js +1 -1
- package/dist/mcGuards.min.js +1 -1
- package/dist/mcp/api.json +3 -3
- package/dist/mcp/docs.d.ts +55 -0
- package/dist/mcp/docs.d.ts.map +1 -0
- package/dist/mcp/index.js +320 -203
- package/dist/mcp/server.d.ts +43 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/tools.d.ts +54 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/microcomponents.js +83 -38
- package/dist/microcomponents.min.js +1 -1
- package/dist/observables.min.js +1 -1
- package/dist/state/formValues.d.ts.map +1 -1
- package/dist/state/formValues.js +59 -31
- package/dist/state/observables.d.ts.map +1 -1
- package/dist/state/observables.js +55 -28
- package/dist/state/placeholders.d.ts +0 -1
- package/dist/state/placeholders.d.ts.map +1 -1
- package/dist/utils/autoload.d.ts.map +1 -1
- package/dist/utils/helpers.d.ts +3 -0
- package/dist/utils/helpers.d.ts.map +1 -1
- package/package.json +4 -2
package/dist/mcp/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
6
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
7
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { z } from "zod";
|
|
8
8
|
//#region \0rolldown/runtime.js
|
|
9
9
|
var __defProp = Object.defineProperty;
|
|
10
10
|
var __esmMin = (fn, res, err) => () => {
|
|
@@ -25,6 +25,301 @@ var __exportAll = (all, no_symbols) => {
|
|
|
25
25
|
return target;
|
|
26
26
|
};
|
|
27
27
|
//#endregion
|
|
28
|
+
//#region src/mcp/docs.ts
|
|
29
|
+
/**
|
|
30
|
+
Documentation data for the MCP server — the single source of truth the tools read.
|
|
31
|
+
|
|
32
|
+
The index is derived, never hand-copied. In a published package the server runs from
|
|
33
|
+
dist/mcp and reads the prebuilt api.json; in development (tsx from src/mcp) the index
|
|
34
|
+
is generated live from src/ at startup, so it cannot drift away from the library it
|
|
35
|
+
documents. The dev/prod decision lives in server.ts; this module is the pure data layer.
|
|
36
|
+
|
|
37
|
+
@module MCP/Docs
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
Reads the prebuilt api.json from a server directory.
|
|
41
|
+
|
|
42
|
+
@param dir - The directory the compiled server sits in; api.json is beside it.
|
|
43
|
+
@returns The parsed index.
|
|
44
|
+
*/
|
|
45
|
+
function loadIndex(dir) {
|
|
46
|
+
return JSON.parse(readFileSync(join(dir, "api.json"), "utf8"));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
Concept essays live one per file: copied into dist/mcp/concepts in production, read
|
|
50
|
+
from docs/concepts in development.
|
|
51
|
+
|
|
52
|
+
@param dir - The concepts folder.
|
|
53
|
+
@returns A record of concept name (filename without `.md`) to essay text. An empty
|
|
54
|
+
record when the folder is absent.
|
|
55
|
+
*/
|
|
56
|
+
function readConcepts(dir) {
|
|
57
|
+
if (!existsSync(dir)) return {};
|
|
58
|
+
const out = {};
|
|
59
|
+
for (const f of readdirSync(dir)) {
|
|
60
|
+
if (!f.endsWith(".md")) continue;
|
|
61
|
+
out[f.replace(/\.md$/, "")] = readFileSync(join(dir, f), "utf8").trim();
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
First paragraph or sentence of a description, truncated honestly.
|
|
67
|
+
|
|
68
|
+
@param text - The description text.
|
|
69
|
+
@param limit - Maximum characters before truncation.
|
|
70
|
+
@returns The summary.
|
|
71
|
+
*/
|
|
72
|
+
function summarize(text, limit = 160) {
|
|
73
|
+
const flat = (text.split(/\n\s*\n|(?<=[.!?])\s+/)[0] ?? text).replace(/\s+/g, " ").trim();
|
|
74
|
+
if (flat.length <= limit) return flat;
|
|
75
|
+
return `${flat.slice(0, limit).replace(/[\s,\-]+$/, "")}...`;
|
|
76
|
+
}
|
|
77
|
+
var FENCE = "`".repeat(3);
|
|
78
|
+
/**
|
|
79
|
+
Scores one index entry against a lowercased query.
|
|
80
|
+
|
|
81
|
+
@param e - The entry to score.
|
|
82
|
+
@param q - The lowercased query.
|
|
83
|
+
@returns The relevance score; 0 when the entry does not match.
|
|
84
|
+
*/
|
|
85
|
+
function scoreEntry(e, q) {
|
|
86
|
+
let score = 0;
|
|
87
|
+
if (e.name.toLowerCase() === q) score += 8;
|
|
88
|
+
if (e.name.toLowerCase().includes(q)) score += 4;
|
|
89
|
+
if ((e.group ?? "").toLowerCase().includes(q)) score += 2;
|
|
90
|
+
if ((e.description ?? "").toLowerCase().includes(q)) score += 3;
|
|
91
|
+
for (const ex of e.examples ?? []) if (ex.code.toLowerCase().includes(q) || ex.title.toLowerCase().includes(q)) score += 2;
|
|
92
|
+
return score;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
Builds the six tools against a given context.
|
|
96
|
+
|
|
97
|
+
@param ctx - The index, the concept essays, and the readable source root.
|
|
98
|
+
@returns A record of tool name to definition, in registration order.
|
|
99
|
+
*/
|
|
100
|
+
function createTools(ctx) {
|
|
101
|
+
const API = ctx.index.entries;
|
|
102
|
+
const undocumented = ctx.index.undocumented;
|
|
103
|
+
const CONCEPT_NAMES = Object.keys(ctx.concepts);
|
|
104
|
+
return {
|
|
105
|
+
"list-components": {
|
|
106
|
+
description: "List all documented Micro Components classes, utilities and types with their group and a short summary",
|
|
107
|
+
schema: {},
|
|
108
|
+
handler: async () => {
|
|
109
|
+
const list = Object.values(API).map((e) => ({
|
|
110
|
+
name: e.name,
|
|
111
|
+
kind: e.kind,
|
|
112
|
+
group: e.group,
|
|
113
|
+
file: e.file,
|
|
114
|
+
summary: summarize(e.description ?? "", 120),
|
|
115
|
+
examples: (e.examples ?? []).length
|
|
116
|
+
}));
|
|
117
|
+
return { content: [{
|
|
118
|
+
type: "text",
|
|
119
|
+
text: JSON.stringify({
|
|
120
|
+
libraryVersion: ctx.index.libraryVersion,
|
|
121
|
+
exportedCount: ctx.index.exportedCount,
|
|
122
|
+
components: list
|
|
123
|
+
}, null, 2)
|
|
124
|
+
}] };
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"get-component-docs": {
|
|
128
|
+
description: "Get comprehensive documentation and usage examples for a Micro Components class or utility",
|
|
129
|
+
schema: { componentName: z.string().describe("The name of the component (e.g., DC, defineCE, observe, injectCSS)") },
|
|
130
|
+
handler: async ({ componentName }) => {
|
|
131
|
+
const entry = API[componentName];
|
|
132
|
+
if (!entry) return {
|
|
133
|
+
content: [{
|
|
134
|
+
type: "text",
|
|
135
|
+
text: `No documentation for "${componentName}". ${Object.keys(API).length} entries exist; call list-components or search-components to discover names.`
|
|
136
|
+
}],
|
|
137
|
+
isError: true
|
|
138
|
+
};
|
|
139
|
+
const lines = [
|
|
140
|
+
`# ${entry.name}`,
|
|
141
|
+
"",
|
|
142
|
+
`**Kind:** ${entry.kind ?? "unknown"}`,
|
|
143
|
+
...entry.group ? [`**Group:** ${entry.group}`] : [],
|
|
144
|
+
...entry.file ? [`**Source:** ${entry.file}`] : [],
|
|
145
|
+
""
|
|
146
|
+
];
|
|
147
|
+
if (entry.description) lines.push("## Description", entry.description, "");
|
|
148
|
+
if (entry.signatures?.length) {
|
|
149
|
+
lines.push("## Signatures");
|
|
150
|
+
for (const sig of entry.signatures) lines.push(`${FENCE}ts`, sig, FENCE, "");
|
|
151
|
+
}
|
|
152
|
+
const examples = entry.examples ?? [];
|
|
153
|
+
if (examples.length) {
|
|
154
|
+
lines.push("## Examples");
|
|
155
|
+
for (const ex of examples) {
|
|
156
|
+
if (ex.title) lines.push(`### ${ex.title}`);
|
|
157
|
+
lines.push(`${FENCE}ts`, ex.code, FENCE, "");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { content: [{
|
|
161
|
+
type: "text",
|
|
162
|
+
text: lines.join("\n")
|
|
163
|
+
}] };
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
"search-components": {
|
|
167
|
+
description: "Search Micro Components documentation by keyword. Searches names, groups, descriptions and code examples.",
|
|
168
|
+
schema: { query: z.string().describe("Search term to find relevant components (e.g., \"event\", \"observable\", \"css\")") },
|
|
169
|
+
handler: async ({ query }) => {
|
|
170
|
+
const q = query.toLowerCase();
|
|
171
|
+
const hits = Object.values(API).map((e) => ({
|
|
172
|
+
e,
|
|
173
|
+
s: scoreEntry(e, q)
|
|
174
|
+
})).filter((h) => h.s > 0).sort((a, b) => b.s - a.s || a.e.name.localeCompare(b.e.name)).slice(0, 12);
|
|
175
|
+
const conceptHits = CONCEPT_NAMES.filter((k) => ctx.concepts[k].toLowerCase().includes(q));
|
|
176
|
+
if (!hits.length && !conceptHits.length) return { content: [{
|
|
177
|
+
type: "text",
|
|
178
|
+
text: `No documentation matched "${query}". Use list-components to see every entry, or get-source-file for raw source.`
|
|
179
|
+
}] };
|
|
180
|
+
const results = hits.map((h) => ({
|
|
181
|
+
name: h.e.name,
|
|
182
|
+
kind: h.e.kind,
|
|
183
|
+
group: h.e.group,
|
|
184
|
+
relevance: h.s,
|
|
185
|
+
snippet: summarize(h.e.description ?? "", 150)
|
|
186
|
+
}));
|
|
187
|
+
return { content: [{
|
|
188
|
+
type: "text",
|
|
189
|
+
text: JSON.stringify({
|
|
190
|
+
query,
|
|
191
|
+
results,
|
|
192
|
+
conceptMatches: conceptHits
|
|
193
|
+
}, null, 2)
|
|
194
|
+
}] };
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
"get-core-concept": {
|
|
198
|
+
description: `Get a core Micro Components concept essay. Available: ${CONCEPT_NAMES.join(", ") || "none"}`,
|
|
199
|
+
schema: { concept: z.string().describe(`Concept name, one of: ${CONCEPT_NAMES.join(", ")}`) },
|
|
200
|
+
handler: async ({ concept }) => {
|
|
201
|
+
const content = ctx.concepts[concept];
|
|
202
|
+
if (!content) return {
|
|
203
|
+
content: [{
|
|
204
|
+
type: "text",
|
|
205
|
+
text: `Concept "${concept}" not found. Available: ${CONCEPT_NAMES.join(", ")}`
|
|
206
|
+
}],
|
|
207
|
+
isError: true
|
|
208
|
+
};
|
|
209
|
+
return { content: [{
|
|
210
|
+
type: "text",
|
|
211
|
+
text: content
|
|
212
|
+
}] };
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
"get-source-file": {
|
|
216
|
+
description: "Retrieve the actual source code from a Micro Components library file (package-root relative)",
|
|
217
|
+
schema: { filepath: z.string().describe("Path to source file, e.g. dist/core/MC.js, dist/core/MC.d.ts, dist/mcp/concepts/event-system.md") },
|
|
218
|
+
handler: async ({ filepath }) => {
|
|
219
|
+
const full = resolve(ctx.sourceRoot, filepath);
|
|
220
|
+
if (full !== ctx.sourceRoot && !full.startsWith(ctx.sourceRoot + "/")) return {
|
|
221
|
+
content: [{
|
|
222
|
+
type: "text",
|
|
223
|
+
text: `Refused: "${filepath}" is outside the package sources.`
|
|
224
|
+
}],
|
|
225
|
+
isError: true
|
|
226
|
+
};
|
|
227
|
+
if (!existsSync(full)) return {
|
|
228
|
+
content: [{
|
|
229
|
+
type: "text",
|
|
230
|
+
text: `File not found: ${filepath}`
|
|
231
|
+
}],
|
|
232
|
+
isError: true
|
|
233
|
+
};
|
|
234
|
+
try {
|
|
235
|
+
const source = readFileSync(full, "utf8");
|
|
236
|
+
if (source.length > 2e5) return {
|
|
237
|
+
content: [{
|
|
238
|
+
type: "text",
|
|
239
|
+
text: `File too large (${source.length} bytes). Request a specific symbol via get-component-docs instead.`
|
|
240
|
+
}],
|
|
241
|
+
isError: true
|
|
242
|
+
};
|
|
243
|
+
return { content: [{
|
|
244
|
+
type: "text",
|
|
245
|
+
text: source
|
|
246
|
+
}] };
|
|
247
|
+
} catch (error) {
|
|
248
|
+
return {
|
|
249
|
+
content: [{
|
|
250
|
+
type: "text",
|
|
251
|
+
text: `Error reading file: ${error}`
|
|
252
|
+
}],
|
|
253
|
+
isError: true
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
"list-undocumented": {
|
|
259
|
+
description: "Report public exports whose source lacks a doc block — the drift signal for src/ documentation",
|
|
260
|
+
schema: {},
|
|
261
|
+
handler: async () => ({ content: [{
|
|
262
|
+
type: "text",
|
|
263
|
+
text: undocumented.length ? `${undocumented.length} undocumented export(s):\n${undocumented.join("\n")}` : `All ${ctx.index.exportedCount} public exports carry a doc block.`
|
|
264
|
+
}] })
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region src/mcp/server.ts
|
|
270
|
+
/**
|
|
271
|
+
Wiring for the MCP server: read the documentation environment, and build the McpServer.
|
|
272
|
+
|
|
273
|
+
The tool behaviour lives in tools.ts and the data layer in docs.ts, both transport-free and
|
|
274
|
+
unit-tested. This module is the seam between them: loadEnv() makes the dev/prod decision and
|
|
275
|
+
reads the index and the concept essays, createServer() registers the tools on a server without
|
|
276
|
+
connecting a transport. Both are testable. Connecting stdio is the entry point's job
|
|
277
|
+
(index.ts), excluded from coverage as the process entry — running it in a test would take over
|
|
278
|
+
the test process's real stdio, which is not available in a worker thread.
|
|
279
|
+
|
|
280
|
+
@module MCP/Server
|
|
281
|
+
*/
|
|
282
|
+
/**
|
|
283
|
+
Reads the documentation environment for the directory the compiled server sits in.
|
|
284
|
+
|
|
285
|
+
Production: the server runs from dist/mcp, so it reads the prebuilt api.json and the copied
|
|
286
|
+
concepts/ that the build step emits next to it. Development: the server runs from src/mcp,
|
|
287
|
+
where index.ts is present, so the index is generated live from src/ (it cannot drift away
|
|
288
|
+
from the library it documents) and concepts are read straight from docs/concepts.
|
|
289
|
+
|
|
290
|
+
@param dir - The directory the server module lives in.
|
|
291
|
+
@param devLoader - Supplies the live-generated index in development. The entry point passes
|
|
292
|
+
the real doc generator; tests pass a fixture, so the dev branch is pinned without running
|
|
293
|
+
the generator as a side effect on dist/.
|
|
294
|
+
@returns The index, the concept essays, and the folder get-source-file is confined to.
|
|
295
|
+
*/
|
|
296
|
+
async function loadEnv(dir, devLoader) {
|
|
297
|
+
const IS_DEV = existsSync(join(dir, "index.ts"));
|
|
298
|
+
const conceptsDir = IS_DEV ? join(dir, "..", "..", "docs", "concepts") : join(dir, "concepts");
|
|
299
|
+
return {
|
|
300
|
+
index: IS_DEV ? await devLoader() : loadIndex(dir),
|
|
301
|
+
concepts: readConcepts(conceptsDir),
|
|
302
|
+
sourceRoot: join(dir, "..", "..")
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
Registers every tool on a fresh McpServer, without connecting a transport.
|
|
307
|
+
|
|
308
|
+
@param env - The index, the concept essays, and the readable source root.
|
|
309
|
+
@returns The server, ready to connect.
|
|
310
|
+
*/
|
|
311
|
+
function createServer(env) {
|
|
312
|
+
const server = new McpServer({
|
|
313
|
+
name: "microcomponents-mcp",
|
|
314
|
+
version: env.index.libraryVersion
|
|
315
|
+
});
|
|
316
|
+
for (const [name, def] of Object.entries(createTools(env))) server.registerTool(name, {
|
|
317
|
+
description: def.description,
|
|
318
|
+
inputSchema: def.schema
|
|
319
|
+
}, (args) => def.handler(args));
|
|
320
|
+
return server;
|
|
321
|
+
}
|
|
322
|
+
//#endregion
|
|
28
323
|
//#region mcp-docs-generate.ts
|
|
29
324
|
/**
|
|
30
325
|
Generates the documentation index for the MCP server directly from src/.
|
|
@@ -45,9 +340,9 @@ above the first overload supplies the prose.
|
|
|
45
340
|
*/
|
|
46
341
|
var mcp_docs_generate_exports = /* @__PURE__ */ __exportAll({
|
|
47
342
|
extract: () => extract,
|
|
48
|
-
index: () => index
|
|
343
|
+
index: () => index,
|
|
49
344
|
scanFile: () => scanFile,
|
|
50
|
-
undocumented: () => undocumented
|
|
345
|
+
undocumented: () => undocumented,
|
|
51
346
|
walk: () => walk
|
|
52
347
|
});
|
|
53
348
|
/** Read a barrel file, returning each re-exported name with its group and hidden flag. */
|
|
@@ -209,7 +504,7 @@ function pickDoc(name) {
|
|
|
209
504
|
}
|
|
210
505
|
return best ?? { decl: candidates[0] };
|
|
211
506
|
}
|
|
212
|
-
var ROOT, SRC, OUTFILE, MAX_DOC_GAP_LINES, DECL_PATTERNS, publicEntries, scanned, byName, pkg, entries, undocumented
|
|
507
|
+
var ROOT, SRC, OUTFILE, MAX_DOC_GAP_LINES, DECL_PATTERNS, publicEntries, scanned, byName, pkg, entries, undocumented, publicCount, index;
|
|
213
508
|
var init_mcp_docs_generate = __esmMin((() => {
|
|
214
509
|
ROOT = dirname(fileURLToPath(import.meta.url));
|
|
215
510
|
SRC = join(ROOT, "src");
|
|
@@ -240,12 +535,12 @@ var init_mcp_docs_generate = __esmMin((() => {
|
|
|
240
535
|
for (const scan of Object.values(scanned)) for (const d of scan.decls) (byName[d.name] ??= []).push(d);
|
|
241
536
|
pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
|
|
242
537
|
entries = {};
|
|
243
|
-
undocumented
|
|
538
|
+
undocumented = [];
|
|
244
539
|
for (const pub of publicEntries) {
|
|
245
540
|
if (pub.hidden) continue;
|
|
246
541
|
const found = pickDoc(pub.name);
|
|
247
542
|
if (!found) {
|
|
248
|
-
undocumented
|
|
543
|
+
undocumented.push(`${pub.kind} ${pub.name}`);
|
|
249
544
|
continue;
|
|
250
545
|
}
|
|
251
546
|
const sigs = byName[pub.name].map((d) => d.sig).filter(Boolean);
|
|
@@ -262,20 +557,20 @@ var init_mcp_docs_generate = __esmMin((() => {
|
|
|
262
557
|
description: doc.description || void 0,
|
|
263
558
|
examples: doc.examples.length ? doc.examples : void 0
|
|
264
559
|
};
|
|
265
|
-
if (!doc.description) undocumented
|
|
560
|
+
if (!doc.description) undocumented.push(`${pub.kind} ${pub.name}`);
|
|
266
561
|
}
|
|
267
562
|
publicCount = publicEntries.filter((p) => !p.hidden).length;
|
|
268
|
-
index
|
|
563
|
+
index = {
|
|
269
564
|
libraryVersion: pkg.version,
|
|
270
565
|
exportedCount: publicCount,
|
|
271
566
|
entries,
|
|
272
|
-
undocumented
|
|
567
|
+
undocumented
|
|
273
568
|
};
|
|
274
569
|
if (Boolean(process.argv[1]) && import.meta.url === `file://${resolve(process.argv[1])}`) {
|
|
275
|
-
writeFileSync(OUTFILE, JSON.stringify(index
|
|
570
|
+
writeFileSync(OUTFILE, JSON.stringify(index, null, 2));
|
|
276
571
|
console.log(`${OUTFILE}: ${Object.keys(entries).length} of ${publicCount} public exports documented`);
|
|
277
|
-
if (undocumented
|
|
278
|
-
console.warn(`missing doc block (${undocumented
|
|
572
|
+
if (undocumented.length) {
|
|
573
|
+
console.warn(`missing doc block (${undocumented.length}): ${undocumented.join(", ")}`);
|
|
279
574
|
process.exit(1);
|
|
280
575
|
}
|
|
281
576
|
}
|
|
@@ -293,199 +588,21 @@ step emits next to it. In development (tsx from src/mcp) the index is generated
|
|
|
293
588
|
from src/ at startup, so it cannot drift away from the library it documents.
|
|
294
589
|
|
|
295
590
|
The server is Node-only and is emitted by tsconfig.mcp.json; package.json "bin" points
|
|
296
|
-
at the compiled dist/mcp/index.js.
|
|
591
|
+
at the compiled dist/mcp/index.js. The tool layer (tools.ts), the data layer (docs.ts)
|
|
592
|
+
and the wiring (server.ts) are transport-free and unit-tested; this file is the process
|
|
593
|
+
entry that supplies the live generator, connects stdio, and reports readiness. It is
|
|
594
|
+
excluded from coverage — running it in a test would take over the test process's real
|
|
595
|
+
stdio, which a worker thread does not have.
|
|
297
596
|
|
|
298
597
|
@example Run the published server: node dist/mcp/index.js
|
|
299
598
|
@example Run from source during development: pnpm run mcp:dev
|
|
300
599
|
@example Regenerate the shipped index only: pnpm run mcp:docs
|
|
301
600
|
*/
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
var IS_DEV = existsSync(join(__dirname, "index.ts"));
|
|
306
|
-
var index;
|
|
307
|
-
var undocumented;
|
|
308
|
-
if (IS_DEV) {
|
|
309
|
-
const live = await Promise.resolve().then(() => (init_mcp_docs_generate(), mcp_docs_generate_exports));
|
|
310
|
-
index = live.index;
|
|
311
|
-
undocumented = live.undocumented;
|
|
312
|
-
} else {
|
|
313
|
-
const data = JSON.parse(readFileSync(join(__dirname, "api.json"), "utf8"));
|
|
314
|
-
index = data;
|
|
315
|
-
undocumented = data.undocumented;
|
|
316
|
-
}
|
|
317
|
-
var API = index.entries;
|
|
318
|
-
/** Concept essays live one per file: copied into dist/mcp/concepts in production, read from docs/concepts in development. */
|
|
319
|
-
var CONCEPTS_DIR = IS_DEV ? join(__dirname, "..", "..", "docs", "concepts") : join(__dirname, "concepts");
|
|
320
|
-
function readConcepts() {
|
|
321
|
-
if (!existsSync(CONCEPTS_DIR)) return {};
|
|
322
|
-
const out = {};
|
|
323
|
-
for (const f of readdirSync(CONCEPTS_DIR)) {
|
|
324
|
-
if (!f.endsWith(".md")) continue;
|
|
325
|
-
out[f.replace(/\.md$/, "")] = readFileSync(join(CONCEPTS_DIR, f), "utf8").trim();
|
|
326
|
-
}
|
|
327
|
-
return out;
|
|
601
|
+
/** Development: tsx runs src/mcp/index.ts, so the index is generated live from src/. */
|
|
602
|
+
async function generateIndex() {
|
|
603
|
+
return (await Promise.resolve().then(() => (init_mcp_docs_generate(), mcp_docs_generate_exports))).index;
|
|
328
604
|
}
|
|
329
|
-
var
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
/** First paragraph or sentence of a description, truncated honestly. */
|
|
333
|
-
function summarize(text, limit = 160) {
|
|
334
|
-
const flat = (text.split(/\n\s*\n|(?<=[.!?])\s+/)[0] ?? text).replace(/\s+/g, " ").trim();
|
|
335
|
-
if (flat.length <= limit) return flat;
|
|
336
|
-
return `${flat.slice(0, limit).replace(/[\s,\-]+$/, "")}...`;
|
|
337
|
-
}
|
|
338
|
-
var server = new McpServer({
|
|
339
|
-
name: "microcomponents-mcp",
|
|
340
|
-
version: index.libraryVersion
|
|
341
|
-
});
|
|
342
|
-
server.tool("list-components", "List all documented Micro Components classes, utilities and types with their group and a short summary", {}, async () => {
|
|
343
|
-
const list = Object.values(API).map((e) => ({
|
|
344
|
-
name: e.name,
|
|
345
|
-
kind: e.kind,
|
|
346
|
-
group: e.group,
|
|
347
|
-
file: e.file,
|
|
348
|
-
summary: summarize(e.description ?? "", 120),
|
|
349
|
-
examples: (e.examples ?? []).length
|
|
350
|
-
}));
|
|
351
|
-
return { content: [{
|
|
352
|
-
type: "text",
|
|
353
|
-
text: JSON.stringify({
|
|
354
|
-
libraryVersion: index.libraryVersion,
|
|
355
|
-
exportedCount: index.exportedCount,
|
|
356
|
-
components: list
|
|
357
|
-
}, null, 2)
|
|
358
|
-
}] };
|
|
359
|
-
});
|
|
360
|
-
server.tool("get-component-docs", "Get comprehensive documentation and usage examples for a Micro Components class or utility", { componentName: z.string().describe("The name of the component (e.g., DC, defineCE, observe, injectCSS)") }, async ({ componentName }) => {
|
|
361
|
-
const entry = API[componentName];
|
|
362
|
-
if (!entry) return {
|
|
363
|
-
content: [{
|
|
364
|
-
type: "text",
|
|
365
|
-
text: `No documentation for "${componentName}". ${Object.keys(API).length} entries exist; call list-components or search-components to discover names.`
|
|
366
|
-
}],
|
|
367
|
-
isError: true
|
|
368
|
-
};
|
|
369
|
-
const lines = [
|
|
370
|
-
`# ${entry.name}`,
|
|
371
|
-
"",
|
|
372
|
-
`**Kind:** ${entry.kind ?? "unknown"}`,
|
|
373
|
-
...entry.group ? [`**Group:** ${entry.group}`] : [],
|
|
374
|
-
...entry.file ? [`**Source:** ${entry.file}`] : [],
|
|
375
|
-
""
|
|
376
|
-
];
|
|
377
|
-
if (entry.description) lines.push("## Description", entry.description, "");
|
|
378
|
-
if (entry.signatures?.length) {
|
|
379
|
-
lines.push("## Signatures");
|
|
380
|
-
for (const sig of entry.signatures) lines.push(`${FENCE}ts`, sig, FENCE, "");
|
|
381
|
-
}
|
|
382
|
-
const examples = entry.examples ?? [];
|
|
383
|
-
if (examples.length) {
|
|
384
|
-
lines.push("## Examples");
|
|
385
|
-
for (const ex of examples) {
|
|
386
|
-
if (ex.title) lines.push(`### ${ex.title}`);
|
|
387
|
-
lines.push(`${FENCE}ts`, ex.code, FENCE, "");
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
return { content: [{
|
|
391
|
-
type: "text",
|
|
392
|
-
text: lines.join("\n")
|
|
393
|
-
}] };
|
|
394
|
-
});
|
|
395
|
-
server.tool("search-components", "Search Micro Components documentation by keyword. Searches names, groups, descriptions and code examples.", { query: z.string().describe("Search term to find relevant components (e.g., \"event\", \"observable\", \"css\")") }, async ({ query }) => {
|
|
396
|
-
const q = query.toLowerCase();
|
|
397
|
-
const scoreEntry = (e) => {
|
|
398
|
-
let score = 0;
|
|
399
|
-
if (e.name.toLowerCase() === q) score += 8;
|
|
400
|
-
if (e.name.toLowerCase().includes(q)) score += 4;
|
|
401
|
-
if ((e.group ?? "").toLowerCase().includes(q)) score += 2;
|
|
402
|
-
if ((e.description ?? "").toLowerCase().includes(q)) score += 3;
|
|
403
|
-
for (const ex of e.examples ?? []) if (ex.code.toLowerCase().includes(q) || ex.title.toLowerCase().includes(q)) score += 2;
|
|
404
|
-
return score;
|
|
405
|
-
};
|
|
406
|
-
const hits = Object.values(API).map((e) => ({
|
|
407
|
-
e,
|
|
408
|
-
s: scoreEntry(e)
|
|
409
|
-
})).filter((h) => h.s > 0).sort((a, b) => b.s - a.s || a.e.name.localeCompare(b.e.name)).slice(0, 12);
|
|
410
|
-
const conceptHits = CONCEPT_NAMES.filter((k) => CONCEPTS[k].toLowerCase().includes(q));
|
|
411
|
-
if (!hits.length && !conceptHits.length) return { content: [{
|
|
412
|
-
type: "text",
|
|
413
|
-
text: `No documentation matched "${query}". Use list-components to see every entry, or get-source-file for raw source.`
|
|
414
|
-
}] };
|
|
415
|
-
const results = hits.map((h) => ({
|
|
416
|
-
name: h.e.name,
|
|
417
|
-
kind: h.e.kind,
|
|
418
|
-
group: h.e.group,
|
|
419
|
-
relevance: h.s,
|
|
420
|
-
snippet: summarize(h.e.description ?? "", 150)
|
|
421
|
-
}));
|
|
422
|
-
return { content: [{
|
|
423
|
-
type: "text",
|
|
424
|
-
text: JSON.stringify({
|
|
425
|
-
query,
|
|
426
|
-
results,
|
|
427
|
-
conceptMatches: conceptHits
|
|
428
|
-
}, null, 2)
|
|
429
|
-
}] };
|
|
430
|
-
});
|
|
431
|
-
server.tool("get-core-concept", `Get a core Micro Components concept essay. Available: ${CONCEPT_NAMES.join(", ") || "none"}`, { concept: z.string().describe(`Concept name, one of: ${CONCEPT_NAMES.join(", ")}`) }, async ({ concept }) => {
|
|
432
|
-
const content = CONCEPTS[concept];
|
|
433
|
-
if (!content) return {
|
|
434
|
-
content: [{
|
|
435
|
-
type: "text",
|
|
436
|
-
text: `Concept "${concept}" not found. Available: ${CONCEPT_NAMES.join(", ")}`
|
|
437
|
-
}],
|
|
438
|
-
isError: true
|
|
439
|
-
};
|
|
440
|
-
return { content: [{
|
|
441
|
-
type: "text",
|
|
442
|
-
text: content
|
|
443
|
-
}] };
|
|
444
|
-
});
|
|
445
|
-
server.tool("get-source-file", "Retrieve the actual source code from a Micro Components library file (package-root relative)", { filepath: z.string().describe("Path to source file, e.g. dist/core/MC.js, dist/core/MC.d.ts, dist/mcp/concepts/event-system.md") }, async ({ filepath }) => {
|
|
446
|
-
const full = resolve(SOURCE_ROOT, filepath);
|
|
447
|
-
if (full !== SOURCE_ROOT && !full.startsWith(SOURCE_ROOT + "/")) return {
|
|
448
|
-
content: [{
|
|
449
|
-
type: "text",
|
|
450
|
-
text: `Refused: "${filepath}" is outside the package sources.`
|
|
451
|
-
}],
|
|
452
|
-
isError: true
|
|
453
|
-
};
|
|
454
|
-
if (!existsSync(full)) return {
|
|
455
|
-
content: [{
|
|
456
|
-
type: "text",
|
|
457
|
-
text: `File not found: ${filepath}`
|
|
458
|
-
}],
|
|
459
|
-
isError: true
|
|
460
|
-
};
|
|
461
|
-
try {
|
|
462
|
-
const source = readFileSync(full, "utf8");
|
|
463
|
-
if (source.length > MAX_SOURCE_BYTES) return {
|
|
464
|
-
content: [{
|
|
465
|
-
type: "text",
|
|
466
|
-
text: `File too large (${source.length} bytes). Request a specific symbol via get-component-docs instead.`
|
|
467
|
-
}],
|
|
468
|
-
isError: true
|
|
469
|
-
};
|
|
470
|
-
return { content: [{
|
|
471
|
-
type: "text",
|
|
472
|
-
text: source
|
|
473
|
-
}] };
|
|
474
|
-
} catch (error) {
|
|
475
|
-
return {
|
|
476
|
-
content: [{
|
|
477
|
-
type: "text",
|
|
478
|
-
text: `Error reading file: ${error}`
|
|
479
|
-
}],
|
|
480
|
-
isError: true
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
});
|
|
484
|
-
server.tool("list-undocumented", "Report public exports whose source lacks a doc block — the drift signal for src/ documentation", {}, async () => ({ content: [{
|
|
485
|
-
type: "text",
|
|
486
|
-
text: undocumented.length ? `${undocumented.length} undocumented export(s):\n${undocumented.join("\n")}` : `All ${index.exportedCount} public exports carry a doc block.`
|
|
487
|
-
}] }));
|
|
488
|
-
var transport = new StdioServerTransport();
|
|
489
|
-
await server.connect(transport);
|
|
490
|
-
console.error(`Micro Components MCP server running on stdio (library ${index.libraryVersion}, ${Object.keys(API).length} documented exports)`);
|
|
605
|
+
var env = await loadEnv(dirname(fileURLToPath(import.meta.url)), generateIndex);
|
|
606
|
+
await createServer(env).connect(new StdioServerTransport());
|
|
607
|
+
console.error(`Micro Components MCP server running on stdio (library ${env.index.libraryVersion}, ${Object.keys(env.index.entries).length} documented exports)`);
|
|
491
608
|
//#endregion
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Wiring for the MCP server: read the documentation environment, and build the McpServer.
|
|
3
|
+
|
|
4
|
+
The tool behaviour lives in tools.ts and the data layer in docs.ts, both transport-free and
|
|
5
|
+
unit-tested. This module is the seam between them: loadEnv() makes the dev/prod decision and
|
|
6
|
+
reads the index and the concept essays, createServer() registers the tools on a server without
|
|
7
|
+
connecting a transport. Both are testable. Connecting stdio is the entry point's job
|
|
8
|
+
(index.ts), excluded from coverage as the process entry — running it in a test would take over
|
|
9
|
+
the test process's real stdio, which is not available in a worker thread.
|
|
10
|
+
|
|
11
|
+
@module MCP/Server
|
|
12
|
+
*/
|
|
13
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
14
|
+
import { type IndexFile } from './docs.js';
|
|
15
|
+
/** Everything the tools read: the index, the concept essays, and the readable source root. */
|
|
16
|
+
export interface ServerEnv {
|
|
17
|
+
index: IndexFile;
|
|
18
|
+
concepts: Record<string, string>;
|
|
19
|
+
sourceRoot: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
Reads the documentation environment for the directory the compiled server sits in.
|
|
23
|
+
|
|
24
|
+
Production: the server runs from dist/mcp, so it reads the prebuilt api.json and the copied
|
|
25
|
+
concepts/ that the build step emits next to it. Development: the server runs from src/mcp,
|
|
26
|
+
where index.ts is present, so the index is generated live from src/ (it cannot drift away
|
|
27
|
+
from the library it documents) and concepts are read straight from docs/concepts.
|
|
28
|
+
|
|
29
|
+
@param dir - The directory the server module lives in.
|
|
30
|
+
@param devLoader - Supplies the live-generated index in development. The entry point passes
|
|
31
|
+
the real doc generator; tests pass a fixture, so the dev branch is pinned without running
|
|
32
|
+
the generator as a side effect on dist/.
|
|
33
|
+
@returns The index, the concept essays, and the folder get-source-file is confined to.
|
|
34
|
+
*/
|
|
35
|
+
export declare function loadEnv(dir: string, devLoader: () => Promise<IndexFile>): Promise<ServerEnv>;
|
|
36
|
+
/**
|
|
37
|
+
Registers every tool on a fresh McpServer, without connecting a transport.
|
|
38
|
+
|
|
39
|
+
@param env - The index, the concept essays, and the readable source root.
|
|
40
|
+
@returns The server, ready to connect.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createServer(env: ServerEnv): McpServer;
|
|
43
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;EAWE;AACF,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGpE,OAAO,EAA2B,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAGpE,8FAA8F;AAC9F,MAAM,WAAW,SAAS;IACzB,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,UAAU,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;EAaE;AACF,wBAAsB,OAAO,CAC5B,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,GACjC,OAAO,CAAC,SAAS,CAAC,CAWpB;AAED;;;;;EAKE;AACF,wBAAgB,YAAY,CAAC,GAAG,EAAE,SAAS,GAAG,SAAS,CAatD"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
The MCP tool layer — six tools over the derived documentation index.
|
|
3
|
+
|
|
4
|
+
Every tool is a plain handler over the index, the concept essays and a source root, so
|
|
5
|
+
the behaviour (including the path-traversal guard of get-source-file) is testable
|
|
6
|
+
without a transport. server.ts wires these onto an McpServer; this module has no
|
|
7
|
+
transport, no console side effects, and no module-level state.
|
|
8
|
+
|
|
9
|
+
@module MCP/Tools
|
|
10
|
+
*/
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { type Entry, type IndexFile } from './docs.js';
|
|
13
|
+
/** Files larger than this are refused by get-source-file. */
|
|
14
|
+
export declare const MAX_SOURCE_BYTES = 200000;
|
|
15
|
+
/**
|
|
16
|
+
The text payload a tool returns. Shaped to satisfy the SDK's ToolCallback return type,
|
|
17
|
+
which requires an index signature — a plain interface does not have one.
|
|
18
|
+
*/
|
|
19
|
+
export type ToolResult = {
|
|
20
|
+
content: Array<{
|
|
21
|
+
type: 'text';
|
|
22
|
+
text: string;
|
|
23
|
+
}>;
|
|
24
|
+
isError?: boolean;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
};
|
|
27
|
+
/** One tool: what it says, what it accepts, and what it does. */
|
|
28
|
+
export interface ToolDef {
|
|
29
|
+
description: string;
|
|
30
|
+
schema: Record<string, z.ZodTypeAny>;
|
|
31
|
+
handler: (args: Record<string, string>) => Promise<ToolResult>;
|
|
32
|
+
}
|
|
33
|
+
/** Everything the tools read: the index, the essays, and the readable source root. */
|
|
34
|
+
export interface ToolContext {
|
|
35
|
+
index: IndexFile;
|
|
36
|
+
concepts: Record<string, string>;
|
|
37
|
+
sourceRoot: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
Scores one index entry against a lowercased query.
|
|
41
|
+
|
|
42
|
+
@param e - The entry to score.
|
|
43
|
+
@param q - The lowercased query.
|
|
44
|
+
@returns The relevance score; 0 when the entry does not match.
|
|
45
|
+
*/
|
|
46
|
+
export declare function scoreEntry(e: Entry, q: string): number;
|
|
47
|
+
/**
|
|
48
|
+
Builds the six tools against a given context.
|
|
49
|
+
|
|
50
|
+
@param ctx - The index, the concept essays, and the readable source root.
|
|
51
|
+
@returns A record of tool name to definition, in registration order.
|
|
52
|
+
*/
|
|
53
|
+
export declare function createTools(ctx: ToolContext): Record<string, ToolDef>;
|
|
54
|
+
//# sourceMappingURL=tools.d.ts.map
|