@twobirds/microcomponents 0.4.0 → 0.5.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/README.md +42 -20
- package/dist/cli/create.js +1 -1
- package/dist/cli/dispatch.d.ts.map +1 -1
- package/dist/cli/dispatch.js +52 -16
- package/dist/cli/writeScaffold.d.ts +35 -6
- package/dist/cli/writeScaffold.d.ts.map +1 -1
- package/dist/cli/writeScaffold.js +75 -21
- package/dist/mcp/api.json +601 -0
- package/dist/mcp/concepts/custom-element-registry.md +92 -0
- package/dist/mcp/concepts/event-system.md +45 -0
- package/dist/mcp/concepts/implementation-priority.md +10 -0
- package/dist/mcp/concepts/mc-types.md +349 -0
- package/dist/mcp/concepts/microcomponent-architecture.md +31 -0
- package/dist/mcp/concepts/state-management.md +18 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +264 -0
- package/package.json +17 -9
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
MCP (Model Context Protocol) Server for the Micro Components library.
|
|
4
|
+
|
|
5
|
+
Gives AI assistants tools to explore and work with @twobirds/microcomponents.
|
|
6
|
+
|
|
7
|
+
Documentation is derived, never hand-copied. In a published package the server runs
|
|
8
|
+
from dist/mcp and reads the prebuilt api.json and the copied concepts/ that the build
|
|
9
|
+
step emits next to it. In development (tsx from src/mcp) the index is generated live
|
|
10
|
+
from src/ at startup, so it cannot drift away from the library it documents.
|
|
11
|
+
|
|
12
|
+
The server is Node-only and is emitted by tsconfig.mcp.json; package.json "bin" points
|
|
13
|
+
at the compiled dist/mcp/index.js.
|
|
14
|
+
|
|
15
|
+
@example Run the published server: node dist/mcp/index.js
|
|
16
|
+
@example Run from source during development: pnpm run mcp:dev
|
|
17
|
+
@example Regenerate the shipped index only: pnpm run mcp:docs
|
|
18
|
+
*/
|
|
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;
|
|
35
|
+
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;
|
|
46
|
+
}
|
|
47
|
+
const API = index.entries;
|
|
48
|
+
/** 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');
|
|
50
|
+
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;
|
|
60
|
+
}
|
|
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, '..', '..');
|
|
65
|
+
/** First paragraph or sentence of a description, truncated honestly. */
|
|
66
|
+
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,\-]+$/, '')}...`;
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// MCP Server Setup
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
const server = new McpServer({
|
|
77
|
+
name: 'microcomponents-mcp',
|
|
78
|
+
version: index.libraryVersion,
|
|
79
|
+
});
|
|
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
|
+
};
|
|
101
|
+
});
|
|
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') }] };
|
|
144
|
+
});
|
|
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
|
+
};
|
|
198
|
+
});
|
|
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 }] };
|
|
213
|
+
});
|
|
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
|
+
}
|
|
248
|
+
});
|
|
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();
|
|
263
|
+
await server.connect(transport);
|
|
264
|
+
console.error(`Micro Components MCP server running on stdio (library ${index.libraryVersion}, ${Object.keys(API).length} documented exports)`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twobirds/microcomponents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Micro Components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"PWA",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"url": "git+https://gitlab.com/twoBirds/microcomponents.git"
|
|
34
34
|
},
|
|
35
35
|
"bin": {
|
|
36
|
-
"mc": "./dist/cli/index.js"
|
|
36
|
+
"mc": "./dist/cli/index.js",
|
|
37
|
+
"microcomponents-mcp": "./dist/mcp/index.js"
|
|
37
38
|
},
|
|
38
39
|
"files": [
|
|
39
40
|
"dist"
|
|
@@ -67,13 +68,17 @@
|
|
|
67
68
|
"types": "./dist/state/formValues.d.ts",
|
|
68
69
|
"import": "./dist/state/formValues.js"
|
|
69
70
|
},
|
|
71
|
+
"./mcp": {
|
|
72
|
+
"types": "./dist/mcp/index.d.ts",
|
|
73
|
+
"import": "./dist/mcp/index.js"
|
|
74
|
+
},
|
|
70
75
|
"./package.json": "./package.json"
|
|
71
76
|
},
|
|
72
77
|
"scripts": {
|
|
73
78
|
"format": "oxfmt .",
|
|
74
79
|
"lint": "oxlint \"src\"",
|
|
75
80
|
"fix": "oxfmt . && oxlint \"src\" --fix",
|
|
76
|
-
"check": "tsc --noEmit -p tsconfig.lib.json && tsc --noEmit -p tsconfig.cli.json && tsc -p tsconfig.test.json && tsc -p demo/web-test/tsconfig.demo.json && tsc --noEmit -p demo/shop/tsconfig.demo.json && tsc --noEmit -p demo/todo_mvc/tsconfig.demo.json && tsc -p tsconfig.node.json",
|
|
81
|
+
"check": "tsc --noEmit -p tsconfig.lib.json && tsc --noEmit -p tsconfig.cli.json && tsc --noEmit -p tsconfig.mcp.json && tsc -p tsconfig.test.json && tsc -p demo/web-test/tsconfig.demo.json && tsc --noEmit -p demo/shop/tsconfig.demo.json && tsc --noEmit -p demo/todo_mvc/tsconfig.demo.json && tsc -p tsconfig.node.json",
|
|
77
82
|
"check:pack": "publint && attw --pack . --profile esm-only",
|
|
78
83
|
"test": "vitest",
|
|
79
84
|
"test:run": "vitest run --reporter=verbose",
|
|
@@ -88,20 +93,24 @@
|
|
|
88
93
|
"typedoc": "typedoc --displayBasePath ./typedoc/",
|
|
89
94
|
"bundle": "tsx vite.build.ts",
|
|
90
95
|
"clean": "rm -rf dist public build coverage",
|
|
91
|
-
"build": "pnpm run clean && tsc -p tsconfig.lib.json && tsc -p tsconfig.cli.json && chmod +x dist/cli/index.js && pnpm run test:coverage && pnpm run bundle && pnpm run check:pack && pnpm run typedoc && pnpm run demo:build && pnpm run demo:shop:build && pnpm run demo:todo:build && find src -name '*.js' -delete",
|
|
96
|
+
"build": "pnpm run clean && tsc -p tsconfig.lib.json && tsc -p tsconfig.cli.json && chmod +x dist/cli/index.js && pnpm run mcp:build && pnpm run test:coverage && pnpm run bundle && pnpm run check:pack && pnpm run typedoc && pnpm run demo:build && pnpm run demo:shop:build && pnpm run demo:todo:build && find src -name '*.js' -delete",
|
|
97
|
+
"mcp:build": "tsc -p tsconfig.mcp.json && node mcp-docs-generate.ts && cp -r docs/concepts dist/mcp/concepts && chmod +x dist/mcp/index.js",
|
|
92
98
|
"demo:build": "tsx demo/web-test/vite.demo.ts",
|
|
93
99
|
"dev": "vite preview --outDir build/web-test --port 10000 --host",
|
|
94
100
|
"doc": "vite preview --outDir public/typedoc --port 11000 --host",
|
|
95
101
|
"watch": "tsx watch.ts",
|
|
96
102
|
"cli": "tsx src/cli/index.ts",
|
|
97
|
-
"mcp:dev": "tsx watch
|
|
103
|
+
"mcp:dev": "tsx watch src/mcp/index.ts",
|
|
98
104
|
"mcp:docs": "node mcp-docs-generate.ts",
|
|
99
|
-
"mcp:run": "tsx
|
|
105
|
+
"mcp:run": "tsx src/mcp/index.ts",
|
|
100
106
|
"prepublishOnly": "pnpm run build && pnpm run check"
|
|
101
107
|
},
|
|
108
|
+
"dependencies": {
|
|
109
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
110
|
+
"zod": "^4.6.5"
|
|
111
|
+
},
|
|
102
112
|
"devDependencies": {
|
|
103
113
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
104
|
-
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
105
114
|
"@types/node": "^22.20.1",
|
|
106
115
|
"@vitest/coverage-v8": "^4.1.10",
|
|
107
116
|
"jsdom": "^29.1.1",
|
|
@@ -112,8 +121,7 @@
|
|
|
112
121
|
"typedoc": "^0.28.20",
|
|
113
122
|
"typescript": "^6.0.3",
|
|
114
123
|
"vite": "^8.1.5",
|
|
115
|
-
"vitest": "^4.1.10"
|
|
116
|
-
"zod": "^4.6.5"
|
|
124
|
+
"vitest": "^4.1.10"
|
|
117
125
|
},
|
|
118
126
|
"engines": {
|
|
119
127
|
"node": ">=22.18"
|