@webjsdev/cli 0.10.10 → 0.10.12
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/bin/webjs.js +6 -4
- package/lib/create.js +16 -2
- package/lib/mcp-docs.js +400 -0
- package/lib/mcp-source.js +244 -0
- package/lib/mcp.js +167 -18
- package/package.json +7 -2
- package/resources/AGENTS.md +404 -0
- package/resources/agent-docs/advanced.md +1090 -0
- package/resources/agent-docs/built-ins.md +367 -0
- package/resources/agent-docs/components.md +486 -0
- package/resources/agent-docs/configuration.md +207 -0
- package/resources/agent-docs/framework-dev.md +65 -0
- package/resources/agent-docs/lit-muscle-memory-gotchas.md +456 -0
- package/resources/agent-docs/metadata.md +334 -0
- package/resources/agent-docs/recipes.md +440 -0
- package/resources/agent-docs/service-worker.md +100 -0
- package/resources/agent-docs/ssr-partial-nav-design.md +214 -0
- package/resources/agent-docs/styling.md +235 -0
- package/resources/agent-docs/testing.md +372 -0
- package/resources/agent-docs/typescript.md +334 -0
- package/templates/.dockerignore +6 -4
- package/templates/AGENTS.md +25 -10
- package/templates/CONVENTIONS.md +18 -1
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `source` tool for `webjs mcp` (#378): read-only access to the FRAMEWORK
|
|
3
|
+
* source itself.
|
|
4
|
+
*
|
|
5
|
+
* webjs is buildless, so every app's `node_modules/@webjsdev/<pkg>/src` holds the
|
|
6
|
+
* authored JSDoc `.js`, and server-side that source runs directly. (The one built
|
|
7
|
+
* artifact is the `@webjsdev/core` browser bundle in `dist/`, which this tool
|
|
8
|
+
* deliberately skips: it surfaces only the authored `src/`.) That is a real
|
|
9
|
+
* advantage: when the docs do not answer a question, an agent can read the real
|
|
10
|
+
* authored source. This tool makes that first-class and
|
|
11
|
+
* discoverable (and reachable for an MCP-only client with no filesystem tools):
|
|
12
|
+
* - no args (or `package`): list the resolved `@webjsdev/*` packages + their
|
|
13
|
+
* `src/` entry-point files.
|
|
14
|
+
* - `query`: grep the framework `src/` trees, returning bounded `file:line`
|
|
15
|
+
* hits (with a disclosed cap, no silent truncation).
|
|
16
|
+
* - `path`: read one source file (e.g. `server/src/ssr.js`), traversal-guarded
|
|
17
|
+
* to stay inside a resolved framework package root.
|
|
18
|
+
*
|
|
19
|
+
* READ-ONLY and side-effect-free: it only reads files, loads no module, and
|
|
20
|
+
* cannot read outside the resolved `@webjsdev/*` package roots. Zero-dependency,
|
|
21
|
+
* consistent with the rest of the server. PURE given injected `deps`
|
|
22
|
+
* (`{ roots, readFile, readdir }`), so it is testable against a fake tree.
|
|
23
|
+
*
|
|
24
|
+
* @module mcp-source
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
import { join, resolve, sep, relative } from 'node:path';
|
|
29
|
+
|
|
30
|
+
/** The published framework packages whose source an agent may want to read. */
|
|
31
|
+
export const FRAMEWORK_PACKAGES = ['core', 'server', 'cli', 'ts-plugin', 'ui'];
|
|
32
|
+
|
|
33
|
+
/** Source file extensions worth grepping / reading (text, not assets). */
|
|
34
|
+
const TEXT_EXT = /\.(?:js|ts|mjs|mts|cjs|cts|json|md)$/i;
|
|
35
|
+
|
|
36
|
+
/** Bound the grep output (disclosed when hit) and the walk (defensive). */
|
|
37
|
+
const MAX_HITS = 60;
|
|
38
|
+
const MAX_FILES = 4000;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve each `@webjsdev/*` package's root + source dir from `cwd`. Locates the
|
|
42
|
+
* root by checking each `require.resolve.paths` node_modules dir on disk for
|
|
43
|
+
* `@webjsdev/<pkg>/package.json`, so it works for a real `node_modules` install
|
|
44
|
+
* AND the monorepo workspace (where the dir is a symlink to `packages/<pkg>`),
|
|
45
|
+
* and honours hoisting. This fs check is deliberate: `<pkg>/package.json` is
|
|
46
|
+
* blocked by `exports` for server/cli/ui, and the bin-only cli has no main
|
|
47
|
+
* entry, so neither `resolve('<pkg>/package.json')` nor `resolve('<pkg>')` is
|
|
48
|
+
* reliable. The source dir is `src/`, or `lib/` for the cli. A package that is
|
|
49
|
+
* not installed is skipped (not every app depends on every `@webjsdev/*`).
|
|
50
|
+
*
|
|
51
|
+
* @param {string} cwd
|
|
52
|
+
* @param {{ exists: (p: string) => boolean }} fsDeps
|
|
53
|
+
* @returns {Array<{ pkg: string, root: string, src: string }>}
|
|
54
|
+
*/
|
|
55
|
+
export function resolveFrameworkRoots(cwd, fsDeps) {
|
|
56
|
+
const req = createRequire(join(cwd, '__webjs_mcp_source__.js'));
|
|
57
|
+
/** @type {Array<{ pkg: string, root: string, src: string }>} */
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const pkg of FRAMEWORK_PACKAGES) {
|
|
60
|
+
// Find the package ROOT by checking each node_modules search path on disk,
|
|
61
|
+
// NOT via `require.resolve('<pkg>')` or `<pkg>/package.json`: a package whose
|
|
62
|
+
// `exports` omits `./package.json` (server/cli/ui) or that has no main entry
|
|
63
|
+
// (cli is a bin-only package) would otherwise be unreachable. The fs check
|
|
64
|
+
// bypasses both and still honours hoisting (the search paths include every
|
|
65
|
+
// parent `node_modules`).
|
|
66
|
+
const bases = req.resolve.paths(`@webjsdev/${pkg}`) || [];
|
|
67
|
+
let root = '';
|
|
68
|
+
for (const base of bases) {
|
|
69
|
+
const cand = join(base, '@webjsdev', pkg);
|
|
70
|
+
if (fsDeps.exists(join(cand, 'package.json'))) { root = cand; break; }
|
|
71
|
+
}
|
|
72
|
+
if (!root) continue;
|
|
73
|
+
// Most packages keep source in `src/`; the cli keeps it in `lib/`. Use
|
|
74
|
+
// whichever exists so every framework package's source is reachable.
|
|
75
|
+
const src = fsDeps.exists(join(root, 'src'))
|
|
76
|
+
? join(root, 'src')
|
|
77
|
+
: fsDeps.exists(join(root, 'lib'))
|
|
78
|
+
? join(root, 'lib')
|
|
79
|
+
: '';
|
|
80
|
+
if (src) out.push({ pkg, root, src });
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Recursively list text-source files under `dir` (absolute paths), skipping
|
|
87
|
+
* `node_modules` / `dist` and bounded by {@link MAX_FILES}.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} dir
|
|
90
|
+
* @param {{ readdir: (d: string) => Array<{ name: string, isDir: boolean }> }} deps
|
|
91
|
+
* @returns {string[]}
|
|
92
|
+
*/
|
|
93
|
+
export function walkSource(dir, deps) {
|
|
94
|
+
/** @type {string[]} */
|
|
95
|
+
const files = [];
|
|
96
|
+
/** @type {string[]} */
|
|
97
|
+
const stack = [dir];
|
|
98
|
+
while (stack.length && files.length < MAX_FILES) {
|
|
99
|
+
const d = stack.pop();
|
|
100
|
+
let entries = [];
|
|
101
|
+
try { entries = deps.readdir(d); } catch { continue; }
|
|
102
|
+
for (const e of entries) {
|
|
103
|
+
if (e.isDir) {
|
|
104
|
+
if (e.name === 'node_modules' || e.name === 'dist' || e.name === '.git') continue;
|
|
105
|
+
stack.push(join(d, e.name));
|
|
106
|
+
} else if (TEXT_EXT.test(e.name)) {
|
|
107
|
+
files.push(join(d, e.name));
|
|
108
|
+
if (files.length >= MAX_FILES) break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return files.sort();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* No-args / `package` mode: list the resolved packages and their `src/`
|
|
117
|
+
* top-level files (the entry points), so the agent has a map to grep or read.
|
|
118
|
+
*
|
|
119
|
+
* @param {{ roots: Array<{ pkg: string, root: string, src: string }>, readdir: Function }} deps
|
|
120
|
+
* @param {string} [pkgFilter]
|
|
121
|
+
* @returns {string}
|
|
122
|
+
*/
|
|
123
|
+
export function listSources(deps, pkgFilter) {
|
|
124
|
+
const roots = pkgFilter ? deps.roots.filter((r) => r.pkg === pkgFilter) : deps.roots;
|
|
125
|
+
if (!roots.length) {
|
|
126
|
+
return pkgFilter
|
|
127
|
+
? `@webjsdev/${pkgFilter} is not installed/resolvable here. Resolvable: ${deps.roots.map((r) => r.pkg).join(', ') || '(none)'}`
|
|
128
|
+
: 'No @webjsdev/* packages resolvable from here (run inside a webjs app or the monorepo).';
|
|
129
|
+
}
|
|
130
|
+
const lines = ['webjs framework authored source (buildless; server-side this runs directly, and core ships a built browser dist/ that is excluded here). Read with `source({ path })` or search with `source({ query })`.', ''];
|
|
131
|
+
for (const r of roots) {
|
|
132
|
+
const dirName = r.src.split(sep).pop(); // 'src' for most, 'lib' for cli
|
|
133
|
+
let entries = [];
|
|
134
|
+
try { entries = deps.readdir(r.src); } catch { entries = []; }
|
|
135
|
+
const top = entries.filter((e) => !e.isDir && TEXT_EXT.test(e.name)).map((e) => e.name).sort();
|
|
136
|
+
const dirs = entries.filter((e) => e.isDir).map((e) => e.name).sort();
|
|
137
|
+
lines.push(`@webjsdev/${r.pkg}/${dirName}:`);
|
|
138
|
+
if (top.length) lines.push(` files: ${top.map((f) => `${r.pkg}/${dirName}/${f}`).join(', ')}`);
|
|
139
|
+
if (dirs.length) lines.push(` subdirs: ${dirs.join(', ')}`);
|
|
140
|
+
}
|
|
141
|
+
return lines.join('\n');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* `query` mode: grep every resolved `src/` tree for the (case-insensitive)
|
|
146
|
+
* substring, returning bounded `[<pkg>/src/<rel>:<line>] <text>` hits. Discloses
|
|
147
|
+
* truncation rather than silently capping.
|
|
148
|
+
*
|
|
149
|
+
* @param {{ roots: Array<{ pkg: string, root: string, src: string }>, readFile: Function, readdir: Function }} deps
|
|
150
|
+
* @param {string} query
|
|
151
|
+
* @returns {Promise<string>}
|
|
152
|
+
*/
|
|
153
|
+
export async function grepSources(deps, query) {
|
|
154
|
+
const q = String(query).toLowerCase();
|
|
155
|
+
if (!q) return 'Provide a non-empty `query`.';
|
|
156
|
+
/** @type {string[]} */
|
|
157
|
+
const hits = [];
|
|
158
|
+
let capped = false;
|
|
159
|
+
outer: for (const r of deps.roots) {
|
|
160
|
+
for (const file of walkSource(r.src, deps)) {
|
|
161
|
+
let text = '';
|
|
162
|
+
try { text = await deps.readFile(file, 'utf8'); } catch { continue; }
|
|
163
|
+
if (!text.toLowerCase().includes(q)) continue; // fast skip whole file
|
|
164
|
+
const lines = text.split('\n');
|
|
165
|
+
const rel = relative(r.root, file).split(sep).join('/');
|
|
166
|
+
for (let i = 0; i < lines.length; i++) {
|
|
167
|
+
if (!lines[i].toLowerCase().includes(q)) continue;
|
|
168
|
+
if (hits.length >= MAX_HITS) { capped = true; break outer; }
|
|
169
|
+
hits.push(`[@webjsdev/${r.pkg}/${rel}:${i + 1}] ${lines[i].trim()}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!hits.length) return `No matches for "${query}" in the @webjsdev/* source.`;
|
|
174
|
+
if (capped) hits.push(`... (truncated at ${MAX_HITS} matches; narrow the query or read a file with \`path\`)`);
|
|
175
|
+
return hits.join('\n');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** True when `p` is `base` itself or a descendant of it. */
|
|
179
|
+
function within(base, p) {
|
|
180
|
+
return p === base || p.startsWith(base + sep);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* `path` mode: read one AUTHORED-source file. Accepts `<pkg>/...` or
|
|
185
|
+
* `@webjsdev/<pkg>/...`. Scoped to the package's SOURCE dir (`src/`, or `lib/`
|
|
186
|
+
* for cli), so it serves only the authored source and NOT the built `dist/`
|
|
187
|
+
* browser bundle, `node_modules`, etc. Refuses any path that escapes the source
|
|
188
|
+
* dir lexically (`..`/absolute), and (when `deps.realpath` is provided)
|
|
189
|
+
* re-checks the symlink-resolved path so a symlink inside `src/` cannot reach
|
|
190
|
+
* outside. Read-only.
|
|
191
|
+
*
|
|
192
|
+
* @param {{ roots: Array<{ pkg: string, root: string, src: string }>, readFile: Function, realpath?: Function }} deps
|
|
193
|
+
* @param {string} path
|
|
194
|
+
* @returns {Promise<string>}
|
|
195
|
+
*/
|
|
196
|
+
export async function readSource(deps, path) {
|
|
197
|
+
const cleaned = String(path).replace(/^@webjsdev\//, '');
|
|
198
|
+
const segs = cleaned.split('/').filter(Boolean);
|
|
199
|
+
const pkg = segs[0];
|
|
200
|
+
const entry = deps.roots.find((r) => r.pkg === pkg);
|
|
201
|
+
if (!entry) {
|
|
202
|
+
return `Unknown or unresolvable package "${pkg || path}". Resolvable: ${deps.roots.map((r) => r.pkg).join(', ') || '(none)'}. Pass a path like server/src/ssr.js.`;
|
|
203
|
+
}
|
|
204
|
+
const abs = resolve(entry.root, segs.slice(1).join('/'));
|
|
205
|
+
const srcLabel = entry.src.split(sep).pop();
|
|
206
|
+
// Scope to the authored source dir, so dist/ (the built core browser bundle),
|
|
207
|
+
// package.json, node_modules, etc. are not readable; only `src/` (or cli `lib/`).
|
|
208
|
+
if (!within(entry.src, abs)) {
|
|
209
|
+
return `Refusing to read outside the @webjsdev/${pkg} authored source (only ${srcLabel}/ is exposed; the built dist/ is not).`;
|
|
210
|
+
}
|
|
211
|
+
// Defense in depth: a symlink inside the source dir must not resolve outside it.
|
|
212
|
+
if (deps.realpath) {
|
|
213
|
+
try {
|
|
214
|
+
if (!within(deps.realpath(entry.src), deps.realpath(abs))) {
|
|
215
|
+
return `Refusing to read outside the @webjsdev/${pkg} authored source (a symlink escapes ${srcLabel}/).`;
|
|
216
|
+
}
|
|
217
|
+
} catch { /* abs does not exist; the readFile below returns the not-a-file message */ }
|
|
218
|
+
}
|
|
219
|
+
// Legacy guard kept as a belt-and-suspenders against a root escape too.
|
|
220
|
+
if (abs !== entry.root && !abs.startsWith(entry.root + sep)) {
|
|
221
|
+
return `Refusing to read outside @webjsdev/${pkg} (path escapes the package root).`;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
return await deps.readFile(abs, 'utf8');
|
|
225
|
+
} catch {
|
|
226
|
+
return `Could not read ${path} (not a file under @webjsdev/${pkg}).`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The `source` tool entry point. Dispatches on the args: `path` reads a file,
|
|
232
|
+
* `query` greps, otherwise (or with `package`) lists the packages. PURE given
|
|
233
|
+
* `deps` (`{ roots, readFile, readdir }`).
|
|
234
|
+
*
|
|
235
|
+
* @param {object} deps
|
|
236
|
+
* @param {{ query?: string, path?: string, package?: string }} [args]
|
|
237
|
+
* @returns {Promise<string>}
|
|
238
|
+
*/
|
|
239
|
+
export async function runSourceTool(deps, args) {
|
|
240
|
+
const a = args || {};
|
|
241
|
+
if (a.path) return readSource(deps, a.path);
|
|
242
|
+
if (a.query) return grepSources(deps, a.query);
|
|
243
|
+
return listSources(deps, a.package);
|
|
244
|
+
}
|
package/lib/mcp.js
CHANGED
|
@@ -23,6 +23,17 @@
|
|
|
23
23
|
import { createInterface } from 'node:readline';
|
|
24
24
|
import { relative } from 'node:path';
|
|
25
25
|
|
|
26
|
+
import {
|
|
27
|
+
resolveDocsLocation,
|
|
28
|
+
listResources,
|
|
29
|
+
readResource,
|
|
30
|
+
initText,
|
|
31
|
+
searchDocs,
|
|
32
|
+
PROMPTS,
|
|
33
|
+
getPrompt,
|
|
34
|
+
} from './mcp-docs.js';
|
|
35
|
+
import { resolveFrameworkRoots, runSourceTool } from './mcp-source.js';
|
|
36
|
+
|
|
26
37
|
const PROTOCOL_VERSION = '2024-11-05';
|
|
27
38
|
|
|
28
39
|
/**
|
|
@@ -31,41 +42,109 @@ const PROTOCOL_VERSION = '2024-11-05';
|
|
|
31
42
|
* into an agent-friendly shape. Descriptions are crisp so a model picks the
|
|
32
43
|
* right tool without reading source.
|
|
33
44
|
*/
|
|
45
|
+
/** The shared input schema for the introspection tools: an optional appDir override. */
|
|
46
|
+
const APPDIR_SCHEMA = {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
appDir: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
description: 'App directory to introspect. Defaults to the server cwd.',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
required: [],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** `init` takes no input. */
|
|
58
|
+
const INIT_SCHEMA = { type: 'object', properties: {}, required: [] };
|
|
59
|
+
|
|
60
|
+
/** `docs` takes an optional topic OR a free-text query. */
|
|
61
|
+
const DOCS_SCHEMA = {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
topic: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
description: 'A doc name (e.g. components, recipes, lit-muscle-memory-gotchas, AGENTS). Returns the full doc.',
|
|
67
|
+
},
|
|
68
|
+
query: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: 'Free-text search across all webjs docs. Returns matching lines with their source.',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
required: [],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** `source` reads the framework source: a `path` to read, a `query` to grep, or a `package` to list. */
|
|
77
|
+
const SOURCE_SCHEMA = {
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: {
|
|
80
|
+
path: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
description: 'A framework source file to read, e.g. server/src/ssr.js or @webjsdev/core/src/render-client.js.',
|
|
83
|
+
},
|
|
84
|
+
query: {
|
|
85
|
+
type: 'string',
|
|
86
|
+
description: 'Grep the @webjsdev/* src trees for this substring. Returns file:line hits.',
|
|
87
|
+
},
|
|
88
|
+
package: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: 'Limit a no-args listing to one package (core, server, cli, ts-plugin, ui).',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
required: [],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The tools. The four introspection tools project an EXISTING @webjsdev/server
|
|
98
|
+
* function (read-only, appDir-scoped). `init` + `docs` (#376) surface the
|
|
99
|
+
* framework knowledge: `init` is the "read first" mental-model primer, `docs`
|
|
100
|
+
* retrieves a doc by topic or searches the corpus. Descriptions are crisp so a
|
|
101
|
+
* model picks the right tool without reading source.
|
|
102
|
+
*/
|
|
34
103
|
const TOOL_DEFS = [
|
|
104
|
+
{
|
|
105
|
+
name: 'init',
|
|
106
|
+
description:
|
|
107
|
+
'READ THIS FIRST before writing or editing a webjs app. Returns the webjs mental model (NOT React/Next: no RSC, components hydrate but pages do not, signals-default state, the .server boundary) plus the invariants and the doc index. Read-only.',
|
|
108
|
+
inputSchema: INIT_SCHEMA,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'docs',
|
|
112
|
+
description:
|
|
113
|
+
'Retrieve webjs framework docs: pass `topic` for a full doc (components, recipes, styling, built-ins, configuration, advanced, metadata, typescript, testing, lit-muscle-memory-gotchas, AGENTS, ...) or `query` to search the corpus. No args returns the topic index. Read-only.',
|
|
114
|
+
inputSchema: DOCS_SCHEMA,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'source',
|
|
118
|
+
description:
|
|
119
|
+
'Read the FRAMEWORK authored source (webjs is buildless: node_modules/@webjsdev/*/src is the JSDoc source, run directly server-side; only the core browser bundle is built into dist/, which this skips). Pass `path` to read a file (e.g. server/src/ssr.js), `query` to grep the @webjsdev/* src trees, or no args to list the packages + entry points. Use when the docs do not answer something. Read-only.',
|
|
120
|
+
inputSchema: SOURCE_SCHEMA,
|
|
121
|
+
},
|
|
35
122
|
{
|
|
36
123
|
name: 'list_routes',
|
|
37
124
|
description:
|
|
38
125
|
'List the app route table: SSR pages (path, file, dynamic flag, param names) and route.{js,ts} API handlers (path, file, HTTP methods). Read-only.',
|
|
126
|
+
inputSchema: APPDIR_SCHEMA,
|
|
39
127
|
},
|
|
40
128
|
{
|
|
41
129
|
name: 'list_actions',
|
|
42
130
|
description:
|
|
43
131
|
'List registered server actions (the .server.{js,ts} files with "use server"): file, exported function name, and the /__webjs/action/<hash>/<fn> RPC endpoint. Read-only.',
|
|
132
|
+
inputSchema: APPDIR_SCHEMA,
|
|
44
133
|
},
|
|
45
134
|
{
|
|
46
135
|
name: 'list_components',
|
|
47
136
|
description:
|
|
48
137
|
'List registered custom-element tags: tag name, defining file, and class name. Read-only.',
|
|
138
|
+
inputSchema: APPDIR_SCHEMA,
|
|
49
139
|
},
|
|
50
140
|
{
|
|
51
141
|
name: 'check',
|
|
52
142
|
description:
|
|
53
143
|
'Run webjs check (correctness rules) and return the structured violations { rule, file, message, fix } plus a summary count and per-rule breakdown. Read-only.',
|
|
144
|
+
inputSchema: APPDIR_SCHEMA,
|
|
54
145
|
},
|
|
55
146
|
];
|
|
56
147
|
|
|
57
|
-
/** The shared input schema: every tool takes an optional appDir override. */
|
|
58
|
-
const TOOL_INPUT_SCHEMA = {
|
|
59
|
-
type: 'object',
|
|
60
|
-
properties: {
|
|
61
|
-
appDir: {
|
|
62
|
-
type: 'string',
|
|
63
|
-
description: 'App directory to introspect. Defaults to the server cwd.',
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
required: [],
|
|
67
|
-
};
|
|
68
|
-
|
|
69
148
|
/**
|
|
70
149
|
* Lexically extract the names exported from a module source. Recognises the
|
|
71
150
|
* common forms a server-action / route file uses without LOADING the module
|
|
@@ -302,6 +381,39 @@ export async function runMcpServer(opts) {
|
|
|
302
381
|
}
|
|
303
382
|
const runners = makeToolRunners(deps);
|
|
304
383
|
|
|
384
|
+
// The docs corpus deps for the knowledge layer (#376): resources / prompts /
|
|
385
|
+
// init / docs. Injectable for tests; otherwise resolved from the bundled
|
|
386
|
+
// (published) or repo-root (dev) docs and node fs.
|
|
387
|
+
let docsDeps = opts.docsDeps;
|
|
388
|
+
if (!docsDeps) {
|
|
389
|
+
const loc = resolveDocsLocation(import.meta.url);
|
|
390
|
+
const { readFile } = await import('node:fs/promises');
|
|
391
|
+
const { readdirSync, existsSync } = await import('node:fs');
|
|
392
|
+
docsDeps = {
|
|
393
|
+
docsDir: loc.docsDir,
|
|
394
|
+
agentsPath: loc.agentsPath,
|
|
395
|
+
listDir: readdirSync,
|
|
396
|
+
exists: existsSync,
|
|
397
|
+
readFile,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// The `source` tool (#378): read the framework's own source from
|
|
402
|
+
// node_modules/@webjsdev/*/src (no-build, so it is the real JSDoc). Roots are
|
|
403
|
+
// resolved once from the server cwd. Injectable for tests.
|
|
404
|
+
let sourceDeps = opts.sourceDeps;
|
|
405
|
+
if (!sourceDeps) {
|
|
406
|
+
const { readFile } = await import('node:fs/promises');
|
|
407
|
+
const { readdirSync, existsSync, realpathSync } = await import('node:fs');
|
|
408
|
+
const readdir = (d) => readdirSync(d, { withFileTypes: true }).map((e) => ({ name: e.name, isDir: e.isDirectory() }));
|
|
409
|
+
sourceDeps = {
|
|
410
|
+
roots: resolveFrameworkRoots(cwd, { exists: existsSync }),
|
|
411
|
+
readFile,
|
|
412
|
+
readdir,
|
|
413
|
+
realpath: realpathSync,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
305
417
|
/** Write one JSON-RPC frame as a single line to stdout. */
|
|
306
418
|
const send = (frame) => {
|
|
307
419
|
stdout.write(JSON.stringify(frame) + '\n');
|
|
@@ -323,7 +435,7 @@ export async function runMcpServer(opts) {
|
|
|
323
435
|
if (method === 'initialize') {
|
|
324
436
|
return rpcResult(id, {
|
|
325
437
|
protocolVersion: PROTOCOL_VERSION,
|
|
326
|
-
capabilities: { tools: {} },
|
|
438
|
+
capabilities: { tools: {}, resources: {}, prompts: {} },
|
|
327
439
|
serverInfo: { name: 'webjs', version },
|
|
328
440
|
});
|
|
329
441
|
}
|
|
@@ -336,24 +448,61 @@ export async function runMcpServer(opts) {
|
|
|
336
448
|
tools: TOOL_DEFS.map((t) => ({
|
|
337
449
|
name: t.name,
|
|
338
450
|
description: t.description,
|
|
339
|
-
inputSchema:
|
|
451
|
+
inputSchema: t.inputSchema,
|
|
340
452
|
})),
|
|
341
453
|
});
|
|
342
454
|
}
|
|
343
455
|
|
|
456
|
+
// Knowledge layer (#376): the framework docs as MCP resources.
|
|
457
|
+
if (method === 'resources/list') {
|
|
458
|
+
return rpcResult(id, { resources: listResources(docsDeps) });
|
|
459
|
+
}
|
|
460
|
+
if (method === 'resources/read') {
|
|
461
|
+
const uri = ((msg && msg.params) || {}).uri;
|
|
462
|
+
try {
|
|
463
|
+
const r = await readResource(docsDeps, uri);
|
|
464
|
+
return rpcResult(id, { contents: [r] });
|
|
465
|
+
} catch (e) {
|
|
466
|
+
return rpcError(id, -32602, e && e.message ? e.message : String(e));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Knowledge layer (#376): the recipes as guided-workflow prompts.
|
|
471
|
+
if (method === 'prompts/list') {
|
|
472
|
+
return rpcResult(id, { prompts: PROMPTS });
|
|
473
|
+
}
|
|
474
|
+
if (method === 'prompts/get') {
|
|
475
|
+
const params = (msg && msg.params) || {};
|
|
476
|
+
try {
|
|
477
|
+
return rpcResult(id, getPrompt(params.name, params.arguments));
|
|
478
|
+
} catch (e) {
|
|
479
|
+
return rpcError(id, -32602, e && e.message ? e.message : String(e));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
344
483
|
if (method === 'tools/call') {
|
|
345
484
|
const params = (msg && msg.params) || {};
|
|
346
485
|
const name = params.name;
|
|
347
486
|
const args = params.arguments || {};
|
|
348
|
-
|
|
349
|
-
|
|
487
|
+
// The knowledge tools route to the docs / source layer; they return text.
|
|
488
|
+
const isKnowledgeTool = name === 'init' || name === 'docs' || name === 'source';
|
|
489
|
+
if (!isKnowledgeTool && !runners[name]) {
|
|
350
490
|
return rpcError(id, -32602, `Unknown tool: ${String(name)}`);
|
|
351
491
|
}
|
|
352
492
|
const appDir = typeof args.appDir === 'string' && args.appDir ? args.appDir : cwd;
|
|
353
493
|
try {
|
|
354
|
-
const result =
|
|
494
|
+
const result = isKnowledgeTool
|
|
495
|
+
? name === 'init'
|
|
496
|
+
? await initText(docsDeps)
|
|
497
|
+
: name === 'docs'
|
|
498
|
+
? await searchDocs(docsDeps, args)
|
|
499
|
+
: await runSourceTool(sourceDeps, args)
|
|
500
|
+
: await runners[name](appDir);
|
|
501
|
+
// Knowledge tools return a markdown string; introspection tools return
|
|
502
|
+
// a JSON-serialisable object.
|
|
503
|
+
const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
355
504
|
return rpcResult(id, {
|
|
356
|
-
content: [{ type: 'text', text
|
|
505
|
+
content: [{ type: 'text', text }],
|
|
357
506
|
});
|
|
358
507
|
} catch (e) {
|
|
359
508
|
// A tool failure is an MCP tool-result error (isError), not a transport
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webjsdev/cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "webjs CLI - dev, start, create, db",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"bin",
|
|
11
11
|
"lib",
|
|
12
12
|
"templates",
|
|
13
|
-
"README.md"
|
|
13
|
+
"README.md",
|
|
14
|
+
"resources"
|
|
14
15
|
],
|
|
15
16
|
"dependencies": {
|
|
16
17
|
"@webjsdev/server": "^0.8.0",
|
|
@@ -35,5 +36,9 @@
|
|
|
35
36
|
],
|
|
36
37
|
"engines": {
|
|
37
38
|
"node": ">=24.0.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"prepack": "node scripts/copy-mcp-resources.js",
|
|
42
|
+
"postpack": "node scripts/clean-mcp-resources.js"
|
|
38
43
|
}
|
|
39
44
|
}
|