@twobirds/microcomponents 0.9.0 → 0.10.1
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/README.md +2 -16
- package/dist/cli/bundle.d.ts +106 -0
- package/dist/cli/bundle.d.ts.map +1 -0
- package/dist/cli/bundler.d.ts +44 -0
- package/dist/cli/bundler.d.ts.map +1 -0
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/dev.d.ts +87 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dispatch.d.ts +6 -2
- package/dist/cli/dispatch.d.ts.map +1 -1
- package/dist/cli/index.js +1807 -1
- package/dist/cli/inliner.d.ts +42 -0
- package/dist/cli/inliner.d.ts.map +1 -0
- package/dist/cli/preview.d.ts +48 -0
- package/dist/cli/preview.d.ts.map +1 -0
- package/dist/core/elements.js +1294 -75
- package/dist/core/mcGuards.js +5 -8
- package/dist/mcp/api.json +2 -17
- package/dist/mcp/concepts/project-layout.md +9 -8
- package/dist/mcp/index.js +450 -230
- package/dist/microcomponents.js +2882 -9
- package/dist/state/formValues.js +2236 -281
- package/dist/state/observables.js +1794 -342
- package/dist/utils/events.js +171 -197
- package/package.json +7 -8
- package/dist/cli/create.js +0 -653
- package/dist/cli/dispatch.js +0 -316
- package/dist/cli/scaffold.js +0 -109
- package/dist/cli/writeScaffold.js +0 -114
- package/dist/core/MC.js +0 -837
- package/dist/core/index.js +0 -72
- package/dist/state/index.js +0 -5
- package/dist/state/placeholders.js +0 -187
- package/dist/types/index.js +0 -1
- package/dist/types/mcTypes.js +0 -9
- package/dist/utils/autoload.js +0 -160
- package/dist/utils/helpers.js +0 -532
- package/dist/utils/index.js +0 -12
- package/dist/utils/resolveComponentPath.js +0 -39
- package/dist/vite/index.d.ts +0 -7
- package/dist/vite/index.d.ts.map +0 -1
- package/dist/vite/index.js +0 -6
- package/dist/vite/loadAssets.d.ts +0 -36
- package/dist/vite/loadAssets.d.ts.map +0 -1
- package/dist/vite/loadAssets.js +0 -221
package/dist/mcp/index.js
CHANGED
|
@@ -1,4 +1,280 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
//#region \0rolldown/runtime.js
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __esmMin = (fn, res, err) => () => {
|
|
11
|
+
if (err) throw err[0];
|
|
12
|
+
try {
|
|
13
|
+
return fn && (res = fn(fn = 0)), res;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
throw err = [e], e;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var __exportAll = (all, no_symbols) => {
|
|
19
|
+
let target = {};
|
|
20
|
+
for (var name in all) __defProp(target, name, {
|
|
21
|
+
get: all[name],
|
|
22
|
+
enumerable: true
|
|
23
|
+
});
|
|
24
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
25
|
+
return target;
|
|
26
|
+
};
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region mcp-docs-generate.ts
|
|
29
|
+
/**
|
|
30
|
+
Generates the documentation index for the MCP server directly from src/.
|
|
31
|
+
|
|
32
|
+
The public API surface is defined by the barrel files (one per src subdirectory,
|
|
33
|
+
named index.ts): every name re-exported there is public, except those preceded by
|
|
34
|
+
a block comment containing @hidden. For each public name the generator locates its
|
|
35
|
+
declaration in the module where it is actually defined and attaches the nearest
|
|
36
|
+
preceding doc block from that same file. Association is proximity-based rather than
|
|
37
|
+
strict AST attachment because some exports carry a private statement between their
|
|
38
|
+
doc block and the export keyword (injectCSS declares a WeakMap cache first).
|
|
39
|
+
|
|
40
|
+
Overloaded functions contribute every signature to the index, while the doc block
|
|
41
|
+
above the first overload supplies the prose.
|
|
42
|
+
|
|
43
|
+
@example Regenerate once: pnpm run mcp:docs
|
|
44
|
+
@example Keep it fresh: pnpm run mcp:dev restarts the server process on change
|
|
45
|
+
*/
|
|
46
|
+
var mcp_docs_generate_exports = /* @__PURE__ */ __exportAll({
|
|
47
|
+
index: () => index$1,
|
|
48
|
+
undocumented: () => undocumented$1
|
|
49
|
+
});
|
|
50
|
+
/** Read a barrel file, returning each re-exported name with its group and hidden flag. */
|
|
51
|
+
function readBarrel(file) {
|
|
52
|
+
const lines = readFileSync(file, "utf8").split("\n");
|
|
53
|
+
for (let i = 0; i < lines.length; i++) {
|
|
54
|
+
if (!/^\s*export\s+(type\s+)?\{/.test(lines[i])) continue;
|
|
55
|
+
let k = i;
|
|
56
|
+
while (k + 1 < lines.length && !/}\s*from/.test(lines[k])) {
|
|
57
|
+
k++;
|
|
58
|
+
lines[i] += " " + lines[k].trim();
|
|
59
|
+
lines[k] = "";
|
|
60
|
+
}
|
|
61
|
+
i = k;
|
|
62
|
+
}
|
|
63
|
+
const out = [];
|
|
64
|
+
let pending = "";
|
|
65
|
+
for (let i = 0; i < lines.length; i++) {
|
|
66
|
+
const line = lines[i].trim();
|
|
67
|
+
if (line.startsWith("/**") && !pending) {
|
|
68
|
+
let k = i;
|
|
69
|
+
while (k < lines.length && !lines[k].includes("*/")) k++;
|
|
70
|
+
pending = lines.slice(i, k + 1).join("\n");
|
|
71
|
+
i = k;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const m = /^export\s+(type\s+)?\{([^}]*)\}\s*from/.exec(line);
|
|
75
|
+
if (!m) {
|
|
76
|
+
if (pending && line) pending = "";
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
for (const raw of m[2].split(",")) {
|
|
80
|
+
const name = (raw.split(/\bas\b/i).pop() ?? "").trim();
|
|
81
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(name)) continue;
|
|
82
|
+
out.push({
|
|
83
|
+
name,
|
|
84
|
+
kind: m[1] ? "type" : "value",
|
|
85
|
+
group: /@group\s+([^\n*/]+)/.exec(pending)?.[1]?.trim(),
|
|
86
|
+
hidden: pending.includes("@hidden")
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
pending = "";
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
function walk(dir) {
|
|
94
|
+
const files = [];
|
|
95
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
96
|
+
const path = join(dir, entry.name);
|
|
97
|
+
if (entry.isDirectory()) files.push(...walk(path));
|
|
98
|
+
else if (basename(path).endsWith(".ts")) files.push(path);
|
|
99
|
+
}
|
|
100
|
+
return files;
|
|
101
|
+
}
|
|
102
|
+
function signatureAt(lines, start) {
|
|
103
|
+
const parts = [];
|
|
104
|
+
for (let i = start; i < Math.min(lines.length, start + 10); i++) {
|
|
105
|
+
const t = lines[i].replace(/\s+/g, " ").trim();
|
|
106
|
+
if (!t) continue;
|
|
107
|
+
parts.push(t);
|
|
108
|
+
if (/[{;]\s*$/.test(t)) break;
|
|
109
|
+
}
|
|
110
|
+
return parts.join(" ").trim();
|
|
111
|
+
}
|
|
112
|
+
/** Collect doc blocks and declarations for one source file. */
|
|
113
|
+
function scanFile(file) {
|
|
114
|
+
const lines = readFileSync(file, "utf8").split("\n");
|
|
115
|
+
const blocks = [];
|
|
116
|
+
for (let i = 0; i < lines.length; i++) {
|
|
117
|
+
if (!lines[i].trim().startsWith("/**")) continue;
|
|
118
|
+
let k = i;
|
|
119
|
+
while (k < lines.length && !lines[k].includes("*/")) k++;
|
|
120
|
+
blocks.push({
|
|
121
|
+
start: i,
|
|
122
|
+
end: Math.min(k, lines.length - 1),
|
|
123
|
+
text: lines.slice(i, k + 1).join("\n")
|
|
124
|
+
});
|
|
125
|
+
i = k;
|
|
126
|
+
}
|
|
127
|
+
const decls = [];
|
|
128
|
+
for (let i = 0; i < lines.length; i++) for (const [re, kind] of DECL_PATTERNS) {
|
|
129
|
+
const m = re.exec(lines[i]);
|
|
130
|
+
if (!m) continue;
|
|
131
|
+
decls.push({
|
|
132
|
+
name: m[1],
|
|
133
|
+
line: i,
|
|
134
|
+
file,
|
|
135
|
+
kind,
|
|
136
|
+
sig: signatureAt(lines, i)
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
decls,
|
|
141
|
+
blocks
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/** Strip comment delimiters and typedoc noise so prose survives as markdown. */
|
|
145
|
+
function extract(block) {
|
|
146
|
+
const raw = block.text.split("\n");
|
|
147
|
+
if (raw.length) raw[0] = raw[0].replace(/^\/\*\*/, "");
|
|
148
|
+
if (raw.length > 1) raw[raw.length - 1] = raw[raw.length - 1].replace(/\*\/$/, "");
|
|
149
|
+
const markerCol = (/^[\t ]*/.exec(raw[1])?.[0] ?? "").length;
|
|
150
|
+
const lines = raw.map((l) => l.replace(/^\s*\*?\s?/, "")).map((l) => l.replace(/\{\@link(?:code|linkplain)?\s+([^\s}]+)[^}]*\}/g, "$1"));
|
|
151
|
+
const isContinuation = (index) => {
|
|
152
|
+
return (/^[\t ]*/.exec(raw[index])?.[0] ?? "").length > markerCol;
|
|
153
|
+
};
|
|
154
|
+
const desc = [];
|
|
155
|
+
const examples = [];
|
|
156
|
+
for (let i = 0; i < lines.length; i++) {
|
|
157
|
+
const ex = /^@example\b\s*(.*)$/.exec(lines[i]);
|
|
158
|
+
if (!ex) {
|
|
159
|
+
if (/^@\w/.test(lines[i])) {
|
|
160
|
+
while (i + 1 < lines.length && isContinuation(i + 1) && !/^@\w/.test(lines[i + 1])) i++;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
desc.push(lines[i]);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const title = ex[1].trim();
|
|
167
|
+
let j = i + 1;
|
|
168
|
+
while (j < lines.length && !lines[j].trim()) j++;
|
|
169
|
+
if (!/^`{3}(\w*)/.exec(lines[j]?.trim() ?? "")) continue;
|
|
170
|
+
const body = [];
|
|
171
|
+
j++;
|
|
172
|
+
while (j < lines.length && !/^\s*`{3}/.test(lines[j])) {
|
|
173
|
+
body.push(lines[j]);
|
|
174
|
+
j++;
|
|
175
|
+
}
|
|
176
|
+
examples.push({
|
|
177
|
+
title,
|
|
178
|
+
code: body.join("\n").trim()
|
|
179
|
+
});
|
|
180
|
+
i = j;
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
description: desc.join("\n").replace(/\n{3,}/g, "\n\n").replace(/^-{3,}\s*\n/gm, "\n").trim(),
|
|
184
|
+
examples
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function pickDoc(name) {
|
|
188
|
+
const candidates = byName[name];
|
|
189
|
+
if (!candidates) return void 0;
|
|
190
|
+
let best;
|
|
191
|
+
for (const d of candidates) {
|
|
192
|
+
const before = (scanned[d.file]?.blocks ?? []).filter((b) => b.end < d.line && !/@module\b/.test(b.text));
|
|
193
|
+
if (!before.length) continue;
|
|
194
|
+
const near = before[before.length - 1];
|
|
195
|
+
const gap = d.line - near.end;
|
|
196
|
+
if (gap > MAX_DOC_GAP_LINES) continue;
|
|
197
|
+
if (!best || near.end > best.block.end) best = {
|
|
198
|
+
decl: d,
|
|
199
|
+
block: near,
|
|
200
|
+
gap
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return best ?? { decl: candidates[0] };
|
|
204
|
+
}
|
|
205
|
+
var ROOT, SRC, OUTFILE, MAX_DOC_GAP_LINES, DECL_PATTERNS, publicEntries, scanned, byName, pkg, entries, undocumented$1, publicCount, index$1;
|
|
206
|
+
var init_mcp_docs_generate = __esmMin((() => {
|
|
207
|
+
ROOT = dirname(fileURLToPath(import.meta.url));
|
|
208
|
+
SRC = join(ROOT, "src");
|
|
209
|
+
OUTFILE = join(ROOT, "dist", "mcp", "api.json");
|
|
210
|
+
MAX_DOC_GAP_LINES = 14;
|
|
211
|
+
DECL_PATTERNS = [
|
|
212
|
+
[/^(?:export\s+)?(?:async\s+)?function\s*([A-Za-z_$][\w$]*)/, "function"],
|
|
213
|
+
[/^(?:export\s+)?class\s+([A-Za-z_$][\w$]*)/, "class"],
|
|
214
|
+
[/^(?:export\s+)?type\s+([A-Za-z_$][\w$]*)(?=[<\s=])/, "type alias"],
|
|
215
|
+
[/^(?:export\s+)?interface\s+([A-Za-z_$][\w$]*)/, "interface"]
|
|
216
|
+
];
|
|
217
|
+
publicEntries = [];
|
|
218
|
+
for (const dir of [
|
|
219
|
+
"core",
|
|
220
|
+
"state",
|
|
221
|
+
"utils",
|
|
222
|
+
"types"
|
|
223
|
+
]) {
|
|
224
|
+
const barrel = join(SRC, dir, "index.ts");
|
|
225
|
+
try {
|
|
226
|
+
publicEntries.push(...readBarrel(barrel));
|
|
227
|
+
} catch {
|
|
228
|
+
console.warn(`no barrel at ${barrel}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
scanned = Object.fromEntries(walk(SRC).map((file) => [file, scanFile(file)]));
|
|
232
|
+
byName = {};
|
|
233
|
+
for (const scan of Object.values(scanned)) for (const d of scan.decls) (byName[d.name] ??= []).push(d);
|
|
234
|
+
pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
|
|
235
|
+
entries = {};
|
|
236
|
+
undocumented$1 = [];
|
|
237
|
+
for (const pub of publicEntries) {
|
|
238
|
+
if (pub.hidden) continue;
|
|
239
|
+
const found = pickDoc(pub.name);
|
|
240
|
+
if (!found) {
|
|
241
|
+
undocumented$1.push(`${pub.kind} ${pub.name}`);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const sigs = byName[pub.name].map((d) => d.sig).filter(Boolean);
|
|
245
|
+
const doc = found.block ? extract(found.block) : {
|
|
246
|
+
description: "",
|
|
247
|
+
examples: []
|
|
248
|
+
};
|
|
249
|
+
entries[pub.name] = {
|
|
250
|
+
name: pub.name,
|
|
251
|
+
kind: found.decl.kind,
|
|
252
|
+
group: pub.group,
|
|
253
|
+
file: found.decl.file.replace(ROOT + "/", ""),
|
|
254
|
+
signatures: sigs.length ? [...new Set(sigs)] : void 0,
|
|
255
|
+
description: doc.description || void 0,
|
|
256
|
+
examples: doc.examples.length ? doc.examples : void 0
|
|
257
|
+
};
|
|
258
|
+
if (!doc.description) undocumented$1.push(`${pub.kind} ${pub.name}`);
|
|
259
|
+
}
|
|
260
|
+
publicCount = publicEntries.filter((p) => !p.hidden).length;
|
|
261
|
+
index$1 = {
|
|
262
|
+
libraryVersion: pkg.version,
|
|
263
|
+
exportedCount: publicCount,
|
|
264
|
+
entries,
|
|
265
|
+
undocumented: undocumented$1
|
|
266
|
+
};
|
|
267
|
+
if (Boolean(process.argv[1]) && import.meta.url === `file://${resolve(process.argv[1])}`) {
|
|
268
|
+
writeFileSync(OUTFILE, JSON.stringify(index$1, null, 2));
|
|
269
|
+
console.log(`${OUTFILE}: ${Object.keys(entries).length} of ${publicCount} public exports documented`);
|
|
270
|
+
if (undocumented$1.length) {
|
|
271
|
+
console.warn(`missing doc block (${undocumented$1.length}): ${undocumented$1.join(", ")}`);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}));
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/mcp/index.ts
|
|
2
278
|
/**
|
|
3
279
|
MCP (Model Context Protocol) Server for the Micro Components library.
|
|
4
280
|
|
|
@@ -16,249 +292,193 @@ at the compiled dist/mcp/index.js.
|
|
|
16
292
|
@example Run from source during development: pnpm run mcp:dev
|
|
17
293
|
@example Regenerate the shipped index only: pnpm run mcp:docs
|
|
18
294
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
|
-
const MAX_SOURCE_BYTES = 200_000;
|
|
27
|
-
const FENCE = '`'.repeat(3);
|
|
28
|
-
// Development: tsx runs src/mcp/index.ts, where the source file is present, so the
|
|
29
|
-
// index is generated live from src/ and concepts are read straight from docs/concepts.
|
|
30
|
-
// Production: the compiled server sits in dist/mcp, where no .ts source exists, so it
|
|
31
|
-
// reads the prebuilt api.json and the copied concepts/ that the build step emits next to it.
|
|
32
|
-
const IS_DEV = existsSync(join(__dirname, 'index.ts'));
|
|
33
|
-
let index;
|
|
34
|
-
let undocumented;
|
|
295
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
296
|
+
var MAX_SOURCE_BYTES = 2e5;
|
|
297
|
+
var FENCE = "`".repeat(3);
|
|
298
|
+
var IS_DEV = existsSync(join(__dirname, "index.ts"));
|
|
299
|
+
var index;
|
|
300
|
+
var undocumented;
|
|
35
301
|
if (IS_DEV) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const data = JSON.parse(readFileSync(join(__dirname, 'api.json'), 'utf8'));
|
|
44
|
-
index = data;
|
|
45
|
-
undocumented = data.undocumented;
|
|
302
|
+
const live = await Promise.resolve().then(() => (init_mcp_docs_generate(), mcp_docs_generate_exports));
|
|
303
|
+
index = live.index;
|
|
304
|
+
undocumented = live.undocumented;
|
|
305
|
+
} else {
|
|
306
|
+
const data = JSON.parse(readFileSync(join(__dirname, "api.json"), "utf8"));
|
|
307
|
+
index = data;
|
|
308
|
+
undocumented = data.undocumented;
|
|
46
309
|
}
|
|
47
|
-
|
|
310
|
+
var API = index.entries;
|
|
48
311
|
/** Concept essays live one per file: copied into dist/mcp/concepts in production, read from docs/concepts in development. */
|
|
49
|
-
|
|
312
|
+
var CONCEPTS_DIR = IS_DEV ? join(__dirname, "..", "..", "docs", "concepts") : join(__dirname, "concepts");
|
|
50
313
|
function readConcepts() {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
return out;
|
|
314
|
+
if (!existsSync(CONCEPTS_DIR)) return {};
|
|
315
|
+
const out = {};
|
|
316
|
+
for (const f of readdirSync(CONCEPTS_DIR)) {
|
|
317
|
+
if (!f.endsWith(".md")) continue;
|
|
318
|
+
out[f.replace(/\.md$/, "")] = readFileSync(join(CONCEPTS_DIR, f), "utf8").trim();
|
|
319
|
+
}
|
|
320
|
+
return out;
|
|
60
321
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const SOURCE_ROOT = join(__dirname, '..', '..');
|
|
322
|
+
var CONCEPTS = readConcepts();
|
|
323
|
+
var CONCEPT_NAMES = Object.keys(CONCEPTS);
|
|
324
|
+
var SOURCE_ROOT = join(__dirname, "..", "..");
|
|
65
325
|
/** First paragraph or sentence of a description, truncated honestly. */
|
|
66
326
|
function summarize(text, limit = 160) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return flat;
|
|
71
|
-
return `${flat.slice(0, limit).replace(/[\s,\-]+$/, '')}...`;
|
|
327
|
+
const flat = (text.split(/\n\s*\n|(?<=[.!?])\s+/)[0] ?? text).replace(/\s+/g, " ").trim();
|
|
328
|
+
if (flat.length <= limit) return flat;
|
|
329
|
+
return `${flat.slice(0, limit).replace(/[\s,\-]+$/, "")}...`;
|
|
72
330
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const server = new McpServer({
|
|
77
|
-
name: 'microcomponents-mcp',
|
|
78
|
-
version: index.libraryVersion,
|
|
331
|
+
var server = new McpServer({
|
|
332
|
+
name: "microcomponents-mcp",
|
|
333
|
+
version: index.libraryVersion
|
|
79
334
|
});
|
|
80
|
-
server.tool(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}, null, 2),
|
|
98
|
-
},
|
|
99
|
-
],
|
|
100
|
-
};
|
|
335
|
+
server.tool("list-components", "List all documented Micro Components classes, utilities and types with their group and a short summary", {}, async () => {
|
|
336
|
+
const list = Object.values(API).map((e) => ({
|
|
337
|
+
name: e.name,
|
|
338
|
+
kind: e.kind,
|
|
339
|
+
group: e.group,
|
|
340
|
+
file: e.file,
|
|
341
|
+
summary: summarize(e.description ?? "", 120),
|
|
342
|
+
examples: (e.examples ?? []).length
|
|
343
|
+
}));
|
|
344
|
+
return { content: [{
|
|
345
|
+
type: "text",
|
|
346
|
+
text: JSON.stringify({
|
|
347
|
+
libraryVersion: index.libraryVersion,
|
|
348
|
+
exportedCount: index.exportedCount,
|
|
349
|
+
components: list
|
|
350
|
+
}, null, 2)
|
|
351
|
+
}] };
|
|
101
352
|
});
|
|
102
|
-
server.tool(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
lines.push('## Examples');
|
|
137
|
-
for (const ex of examples) {
|
|
138
|
-
if (ex.title)
|
|
139
|
-
lines.push(`### ${ex.title}`);
|
|
140
|
-
lines.push(`${FENCE}ts`, ex.code, FENCE, '');
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
353
|
+
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 }) => {
|
|
354
|
+
const entry = API[componentName];
|
|
355
|
+
if (!entry) return {
|
|
356
|
+
content: [{
|
|
357
|
+
type: "text",
|
|
358
|
+
text: `No documentation for "${componentName}". ${Object.keys(API).length} entries exist; call list-components or search-components to discover names.`
|
|
359
|
+
}],
|
|
360
|
+
isError: true
|
|
361
|
+
};
|
|
362
|
+
const lines = [
|
|
363
|
+
`# ${entry.name}`,
|
|
364
|
+
"",
|
|
365
|
+
`**Kind:** ${entry.kind ?? "unknown"}`,
|
|
366
|
+
...entry.group ? [`**Group:** ${entry.group}`] : [],
|
|
367
|
+
...entry.file ? [`**Source:** ${entry.file}`] : [],
|
|
368
|
+
""
|
|
369
|
+
];
|
|
370
|
+
if (entry.description) lines.push("## Description", entry.description, "");
|
|
371
|
+
if (entry.signatures?.length) {
|
|
372
|
+
lines.push("## Signatures");
|
|
373
|
+
for (const sig of entry.signatures) lines.push(`${FENCE}ts`, sig, FENCE, "");
|
|
374
|
+
}
|
|
375
|
+
const examples = entry.examples ?? [];
|
|
376
|
+
if (examples.length) {
|
|
377
|
+
lines.push("## Examples");
|
|
378
|
+
for (const ex of examples) {
|
|
379
|
+
if (ex.title) lines.push(`### ${ex.title}`);
|
|
380
|
+
lines.push(`${FENCE}ts`, ex.code, FENCE, "");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return { content: [{
|
|
384
|
+
type: "text",
|
|
385
|
+
text: lines.join("\n")
|
|
386
|
+
}] };
|
|
144
387
|
});
|
|
145
|
-
server.tool(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
],
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
const results = hits.map((h) => ({
|
|
184
|
-
name: h.e.name,
|
|
185
|
-
kind: h.e.kind,
|
|
186
|
-
group: h.e.group,
|
|
187
|
-
relevance: h.s,
|
|
188
|
-
snippet: summarize(h.e.description ?? '', 150),
|
|
189
|
-
}));
|
|
190
|
-
return {
|
|
191
|
-
content: [
|
|
192
|
-
{
|
|
193
|
-
type: 'text',
|
|
194
|
-
text: JSON.stringify({ query, results, conceptMatches: conceptHits }, null, 2),
|
|
195
|
-
},
|
|
196
|
-
],
|
|
197
|
-
};
|
|
388
|
+
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 }) => {
|
|
389
|
+
const q = query.toLowerCase();
|
|
390
|
+
const scoreEntry = (e) => {
|
|
391
|
+
let score = 0;
|
|
392
|
+
if (e.name.toLowerCase() === q) score += 8;
|
|
393
|
+
if (e.name.toLowerCase().includes(q)) score += 4;
|
|
394
|
+
if ((e.group ?? "").toLowerCase().includes(q)) score += 2;
|
|
395
|
+
if ((e.description ?? "").toLowerCase().includes(q)) score += 3;
|
|
396
|
+
for (const ex of e.examples ?? []) if (ex.code.toLowerCase().includes(q) || ex.title.toLowerCase().includes(q)) score += 2;
|
|
397
|
+
return score;
|
|
398
|
+
};
|
|
399
|
+
const hits = Object.values(API).map((e) => ({
|
|
400
|
+
e,
|
|
401
|
+
s: scoreEntry(e)
|
|
402
|
+
})).filter((h) => h.s > 0).sort((a, b) => b.s - a.s || a.e.name.localeCompare(b.e.name)).slice(0, 12);
|
|
403
|
+
const conceptHits = CONCEPT_NAMES.filter((k) => CONCEPTS[k].toLowerCase().includes(q));
|
|
404
|
+
if (!hits.length && !conceptHits.length) return { content: [{
|
|
405
|
+
type: "text",
|
|
406
|
+
text: `No documentation matched "${query}". Use list-components to see every entry, or get-source-file for raw source.`
|
|
407
|
+
}] };
|
|
408
|
+
const results = hits.map((h) => ({
|
|
409
|
+
name: h.e.name,
|
|
410
|
+
kind: h.e.kind,
|
|
411
|
+
group: h.e.group,
|
|
412
|
+
relevance: h.s,
|
|
413
|
+
snippet: summarize(h.e.description ?? "", 150)
|
|
414
|
+
}));
|
|
415
|
+
return { content: [{
|
|
416
|
+
type: "text",
|
|
417
|
+
text: JSON.stringify({
|
|
418
|
+
query,
|
|
419
|
+
results,
|
|
420
|
+
conceptMatches: conceptHits
|
|
421
|
+
}, null, 2)
|
|
422
|
+
}] };
|
|
198
423
|
});
|
|
199
|
-
server.tool(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return { content: [{ type: 'text', text: content }] };
|
|
424
|
+
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 }) => {
|
|
425
|
+
const content = CONCEPTS[concept];
|
|
426
|
+
if (!content) return {
|
|
427
|
+
content: [{
|
|
428
|
+
type: "text",
|
|
429
|
+
text: `Concept "${concept}" not found. Available: ${CONCEPT_NAMES.join(", ")}`
|
|
430
|
+
}],
|
|
431
|
+
isError: true
|
|
432
|
+
};
|
|
433
|
+
return { content: [{
|
|
434
|
+
type: "text",
|
|
435
|
+
text: content
|
|
436
|
+
}] };
|
|
213
437
|
});
|
|
214
|
-
server.tool(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
438
|
+
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 }) => {
|
|
439
|
+
const full = resolve(SOURCE_ROOT, filepath);
|
|
440
|
+
if (full !== SOURCE_ROOT && !full.startsWith(SOURCE_ROOT + "/")) return {
|
|
441
|
+
content: [{
|
|
442
|
+
type: "text",
|
|
443
|
+
text: `Refused: "${filepath}" is outside the package sources.`
|
|
444
|
+
}],
|
|
445
|
+
isError: true
|
|
446
|
+
};
|
|
447
|
+
if (!existsSync(full)) return {
|
|
448
|
+
content: [{
|
|
449
|
+
type: "text",
|
|
450
|
+
text: `File not found: ${filepath}`
|
|
451
|
+
}],
|
|
452
|
+
isError: true
|
|
453
|
+
};
|
|
454
|
+
try {
|
|
455
|
+
const source = readFileSync(full, "utf8");
|
|
456
|
+
if (source.length > MAX_SOURCE_BYTES) return {
|
|
457
|
+
content: [{
|
|
458
|
+
type: "text",
|
|
459
|
+
text: `File too large (${source.length} bytes). Request a specific symbol via get-component-docs instead.`
|
|
460
|
+
}],
|
|
461
|
+
isError: true
|
|
462
|
+
};
|
|
463
|
+
return { content: [{
|
|
464
|
+
type: "text",
|
|
465
|
+
text: source
|
|
466
|
+
}] };
|
|
467
|
+
} catch (error) {
|
|
468
|
+
return {
|
|
469
|
+
content: [{
|
|
470
|
+
type: "text",
|
|
471
|
+
text: `Error reading file: ${error}`
|
|
472
|
+
}],
|
|
473
|
+
isError: true
|
|
474
|
+
};
|
|
475
|
+
}
|
|
248
476
|
});
|
|
249
|
-
server.tool(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
? `${undocumented.length} undocumented export(s):\n${undocumented.join('\n')}`
|
|
255
|
-
: `All ${index.exportedCount} public exports carry a doc block.`,
|
|
256
|
-
},
|
|
257
|
-
],
|
|
258
|
-
}));
|
|
259
|
-
// ============================================================================
|
|
260
|
-
// Start Server
|
|
261
|
-
// ============================================================================
|
|
262
|
-
const transport = new StdioServerTransport();
|
|
477
|
+
server.tool("list-undocumented", "Report public exports whose source lacks a doc block — the drift signal for src/ documentation", {}, async () => ({ content: [{
|
|
478
|
+
type: "text",
|
|
479
|
+
text: undocumented.length ? `${undocumented.length} undocumented export(s):\n${undocumented.join("\n")}` : `All ${index.exportedCount} public exports carry a doc block.`
|
|
480
|
+
}] }));
|
|
481
|
+
var transport = new StdioServerTransport();
|
|
263
482
|
await server.connect(transport);
|
|
264
483
|
console.error(`Micro Components MCP server running on stdio (library ${index.libraryVersion}, ${Object.keys(API).length} documented exports)`);
|
|
484
|
+
//#endregion
|