@twobirds/microcomponents 0.9.0 → 0.10.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.
Files changed (45) hide show
  1. package/README.md +2 -16
  2. package/dist/cli/bundle.d.ts +106 -0
  3. package/dist/cli/bundle.d.ts.map +1 -0
  4. package/dist/cli/bundler.d.ts +44 -0
  5. package/dist/cli/bundler.d.ts.map +1 -0
  6. package/dist/cli/create.d.ts.map +1 -1
  7. package/dist/cli/dev.d.ts +87 -0
  8. package/dist/cli/dev.d.ts.map +1 -0
  9. package/dist/cli/dispatch.d.ts +6 -2
  10. package/dist/cli/dispatch.d.ts.map +1 -1
  11. package/dist/cli/index.js +1807 -1
  12. package/dist/cli/inliner.d.ts +42 -0
  13. package/dist/cli/inliner.d.ts.map +1 -0
  14. package/dist/cli/preview.d.ts +48 -0
  15. package/dist/cli/preview.d.ts.map +1 -0
  16. package/dist/core/elements.js +1294 -75
  17. package/dist/core/mcGuards.js +5 -8
  18. package/dist/mcp/api.json +2 -17
  19. package/dist/mcp/concepts/project-layout.md +9 -8
  20. package/dist/mcp/index.js +450 -230
  21. package/dist/microcomponents.js +2882 -9
  22. package/dist/state/formValues.js +2236 -281
  23. package/dist/state/observables.js +1794 -342
  24. package/dist/utils/events.js +171 -197
  25. package/package.json +3 -7
  26. package/dist/cli/create.js +0 -653
  27. package/dist/cli/dispatch.js +0 -316
  28. package/dist/cli/scaffold.js +0 -109
  29. package/dist/cli/writeScaffold.js +0 -114
  30. package/dist/core/MC.js +0 -837
  31. package/dist/core/index.js +0 -72
  32. package/dist/state/index.js +0 -5
  33. package/dist/state/placeholders.js +0 -187
  34. package/dist/types/index.js +0 -1
  35. package/dist/types/mcTypes.js +0 -9
  36. package/dist/utils/autoload.js +0 -160
  37. package/dist/utils/helpers.js +0 -532
  38. package/dist/utils/index.js +0 -12
  39. package/dist/utils/resolveComponentPath.js +0 -39
  40. package/dist/vite/index.d.ts +0 -7
  41. package/dist/vite/index.d.ts.map +0 -1
  42. package/dist/vite/index.js +0 -6
  43. package/dist/vite/loadAssets.d.ts +0 -36
  44. package/dist/vite/loadAssets.d.ts.map +0 -1
  45. 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
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
20
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
21
- import { z } from 'zod';
22
- import { readdirSync, readFileSync, existsSync } from 'node:fs';
23
- import { dirname, join, resolve } from 'node:path';
24
- import { fileURLToPath } from 'node:url';
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
- // variable specifier keeps tsc from resolving the .ts path; tsx resolves it at runtime
37
- const generator = '../../mcp-docs-generate.ts';
38
- const live = await import(generator);
39
- index = live.index;
40
- undocumented = live.undocumented;
41
- }
42
- else {
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
- const API = index.entries;
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
- const CONCEPTS_DIR = IS_DEV ? join(__dirname, '..', '..', 'docs', 'concepts') : join(__dirname, 'concepts');
312
+ var CONCEPTS_DIR = IS_DEV ? join(__dirname, "..", "..", "docs", "concepts") : join(__dirname, "concepts");
50
313
  function readConcepts() {
51
- if (!existsSync(CONCEPTS_DIR))
52
- return {};
53
- const out = {};
54
- for (const f of readdirSync(CONCEPTS_DIR)) {
55
- if (!f.endsWith('.md'))
56
- continue;
57
- out[f.replace(/\.md$/, '')] = readFileSync(join(CONCEPTS_DIR, f), 'utf8').trim();
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
- const CONCEPTS = readConcepts();
62
- const CONCEPT_NAMES = Object.keys(CONCEPTS);
63
- // get-source-file is confined to the package root: the installed package in production, the repo root in development.
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
- const first = text.split(/\n\s*\n|(?<=[.!?])\s+/)[0] ?? text;
68
- const flat = first.replace(/\s+/g, ' ').trim();
69
- if (flat.length <= limit)
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
- // MCP Server Setup
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('list-components', 'List all documented Micro Components classes, utilities and types with their group and a short summary', {}, async () => {
81
- const list = Object.values(API).map((e) => ({
82
- name: e.name,
83
- kind: e.kind,
84
- group: e.group,
85
- file: e.file,
86
- summary: summarize(e.description ?? '', 120),
87
- examples: (e.examples ?? []).length,
88
- }));
89
- return {
90
- content: [
91
- {
92
- type: 'text',
93
- text: JSON.stringify({
94
- libraryVersion: index.libraryVersion,
95
- exportedCount: index.exportedCount,
96
- components: list,
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('get-component-docs', 'Get comprehensive documentation and usage examples for a Micro Components class or utility', {
103
- componentName: z
104
- .string()
105
- .describe('The name of the component (e.g., DC, defineCE, observe, injectCSS)'),
106
- }, async ({ componentName }) => {
107
- const entry = API[componentName];
108
- if (!entry) {
109
- return {
110
- content: [
111
- {
112
- type: 'text',
113
- text: `No documentation for "${componentName}". ${Object.keys(API).length} entries exist; call list-components or search-components to discover names.`,
114
- },
115
- ],
116
- isError: true,
117
- };
118
- }
119
- const lines = [
120
- `# ${entry.name}`,
121
- '',
122
- `**Kind:** ${entry.kind ?? 'unknown'}`,
123
- ...(entry.group ? [`**Group:** ${entry.group}`] : []),
124
- ...(entry.file ? [`**Source:** ${entry.file}`] : []),
125
- '',
126
- ];
127
- if (entry.description)
128
- lines.push('## Description', entry.description, '');
129
- if (entry.signatures?.length) {
130
- lines.push('## Signatures');
131
- for (const sig of entry.signatures)
132
- lines.push(`${FENCE}ts`, sig, FENCE, '');
133
- }
134
- const examples = entry.examples ?? [];
135
- if (examples.length) {
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('search-components', 'Search Micro Components documentation by keyword. Searches names, groups, descriptions and code examples.', {
146
- query: z
147
- .string()
148
- .describe('Search term to find relevant components (e.g., "event", "observable", "css")'),
149
- }, async ({ query }) => {
150
- const q = query.toLowerCase();
151
- const scoreEntry = (e) => {
152
- let score = 0;
153
- if (e.name.toLowerCase() === q)
154
- score += 8;
155
- if (e.name.toLowerCase().includes(q))
156
- score += 4;
157
- if ((e.group ?? '').toLowerCase().includes(q))
158
- score += 2;
159
- if ((e.description ?? '').toLowerCase().includes(q))
160
- score += 3;
161
- for (const ex of e.examples ?? []) {
162
- if (ex.code.toLowerCase().includes(q) || ex.title.toLowerCase().includes(q))
163
- score += 2;
164
- }
165
- return score;
166
- };
167
- const hits = Object.values(API)
168
- .map((e) => ({ e, s: scoreEntry(e) }))
169
- .filter((h) => h.s > 0)
170
- .sort((a, b) => b.s - a.s || a.e.name.localeCompare(b.e.name))
171
- .slice(0, 12);
172
- const conceptHits = CONCEPT_NAMES.filter((k) => CONCEPTS[k].toLowerCase().includes(q));
173
- if (!hits.length && !conceptHits.length) {
174
- return {
175
- content: [
176
- {
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
- ],
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('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 }) => {
200
- const content = CONCEPTS[concept];
201
- if (!content) {
202
- return {
203
- content: [
204
- {
205
- type: 'text',
206
- text: `Concept "${concept}" not found. Available: ${CONCEPT_NAMES.join(', ')}`,
207
- },
208
- ],
209
- isError: true,
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('get-source-file', 'Retrieve the actual source code from a Micro Components library file (package-root relative)', {
215
- filepath: z
216
- .string()
217
- .describe('Path to source file, e.g. dist/core/MC.js, dist/core/MC.d.ts, dist/mcp/concepts/event-system.md'),
218
- }, async ({ filepath }) => {
219
- const full = resolve(SOURCE_ROOT, filepath);
220
- // confine reads to the shipped sources so a caller cannot traverse outside them.
221
- if (full !== SOURCE_ROOT && !full.startsWith(SOURCE_ROOT + '/')) {
222
- return {
223
- content: [{ type: 'text', text: `Refused: "${filepath}" is outside the package sources.` }],
224
- isError: true,
225
- };
226
- }
227
- if (!existsSync(full)) {
228
- return { content: [{ type: 'text', text: `File not found: ${filepath}` }], isError: true };
229
- }
230
- try {
231
- const source = readFileSync(full, 'utf8');
232
- if (source.length > MAX_SOURCE_BYTES) {
233
- return {
234
- content: [
235
- {
236
- type: 'text',
237
- text: `File too large (${source.length} bytes). Request a specific symbol via get-component-docs instead.`,
238
- },
239
- ],
240
- isError: true,
241
- };
242
- }
243
- return { content: [{ type: 'text', text: source }] };
244
- }
245
- catch (error) {
246
- return { content: [{ type: 'text', text: `Error reading file: ${error}` }], isError: true };
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('list-undocumented', 'Report public exports whose source lacks a doc block — the drift signal for src/ documentation', {}, async () => ({
250
- content: [
251
- {
252
- type: 'text',
253
- text: undocumented.length
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