@webjsdev/mcp 0.1.18 → 0.1.19

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/src/mcp.js CHANGED
@@ -23,19 +23,12 @@
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';
26
+ import { initText } from './mcp-init.js';
27
+ import { resolveFrameworkRoots, runSourceTool, listDir } from './mcp-source.js';
36
28
  // The export lookup behind the `source` tool's `export` arg, re-exported for
37
29
  // `webjs source <Export>` in the CLI (#1623), which shells the same text.
38
- export { lookupExport, resolveFrameworkRoots } from './mcp-source.js';
30
+ export { lookupExport, resolveFrameworkRoots, listDir, listSources } from './mcp-source.js';
31
+ export { featureCatalog, FEATURE_CATALOG } from './catalog.js';
39
32
  import { projectRoutes } from './routes-report.js';
40
33
 
41
34
  const PROTOCOL_VERSION = '2024-11-05';
@@ -56,10 +49,9 @@ const RESERVED_CONFIG = new Set(['method', 'cache', 'tags', 'invalidates', 'vali
56
49
  /**
57
50
  * The `appDir` property, shared by every tool that is scoped to one app.
58
51
  *
59
- * `init` and `docs` carry it too, not just the introspection tools: the docs
60
- * corpus is resolved from the app being asked about (#1319), so a tool that did
61
- * not ADVERTISE `appDir` could never receive one from a conforming client, and
62
- * the app-corpus rung would be reachable only from hand-written JSON-RPC.
52
+ * `init` carries it too: it compares the app's installed `@webjsdev/mcp` with
53
+ * the running server's to warn about a stale server (#1319), and a tool that did
54
+ * not ADVERTISE `appDir` could never receive one from a conforming client.
63
55
  */
64
56
  const APPDIR_PROP = {
65
57
  type: 'string',
@@ -73,33 +65,13 @@ const APPDIR_SCHEMA = {
73
65
  required: [],
74
66
  };
75
67
 
76
- /** `init` takes only the optional appDir, which selects whose docs corpus it reports. */
68
+ /** `init` takes only the optional appDir, whose installed server version it compares. */
77
69
  const INIT_SCHEMA = {
78
70
  type: 'object',
79
71
  properties: {
80
72
  appDir: {
81
73
  ...APPDIR_PROP,
82
- description: "App directory whose docs corpus to read. Defaults to the server cwd.",
83
- },
84
- },
85
- required: [],
86
- };
87
-
88
- /** `docs` takes an optional topic OR a free-text query, plus the optional appDir. */
89
- const DOCS_SCHEMA = {
90
- type: 'object',
91
- properties: {
92
- appDir: {
93
- ...APPDIR_PROP,
94
- description: "App directory whose docs corpus to read. Defaults to the server cwd.",
95
- },
96
- topic: {
97
- type: 'string',
98
- description: 'A doc name (e.g. components, recipes, lit-muscle-memory-gotchas, AGENTS). Returns the full doc.',
99
- },
100
- query: {
101
- type: 'string',
102
- description: 'Free-text search across all webjs docs. Returns matching lines with their source.',
74
+ description: 'App directory the agent is editing, checked for a newer @webjsdev/mcp than this server. Defaults to the server cwd.',
103
75
  },
104
76
  },
105
77
  required: [],
@@ -148,36 +120,29 @@ const SOURCE_SCHEMA = {
148
120
  },
149
121
  package: {
150
122
  type: 'string',
151
- description: 'Limit a no-args listing to one package (core, server, cli, intellisense, ui).',
123
+ description: 'List one package\'s source entry points (core, server, cli, intellisense, ui).',
152
124
  },
153
125
  },
154
126
  required: [],
155
127
  };
156
128
 
157
129
  /**
158
- * The tools. The four introspection tools project an EXISTING @webjsdev/server
159
- * function (read-only, appDir-scoped). `init` + `docs` (#376) surface the
160
- * framework knowledge: `init` is the "read first" mental-model primer, `docs`
161
- * retrieves a doc by topic or searches the corpus. Descriptions are crisp so a
162
- * model picks the right tool without reading source.
130
+ * The tools. The introspection tools project an EXISTING @webjsdev/server
131
+ * function (read-only, appDir-scoped). `init` returns the WebJs brief, `source`
132
+ * reads the framework's own source, and `ui` reads the @webjsdev/ui kit.
133
+ * Descriptions are crisp so a model picks the right tool without reading source.
163
134
  */
164
135
  const TOOL_DEFS = [
165
136
  {
166
137
  name: 'init',
167
138
  description:
168
- '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.',
139
+ 'READ THIS FIRST before writing or editing a WebJs app. Returns the WebJs brief (Next.js App Router routing plus Lit component APIs, imported from @webjsdev/*, with the few differences that matter) and what each other tool is for. Read-only.',
169
140
  inputSchema: INIT_SCHEMA,
170
141
  },
171
- {
172
- name: 'docs',
173
- description:
174
- '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.',
175
- inputSchema: DOCS_SCHEMA,
176
- },
177
142
  {
178
143
  name: 'source',
179
144
  description:
180
- '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.',
145
+ '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, `package` to list one package\'s entry points, or NO ARGS for the feature catalogue: what WebJs provides by need (auth, sessions, caching, rate limits, uploads, realtime, SEO, test helpers), each name one `export` call away from its signature and example. Call it before hand-writing any of those. Read-only.',
181
146
  inputSchema: SOURCE_SCHEMA,
182
147
  },
183
148
  {
@@ -204,12 +169,6 @@ const TOOL_DEFS = [
204
169
  'List registered custom-element tags: tag name, defining file, and class name. Read-only.',
205
170
  inputSchema: APPDIR_SCHEMA,
206
171
  },
207
- {
208
- name: 'list_elision',
209
- description:
210
- 'Report the display-only elision verdict: every component module with whether it is elided (the browser never downloads it) or shipped plus the evidence that produced the verdict, every page/layout route module as inert / import-only / shipped (with the first client-effecting blocker that pins it), and any ORPHAN component class, one that either has no registration call at all or registers a computed tag, which the scanner cannot see either way, so it always loses its elision verdict, its tag-to-module registry entry, and its preload hint (and never upgrades at all with no registration call, or when a computed-tag class has no importer that ships whole). Identical to `webjs elision --json`. Read-only.',
211
- inputSchema: APPDIR_SCHEMA,
212
- },
213
172
  {
214
173
  name: 'check',
215
174
  description:
@@ -345,7 +304,7 @@ export function extractActionConfig(src) {
345
304
  * JSON-serialisable projection of an existing server data function. All are
346
305
  * read-only.
347
306
  *
348
- * @param {{ buildRouteTable: Function, buildActionIndex: Function, hashFile: Function, scanComponents: Function, analyzeAppElision: Function, checkConventions: Function, projectCheck: Function, readFile: Function }} deps
307
+ * @param {{ buildRouteTable: Function, buildActionIndex: Function, hashFile: Function, scanComponents: Function, checkConventions: Function, projectCheck: Function, readFile: Function }} deps
349
308
  */
350
309
  export function makeToolRunners(deps) {
351
310
  const {
@@ -353,7 +312,6 @@ export function makeToolRunners(deps) {
353
312
  buildActionIndex,
354
313
  hashFile,
355
314
  scanComponents,
356
- analyzeAppElision,
357
315
  checkConventions,
358
316
  projectCheck,
359
317
  readFile,
@@ -417,15 +375,6 @@ export function makeToolRunners(deps) {
417
375
  .sort((a, b) => a.tag.localeCompare(b.tag));
418
376
  },
419
377
 
420
- async list_elision(appDir) {
421
- // The whole report IS the contract: `analyzeAppElision` already returns
422
- // an app-relative, sorted, JSON-serializable object, so unlike
423
- // `list_routes` there is no projector leaf here and nothing to keep in
424
- // sync. `webjs elision --json` prints this same object, and a drift test
425
- // asserts the two are equal.
426
- return analyzeAppElision(appDir);
427
- },
428
-
429
378
  async check(appDir) {
430
379
  const violations = await checkConventions(appDir);
431
380
  return projectCheck(violations);
@@ -563,7 +512,6 @@ export async function runMcpServer(opts) {
563
512
  buildActionIndex: server.buildActionIndex,
564
513
  hashFile: server.hashFile,
565
514
  scanComponents: server.scanComponents,
566
- analyzeAppElision: server.analyzeAppElision,
567
515
  checkConventions: check.checkConventions,
568
516
  projectCheck,
569
517
  readFile,
@@ -571,47 +519,16 @@ export async function runMcpServer(opts) {
571
519
  }
572
520
  const runners = makeToolRunners(deps);
573
521
 
574
- // The docs corpus deps for the knowledge layer (#376): resources / prompts /
575
- // init / docs. Injectable for tests; otherwise resolved from the app's own
576
- // installed corpus, the bundled (published) snapshot, or the repo-root (dev)
577
- // docs, plus node fs.
578
- //
579
- // Resolved PER CALL and memoized by appDir (#1319), not once at boot. `appDir`
580
- // is a per-call tool argument, so a boot-time resolution would pin the corpus
581
- // to the launch directory forever and defeat the app-corpus rung for any host
582
- // that overrides it. `resources/list` and `resources/read` carry no appDir in
583
- // the MCP protocol, so they resolve from `cwd`, which is the same value a
584
- // `tools/call` with no `appDir` argument defaults to; the surfaces can only
585
- // diverge when a caller deliberately asks about a different app.
586
- let docsFs = null;
587
- if (!opts.docsDeps) {
522
+ // The `init` deps: fs readers for the stale-server check, which compares the
523
+ // app's installed @webjsdev/mcp with this server's version (#1319). Resolved
524
+ // per call from the tool's `appDir`. Injectable for tests via `initFs`.
525
+ let initFs = opts.initFs;
526
+ if (!initFs) {
588
527
  const { readFile } = await import('node:fs/promises');
589
- const { readdirSync, existsSync } = await import('node:fs');
590
- docsFs = { listDir: readdirSync, exists: existsSync, readFile };
528
+ const { existsSync } = await import('node:fs');
529
+ initFs = { readFile, exists: existsSync };
591
530
  }
592
- /** @type {Map<string, object>} */
593
- const docsDepsCache = new Map();
594
- /** The docs deps for one appDir. An injected `opts.docsDeps` wins for every appDir. */
595
- const docsDepsFor = (dir) => {
596
- if (opts.docsDeps) return opts.docsDeps;
597
- const key = typeof dir === 'string' ? dir : '';
598
- let built = docsDepsCache.get(key);
599
- if (!built) {
600
- const loc = resolveDocsLocation(import.meta.url, key);
601
- built = {
602
- docsDir: loc.docsDir,
603
- agentsPath: loc.agentsPath,
604
- skillPath: loc.skillPath,
605
- corpusPath: loc.corpusPath,
606
- corpusSource: loc.corpusSource,
607
- appDir: key,
608
- serverVersion: version,
609
- ...docsFs,
610
- };
611
- docsDepsCache.set(key, built);
612
- }
613
- return built;
614
- };
531
+ const initDepsFor = (dir) => ({ ...initFs, appDir: dir, serverVersion: version });
615
532
 
616
533
  // The `source` tool (#378): read the framework's own source from
617
534
  // node_modules/@webjsdev/*/src (no-build, so it is the real JSDoc). Roots are
@@ -619,8 +536,8 @@ export async function runMcpServer(opts) {
619
536
  let sourceDeps = opts.sourceDeps;
620
537
  if (!sourceDeps) {
621
538
  const { readFile } = await import('node:fs/promises');
622
- const { readdirSync, existsSync, realpathSync } = await import('node:fs');
623
- const readdir = (d) => readdirSync(d, { withFileTypes: true }).map((e) => ({ name: e.name, isDir: e.isDirectory() }));
539
+ const { existsSync, realpathSync } = await import('node:fs');
540
+ const readdir = listDir;
624
541
  sourceDeps = {
625
542
  roots: resolveFrameworkRoots(cwd, { exists: existsSync }),
626
543
  readFile,
@@ -657,7 +574,7 @@ export async function runMcpServer(opts) {
657
574
  if (method === 'initialize') {
658
575
  return rpcResult(id, {
659
576
  protocolVersion: PROTOCOL_VERSION,
660
- capabilities: { tools: {}, resources: {}, prompts: {} },
577
+ capabilities: { tools: {} },
661
578
  serverInfo: { name: 'webjs', version },
662
579
  });
663
580
  }
@@ -675,39 +592,12 @@ export async function runMcpServer(opts) {
675
592
  });
676
593
  }
677
594
 
678
- // Knowledge layer (#376): the framework docs as MCP resources.
679
- if (method === 'resources/list') {
680
- return rpcResult(id, { resources: listResources(docsDepsFor(cwd)) });
681
- }
682
- if (method === 'resources/read') {
683
- const uri = ((msg && msg.params) || {}).uri;
684
- try {
685
- const r = await readResource(docsDepsFor(cwd), uri);
686
- return rpcResult(id, { contents: [r] });
687
- } catch (e) {
688
- return rpcError(id, -32602, e && e.message ? e.message : String(e));
689
- }
690
- }
691
-
692
- // Knowledge layer (#376): the recipes as guided-workflow prompts.
693
- if (method === 'prompts/list') {
694
- return rpcResult(id, { prompts: PROMPTS });
695
- }
696
- if (method === 'prompts/get') {
697
- const params = (msg && msg.params) || {};
698
- try {
699
- return rpcResult(id, getPrompt(params.name, params.arguments));
700
- } catch (e) {
701
- return rpcError(id, -32602, e && e.message ? e.message : String(e));
702
- }
703
- }
704
-
705
595
  if (method === 'tools/call') {
706
596
  const params = (msg && msg.params) || {};
707
597
  const name = params.name;
708
598
  const args = params.arguments || {};
709
- // The knowledge tools route to the docs / source layer; they return text.
710
- const isKnowledgeTool = name === 'init' || name === 'docs' || name === 'source';
599
+ // The knowledge tools (the brief, the framework source) return text.
600
+ const isKnowledgeTool = name === 'init' || name === 'source';
711
601
  // `ui` is kit-scoped (not app-scoped), so it is dispatched separately.
712
602
  const isUiTool = name === 'ui';
713
603
  if (!isKnowledgeTool && !isUiTool && !runners[name]) {
@@ -717,10 +607,8 @@ export async function runMcpServer(opts) {
717
607
  try {
718
608
  const result = isKnowledgeTool
719
609
  ? name === 'init'
720
- ? await initText(docsDepsFor(appDir))
721
- : name === 'docs'
722
- ? await searchDocs(docsDepsFor(appDir), args)
723
- : await runSourceTool(sourceDeps, args)
610
+ ? await initText(initDepsFor(appDir))
611
+ : await runSourceTool(sourceDeps, args)
724
612
  : isUiTool
725
613
  ? runUiTool(uiDeps, args)
726
614
  : await runners[name](appDir);