@webjsdev/mcp 0.1.14 → 0.1.15
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/package.json +1 -1
- package/resources/AGENTS.md +98 -34
- package/resources/SKILL.md +57 -20
- package/resources/corpus.json +3 -3
- package/resources/references/auth-and-sessions.md +15 -0
- package/resources/references/built-ins.md +95 -3
- package/resources/references/client-router-and-streaming.md +54 -5
- package/resources/references/components.md +57 -5
- package/resources/references/data-and-actions.md +3 -1
- package/resources/references/module-structure.md +229 -0
- package/resources/references/muscle-memory-gotchas.md +31 -5
- package/resources/references/optimistic-ui.md +23 -11
- package/resources/references/routing-and-pages.md +107 -9
- package/resources/references/runtime.md +24 -5
- package/resources/references/styling.md +49 -4
- package/resources/references/testing.md +13 -0
- package/resources/references/ui-kit.md +39 -3
- package/src/mcp-source.js +148 -6
- package/src/mcp.js +9 -2
package/src/mcp-source.js
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* `src/` entry-point files.
|
|
14
14
|
* - `query`: grep the framework `src/` trees, returning bounded `file:line`
|
|
15
15
|
* hits (with a disclosed cap, no silent truncation).
|
|
16
|
-
* - `path`: read one source file (e.g. `server/src/ssr.js
|
|
16
|
+
* - `path`: read one source file (e.g. `server/src/ssr/head.js`; the bare
|
|
17
|
+
* `server/src/ssr.js` is a barrel over the sibling directory), traversal-guarded
|
|
17
18
|
* to stay inside a resolved framework package root.
|
|
18
19
|
*
|
|
19
20
|
* READ-ONLY and side-effect-free: it only reads files, loads no module, and
|
|
@@ -209,7 +210,7 @@ export async function readSource(deps, path) {
|
|
|
209
210
|
const pkg = segs[0];
|
|
210
211
|
const entry = deps.roots.find((r) => r.pkg === pkg);
|
|
211
212
|
if (!entry) {
|
|
212
|
-
return `Unknown or unresolvable package "${pkg || path}". Resolvable: ${deps.roots.map((r) => r.pkg).join(', ') || '(none)'}. Pass a path like server/src/ssr.js.`;
|
|
213
|
+
return `Unknown or unresolvable package "${pkg || path}". Resolvable: ${deps.roots.map((r) => r.pkg).join(', ') || '(none)'}. Pass a path like server/src/ssr/head.js.`;
|
|
213
214
|
}
|
|
214
215
|
const abs = resolve(entry.root, segs.slice(1).join('/'));
|
|
215
216
|
const srcLabel = entry.src.split(sep).pop();
|
|
@@ -237,17 +238,158 @@ export async function readSource(deps, path) {
|
|
|
237
238
|
}
|
|
238
239
|
}
|
|
239
240
|
|
|
241
|
+
/** Bound one export's rendered declaration (a long interface body is cut here). */
|
|
242
|
+
const MAX_DECL_LINES = 40;
|
|
243
|
+
/** Bound the hits one lookup returns (overloads plus a JS fallback stay well under this). */
|
|
244
|
+
const MAX_EXPORT_HITS = 8;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Collect the JSDoc block (or the `//` run) that ends on the line right above
|
|
248
|
+
* `line`, as the lines themselves, or `[]` when the declaration has no comment.
|
|
249
|
+
* @param {string[]} lines
|
|
250
|
+
* @param {number} line index of the declaration line
|
|
251
|
+
* @returns {string[]}
|
|
252
|
+
*/
|
|
253
|
+
function docAbove(lines, line) {
|
|
254
|
+
let i = line - 1;
|
|
255
|
+
if (i < 0) return [];
|
|
256
|
+
if (lines[i].trim().endsWith('*/')) {
|
|
257
|
+
const end = i;
|
|
258
|
+
while (i >= 0 && !lines[i].trim().startsWith('/**')) {
|
|
259
|
+
if (lines[i].trim().startsWith('/*') && !lines[i].trim().startsWith('/**')) return [];
|
|
260
|
+
i--;
|
|
261
|
+
}
|
|
262
|
+
return i < 0 ? [] : lines.slice(i, end + 1);
|
|
263
|
+
}
|
|
264
|
+
const end = i;
|
|
265
|
+
while (i >= 0 && lines[i].trim().startsWith('//')) i--;
|
|
266
|
+
return i === end ? [] : lines.slice(i + 1, end + 1);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The declaration that starts on `line`: a `function` / `const` / `type`
|
|
271
|
+
* statement runs to its terminating `;` (a `.js` function stops at its opening
|
|
272
|
+
* brace, so the body is never printed); an `interface` / `class` runs to the
|
|
273
|
+
* brace that closes it. Capped at {@link MAX_DECL_LINES}.
|
|
274
|
+
* @param {string[]} lines
|
|
275
|
+
* @param {number} line
|
|
276
|
+
* @param {boolean} isTypes true for a `.d.ts` file
|
|
277
|
+
* @returns {string[]}
|
|
278
|
+
*/
|
|
279
|
+
function declarationFrom(lines, line, isTypes) {
|
|
280
|
+
const first = lines[line];
|
|
281
|
+
const braced = /^export\s+(?:declare\s+)?(?:abstract\s+)?(?:interface|class|enum)\b/.test(first)
|
|
282
|
+
|| (/^export\s+(?:declare\s+)?(?:const|let|var|type)\b/.test(first) && /[{(]\s*$/.test(first) && !/;\s*$/.test(first));
|
|
283
|
+
const out = [];
|
|
284
|
+
let depth = 0;
|
|
285
|
+
// An authored `export const x = (...) => {` is its first line: the value is
|
|
286
|
+
// the implementation, and the doc above it is the contract.
|
|
287
|
+
if (!isTypes && /^export\s+(?:const|let|var)\b/.test(first)) return [first];
|
|
288
|
+
for (let i = line; i < lines.length && out.length < MAX_DECL_LINES; i++) {
|
|
289
|
+
const l = lines[i];
|
|
290
|
+
if (!isTypes && /^export\s+(?:async\s+)?function\b/.test(first)) {
|
|
291
|
+
// A JS function: the signature only, cut at the body's opening brace.
|
|
292
|
+
const cut = l.indexOf('{');
|
|
293
|
+
if (cut >= 0 && (i > line || !/^\s*\/\//.test(l))) { out.push(l.slice(0, cut).trimEnd()); return out; }
|
|
294
|
+
out.push(l);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
out.push(l);
|
|
298
|
+
if (braced) {
|
|
299
|
+
for (const ch of l) { if (ch === '{') depth++; else if (ch === '}') depth--; }
|
|
300
|
+
if (depth <= 0 && /}/.test(l) && i > line) return out;
|
|
301
|
+
if (depth <= 0 && i === line && /}\s*;?\s*$/.test(l)) return out;
|
|
302
|
+
} else {
|
|
303
|
+
// Balanced parens and braces matter for a multi-line signature; angle
|
|
304
|
+
// brackets are not counted (an arrow's `=>` would unbalance them).
|
|
305
|
+
for (const ch of l) { if (ch === '(' || ch === '{') depth++; else if (ch === ')' || ch === '}') depth--; }
|
|
306
|
+
if (depth <= 0 && /;\s*$/.test(l)) return out;
|
|
307
|
+
if (depth <= 0 && !isTypes && /=\s*\(.*\)\s*=>/.test(first) && i === line) return out;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (out.length >= MAX_DECL_LINES) out.push(` ... (cut at ${MAX_DECL_LINES} lines; read the file with \`path\` for the rest)`);
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* `export` mode: ONE export's signature plus its doc comment, so an agent
|
|
316
|
+
* checking a contract pays for the declaration instead of the whole file. The
|
|
317
|
+
* typed declarations win (`index.d.ts` and `src/**\/*.d.ts` under each package
|
|
318
|
+
* root, where `@webjsdev/server` keeps every export with its doc and
|
|
319
|
+
* `@webjsdev/core` keeps them beside the source); the authored `.js` is the
|
|
320
|
+
* fallback when no typed hit exists or none of them carries a doc, since the
|
|
321
|
+
* JSDoc there is the behaviour contract. Overloads are consecutive hits and
|
|
322
|
+
* all print. A miss discloses the searched packages (the #837 rule): nothing
|
|
323
|
+
* resolvable means NOTHING was searched, never "not found".
|
|
324
|
+
*
|
|
325
|
+
* @param {{ roots: Array<{ pkg: string, root: string, src: string }>, readFile: Function, readdir: Function }} deps
|
|
326
|
+
* @param {string} name an export name, e.g. `createAuth`
|
|
327
|
+
* @param {string} [pkgFilter] one of the framework packages, e.g. `server`
|
|
328
|
+
* @returns {Promise<string>}
|
|
329
|
+
*/
|
|
330
|
+
export async function lookupExport(deps, name, pkgFilter) {
|
|
331
|
+
const n = String(name || '').trim();
|
|
332
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(n)) return 'Pass an export name (letters, digits, _ or $), e.g. `createAuth`.';
|
|
333
|
+
const roots = pkgFilter ? deps.roots.filter((r) => r.pkg === pkgFilter) : deps.roots;
|
|
334
|
+
if (!deps.roots.length) {
|
|
335
|
+
return 'No @webjsdev/* source is resolvable here, so NOTHING was searched (run inside a webjs app or the monorepo). This is not the same as "not found".';
|
|
336
|
+
}
|
|
337
|
+
if (!roots.length) {
|
|
338
|
+
return `@webjsdev/${pkgFilter} is not installed/resolvable here. Resolvable: ${deps.roots.map((r) => r.pkg).join(', ')}.`;
|
|
339
|
+
}
|
|
340
|
+
const typesRe = new RegExp(`^export\\s+(?:declare\\s+)?(?:abstract\\s+)?(?:async\\s+)?(?:function\\*?|const|let|var|class|interface|type|enum)\\s+${n.replace(/\$/g, '\\$')}\\b`);
|
|
341
|
+
const jsRe = new RegExp(`^export\\s+(?:async\\s+)?(?:function\\*?|const|let|var|class)\\s+${n.replace(/\$/g, '\\$')}\\b`);
|
|
342
|
+
/** @type {Array<{ pkg: string, rel: string, line: number, doc: string[], decl: string[], types: boolean }>} */
|
|
343
|
+
const hits = [];
|
|
344
|
+
for (const r of roots) {
|
|
345
|
+
const typeFiles = walkSource(r.root, deps).filter((f) => /\.d\.(?:ts|mts|cts)$/.test(f));
|
|
346
|
+
const jsFiles = walkSource(r.src, deps).filter((f) => /\.(?:js|mjs)$/.test(f));
|
|
347
|
+
for (const [files, isTypes] of [[typeFiles, true], [jsFiles, false]]) {
|
|
348
|
+
for (const file of files) {
|
|
349
|
+
let text = '';
|
|
350
|
+
try { text = await deps.readFile(file, 'utf8'); } catch { continue; }
|
|
351
|
+
if (!text.includes(n)) continue;
|
|
352
|
+
const lines = text.split('\n');
|
|
353
|
+
const rel = relative(r.root, file).split(sep).join('/');
|
|
354
|
+
for (let i = 0; i < lines.length; i++) {
|
|
355
|
+
if (!(isTypes ? typesRe : jsRe).test(lines[i])) continue;
|
|
356
|
+
hits.push({ pkg: r.pkg, rel, line: i + 1, doc: docAbove(lines, i), decl: declarationFrom(lines, i, isTypes), types: isTypes });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const searched = deps.roots.map((r) => r.pkg).join(', ');
|
|
362
|
+
if (!hits.length) {
|
|
363
|
+
return `No export named "${n}" in the searched @webjsdev/* packages (${roots.map((r) => r.pkg).join(', ')}${pkgFilter ? `; resolvable: ${searched}` : ''}). Try \`query\` for a substring search, or \`path\` to read a file.`;
|
|
364
|
+
}
|
|
365
|
+
// The typed declarations are the answer; the JS doc is added only when the
|
|
366
|
+
// typed hits say nothing about behaviour (no doc on any of them), or there
|
|
367
|
+
// is no typed hit at all.
|
|
368
|
+
const typed = hits.filter((h) => h.types);
|
|
369
|
+
const typedHasDoc = typed.some((h) => h.doc.length);
|
|
370
|
+
const shown = typed.length && typedHasDoc ? typed : typed.length ? [...typed, ...hits.filter((h) => !h.types && h.doc.length)] : hits;
|
|
371
|
+
const out = [];
|
|
372
|
+
for (const h of shown.slice(0, MAX_EXPORT_HITS)) {
|
|
373
|
+
out.push(`@webjsdev/${h.pkg} ${h.rel}:${h.line}`);
|
|
374
|
+
out.push(...h.doc, ...h.decl, '');
|
|
375
|
+
}
|
|
376
|
+
if (shown.length > MAX_EXPORT_HITS) out.push(`... (${shown.length - MAX_EXPORT_HITS} more declarations; narrow with \`package\`)`);
|
|
377
|
+
return out.join('\n').trimEnd();
|
|
378
|
+
}
|
|
379
|
+
|
|
240
380
|
/**
|
|
241
|
-
* The `source` tool entry point. Dispatches on the args: `
|
|
242
|
-
*
|
|
243
|
-
* `
|
|
381
|
+
* The `source` tool entry point. Dispatches on the args: `export` looks up one
|
|
382
|
+
* export's signature and doc, `path` reads a file, `query` greps, otherwise
|
|
383
|
+
* (or with `package`) lists the packages. PURE given `deps`
|
|
384
|
+
* (`{ roots, readFile, readdir }`).
|
|
244
385
|
*
|
|
245
386
|
* @param {object} deps
|
|
246
|
-
* @param {{ query?: string, path?: string, package?: string }} [args]
|
|
387
|
+
* @param {{ export?: string, query?: string, path?: string, package?: string }} [args]
|
|
247
388
|
* @returns {Promise<string>}
|
|
248
389
|
*/
|
|
249
390
|
export async function runSourceTool(deps, args) {
|
|
250
391
|
const a = args || {};
|
|
392
|
+
if (a.export) return lookupExport(deps, a.export, a.package);
|
|
251
393
|
if (a.path) return readSource(deps, a.path);
|
|
252
394
|
if (a.query) return grepSources(deps, a.query);
|
|
253
395
|
return listSources(deps, a.package);
|
package/src/mcp.js
CHANGED
|
@@ -33,6 +33,9 @@ import {
|
|
|
33
33
|
getPrompt,
|
|
34
34
|
} from './mcp-docs.js';
|
|
35
35
|
import { resolveFrameworkRoots, runSourceTool } from './mcp-source.js';
|
|
36
|
+
// The export lookup behind the `source` tool's `export` arg, re-exported for
|
|
37
|
+
// `webjs source <Export>` in the CLI (#1623), which shells the same text.
|
|
38
|
+
export { lookupExport, resolveFrameworkRoots } from './mcp-source.js';
|
|
36
39
|
import { projectRoutes } from './routes-report.js';
|
|
37
40
|
|
|
38
41
|
const PROTOCOL_VERSION = '2024-11-05';
|
|
@@ -118,9 +121,13 @@ const UI_SCHEMA = {
|
|
|
118
121
|
const SOURCE_SCHEMA = {
|
|
119
122
|
type: 'object',
|
|
120
123
|
properties: {
|
|
124
|
+
export: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
description: 'One export name, e.g. createAuth or optimistic: returns its signature plus the doc comment above it (from the typed declarations, with the authored JSDoc as the fallback), not the whole file. The cheapest way to check a contract.',
|
|
127
|
+
},
|
|
121
128
|
path: {
|
|
122
129
|
type: 'string',
|
|
123
|
-
description: 'A framework source file to read, e.g. server/src/ssr.js or @webjsdev/core/src/render-client.js.',
|
|
130
|
+
description: 'A framework source file to read, e.g. server/src/ssr/head.js or @webjsdev/core/src/render-client/parts.js. The top-level ssr.js / render-client.js paths are barrels; the code is in the sibling directory.',
|
|
124
131
|
},
|
|
125
132
|
query: {
|
|
126
133
|
type: 'string',
|
|
@@ -157,7 +164,7 @@ const TOOL_DEFS = [
|
|
|
157
164
|
{
|
|
158
165
|
name: 'source',
|
|
159
166
|
description:
|
|
160
|
-
'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.',
|
|
167
|
+
'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 `export` for the signature plus doc comment of ONE export (the cheap way to check a contract: `export: "createAuth"`), `path` to read a file (e.g. server/src/ssr/head.js; the bare server/src/ssr.js is a barrel that re-exports from the sibling directory), `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.',
|
|
161
168
|
inputSchema: SOURCE_SCHEMA,
|
|
162
169
|
},
|
|
163
170
|
{
|