@webjsdev/mcp 0.1.17 → 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/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # @webjsdev/mcp
2
2
 
3
- The webjs **Model Context Protocol server** for AI coding agents. A read-only
3
+ The WebJs **Model Context Protocol server** for AI coding agents. A read-only
4
4
  MCP server (newline-delimited JSON-RPC 2.0 over stdio) that gives an agent the
5
- live introspection surface plus the framework knowledge layer it needs while
6
- editing a webjs app.
5
+ WebJs brief, the live introspection surface, and the framework source it needs
6
+ while editing a WebJs app.
7
7
 
8
8
  ## Run it
9
9
 
@@ -19,114 +19,41 @@ no install:
19
19
  }
20
20
  ```
21
21
 
22
- Every webjs scaffold wires this entry automatically. `webjs mcp` (the CLI
22
+ Every WebJs scaffold wires this entry automatically. `webjs mcp` (the CLI
23
23
  subcommand) delegates to this same server, so both routes run identical code.
24
24
 
25
25
  ## What it exposes
26
26
 
27
+ - **`init` tool**: the WebJs brief, the short list of what differs from Next.js
28
+ App Router routing and Lit components, plus what each other tool is for.
29
+ WebJs is designed so Lit and Next.js habits produce correct code, and where
30
+ one cannot, `webjs check` and the runtime refuse it with the fix in the
31
+ message, so the brief is the whole of the agent-facing documentation. The
32
+ human docs live at https://webjs.dev/docs. `init` prepends a warning when the
33
+ app's installed `@webjsdev/mcp` is strictly newer than the running server's,
34
+ naming every way the server may have been started (a global install, the
35
+ `npx` package cache, `@webjsdev/cli` behind `webjs mcp`, or a checkout).
27
36
  - **Introspection tools** (read-only, scoped to an `appDir`): `list_routes`,
28
37
  `list_actions` (RPC endpoints plus the full data contract: HTTP verb, cache
29
38
  config, and boolean flags for tags/invalidates/validate/middleware; reserved
30
39
  config exports are excluded from the callable-action list), `list_components`,
31
- `list_elision` (the display-only elision verdict: which component modules the
32
- browser never downloads, the evidence behind each one that ships, every
33
- page/layout as inert / import-only / shipped, and any orphan class that gets
34
- no verdict at all; identical to `webjs elision --json`),
35
- `check` (the structured `webjs check` violations). Each projects an existing
36
- `@webjsdev/server` data function and mutates nothing.
37
- - **Knowledge layer**: an `init` mental-model primer (which also names the docs
38
- corpus it is serving, and warns when this server's copy looks stale against
39
- the app's), a `docs` retrieval tool, MCP `resources` (the skill references +
40
- `SKILL.md` + `AGENTS.md` as `webjs-docs://*`), and `prompts` (the recipes as
41
- guided workflows).
40
+ `check` (the structured `webjs check` violations, each with its fix). Each
41
+ projects an existing `@webjsdev/server` data function and mutates nothing.
42
42
  - **`source` tool**: reads the framework's own no-build source from
43
- `node_modules/@webjsdev/*/src` (read-only, traversal-guarded).
44
- - **`ui` tool**: the `@webjsdev/ui` kit inventory (no args) or one component's
45
- helper signatures, paste-ready structural example, a11y header, and deps (pass
46
- `name`). Kit-scoped (not `appDir`-scoped); shares one projector with
47
- `webjsui view`.
48
-
49
- ## Which docs you get
50
-
51
- The corpus is resolved in three rungs, highest first:
52
-
53
- 1. `<appDir>/node_modules/@webjsdev/mcp/resources`, the app's own installed copy.
54
- 2. `<pkg>/resources`, this server's bundled snapshot, so `npx @webjsdev/mcp` is
55
- self-contained.
56
- 3. Whatever the server resolves locally, which is the repo-root skill on the
57
- monorepo dev path. This rung is an unconditional fallback rather than a
58
- checkout probe, so a published install whose `resources/` bundle is missing
59
- lands here too, with an empty corpus. That is why neither the corpus line nor
60
- the warning asserts a checkout.
61
-
62
- The app's copy wins because it is version-matched to the framework you are
63
- editing, and so is the only corpus that can be correct about that app. This
64
- matters most for a GLOBAL install (`npm i -g` / `bun add -g`), which otherwise
65
- keeps serving the docs it was published with forever, contradicting the copy
66
- sitting in your own `node_modules`. That is a real incident, not a hypothetical:
67
- a server published one day before the client router stopped needing an explicit
68
- `import '@webjsdev/core/client-router'` kept teaching that import for months, and
69
- an agent following it wrote dead imports into three layouts, each of which the
70
- elision analyser then correctly refused to strip.
71
-
72
- `appDir` is a per-call argument, so the corpus follows it. `resources/list` and
73
- `resources/read` carry no `appDir` in the MCP protocol and resolve from the
74
- server's working directory, which is what a `tools/call` with no `appDir`
75
- defaults to, so in the ordinary case every surface reads one corpus.
76
-
77
- `init` names the corpus it served on its first line, and prepends a warning when
78
- the app's installed `@webjsdev/mcp` is strictly newer than the running server's:
79
-
80
- ```
81
- Warning: this MCP server is @webjsdev/mcp@0.1.4, but this app has @webjsdev/mcp@0.1.12,
82
- so the server may be stale. The docs below come from this app's own copy, so they
83
- match it; update the server so its TOOLS match too. Update whichever copy runs this
84
- server: a global install (npm i -g @webjsdev/mcp@latest, or bun add -g @webjsdev/mcp),
85
- the package cache behind npx @webjsdev/mcp, @webjsdev/cli when the server is started
86
- as webjs mcp, or the checkout it runs from.
87
- Docs corpus: @webjsdev/mcp@0.1.12, copied from webjsdev/webjs e5806e2 on 2026-08-08.
88
- ```
89
-
90
- It warns rather than refusing to answer, because a knowledge tool that declines
91
- sends the agent back to its training data, which is what the caveat exists to
92
- correct. Equal versions, an older app, and an unreadable app manifest are all
93
- silent.
94
-
95
- Two details the wording is deliberate about. It does not name a global install as
96
- the thing to fix, because nothing observes how the server was started and the two
97
- shipped configurations are an `npx` invocation (whose staleness is a package
98
- cache) and `webjs mcp` (whose staleness is the CLI's own dependency). And it does
99
- not promise the docs below came from the app: the warning reads the app's
100
- `package.json` while the corpus rung probes that install's `resources/`, so a
101
- workspace-linked install can have a manifest to compare and no corpus to serve.
102
- In that case the corpus falls through to the server's snapshot and the warning
103
- says so, rather than contradicting the corpus line printed under it.
104
-
105
- ## The build stamp
106
-
107
- The bundle is a snapshot frozen at publish time, so a published tarball keeps
108
- serving the docs as they read on the day it was cut. `prepack` stamps it with
109
- `resources/corpus.json` so the snapshot can say which docs it holds:
110
-
111
- ```json
112
- {
113
- "package": "@webjsdev/mcp",
114
- "version": "0.1.12",
115
- "sha": "e5806e2400000000000000000000000000000000",
116
- "copiedAt": "2026-08-08T09:14:22.031Z"
117
- }
118
- ```
119
-
120
- `sha` is the full commit the docs were copied from, so it resolves straight to a
121
- GitHub diff. Every field is `null` rather than a plausible-looking default when
122
- it cannot be established, so a consumer can always tell a real answer from no
123
- answer: `sha` when the source tree is not itself a git checkout root, and
124
- `package` / `version` when the manifest cannot be read. None of those fails the
125
- publish. A dev checkout has no bundle and so no stamp.
126
-
127
- The SHA is deliberately refused when the tree merely SITS inside some other
128
- checkout, because `git rev-parse` walks up to an ancestor and would otherwise
129
- report an unrelated repository's HEAD as the commit these docs came from. That
130
- answer is a well-formed SHA, so nothing downstream could catch it.
43
+ `node_modules/@webjsdev/*/src` (read-only, traversal-guarded). Pass `export`
44
+ for one export's signature plus doc comment, the cheapest way to check a
45
+ contract.
46
+ - **`ui` tool**: the `@webjsdev/ui` kit. With no args it returns the
47
+ component inventory plus the block and chart NAMES grouped by category (a
48
+ block is a ready-made view you install and then edit: a login or signup
49
+ page, a sidebar shell, a dashboard, a chart). `type` (`ui` / `block` /
50
+ `chart` / `all`) and `category` (`login`, `sidebar`, `area`, ...) list the
51
+ matches with one-line descriptions. `name` returns one component's helper
52
+ signatures, paste-ready structural example, a11y header and deps, or one
53
+ block's description, dependencies, install command
54
+ (`npx @webjsdev/ui add <name>`) and files with their targets; `source: true`
55
+ adds a block's sources. Everything it returns is baked plain Tailwind, never
56
+ a `cn-*` name, and a test caps the no-args payload size. Kit-scoped (not
57
+ `appDir`-scoped); shares one projector with `webjsui view`.
131
58
 
132
59
  STDOUT is the JSON-RPC channel; every diagnostic goes to stderr.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@webjsdev/mcp",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
- "description": "The Model Context Protocol server for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Gives AI coding agents live app introspection over routes, actions, components, and convention checks, plus a knowledge layer of docs, recipes, and framework source.",
5
+ "description": "The Model Context Protocol server for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Gives AI coding agents the WebJs brief, live app introspection over routes, actions, components, and convention checks, and the framework source.",
6
6
  "bin": {
7
7
  "webjs-mcp": "bin/webjs-mcp.js"
8
8
  },
@@ -21,12 +21,11 @@
21
21
  "files": [
22
22
  "bin",
23
23
  "src",
24
- "README.md",
25
- "resources"
24
+ "README.md"
26
25
  ],
27
26
  "dependencies": {
28
- "@webjsdev/server": "^0.8.0",
29
- "@webjsdev/ui": "^0.3.9"
27
+ "@webjsdev/server": "^0.10.0",
28
+ "@webjsdev/ui": "^0.5.0"
30
29
  },
31
30
  "publishConfig": {
32
31
  "access": "public"
@@ -48,9 +47,5 @@
48
47
  ],
49
48
  "engines": {
50
49
  "node": ">=24.0.0"
51
- },
52
- "scripts": {
53
- "prepack": "node scripts/copy-mcp-resources.js",
54
- "postpack": "node scripts/clean-mcp-resources.js"
55
50
  }
56
51
  }
package/src/brief.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The WebJs brief: the whole of what an agent needs to know beyond its Lit and
3
+ * Next.js training data. Lit and Next habits are meant to produce correct WebJs
4
+ * code, and where one cannot, `webjs check` and the runtime refuse it with the
5
+ * fix in the message, so the agent-facing docs are this brief and nothing more.
6
+ *
7
+ * Single source. The MCP `init` tool returns it, and the root `AGENTS.md` plus
8
+ * the scaffold's `packages/cli/templates/AGENTS.md` carry it VERBATIM under the
9
+ * `## The WebJs brief` heading. `test/repo-health/brief-parity.test.mjs`
10
+ * asserts all three agree, so edit it here and paste it there.
11
+ *
12
+ * @module brief
13
+ */
14
+
15
+ /** The heading both `AGENTS.md` files place the brief under. */
16
+ export const BRIEF_HEADING = '## The WebJs brief';
17
+
18
+ /** The brief itself, as markdown, without its heading. */
19
+ export const BRIEF = [
20
+ 'WebJs is a no-build full-stack framework. Write it the way you write Next.js',
21
+ 'routing and Lit components, with the differences below.',
22
+ '',
23
+ '- Routing follows the Next.js App Router: `app/page.ts`, `layout.ts`,',
24
+ ' `route.ts`, `middleware.ts`, `[param]`, `[...slug]`, `(group)`,',
25
+ ' `loading.ts`, `error.ts`, `not-found.ts`, `metadata` and',
26
+ ' `generateMetadata`, and server actions.',
27
+ '- Components use Lit\'s APIs (`html`, `css`, reactive properties through',
28
+ ' `static properties` plus `declare` or the `WebComponent({ ... })` factory,',
29
+ ' the lifecycle, directives, context, task), imported from `@webjsdev/core`',
30
+ ' and its `/directives`, `/context`, `/task` and `/signals` subpaths. The base',
31
+ ' class is `WebComponent`. Register with `Class.register(\'my-tag\')` or',
32
+ ' `customElements.define`.',
33
+ '- Components render light DOM by default and `<slot>` works there too. Keep',
34
+ ' them light: page styles such as Tailwind and the UI kit do not reach into a',
35
+ ' shadow root. Use `static shadow = true` only when light DOM cannot work.',
36
+ '- TypeScript must be erasable, so no decorators and no enums.',
37
+ '- Server-only code and server actions live in `*.server.ts` files. With',
38
+ ' `\'use server\'` at the top, their exports are callable from components and',
39
+ ' bindable as `<form action=${fn}>`. A failed form action re-renders the page',
40
+ ' with its result on `actionData`.',
41
+ '- `cookies()`, `headers()` and `revalidatePath()` come from `@webjsdev/server`,',
42
+ ' and so does `createAuth()`, which is Auth.js\'s API. Files in `public/` are',
43
+ ' served from the site root.',
44
+ '- Never import `lit`, `next` or `react`.',
45
+ '',
46
+ 'Before hand-writing auth, sessions, caching, rate limits, uploads, realtime or',
47
+ 'SEO, run `npx webjs source`, which lists what WebJs provides for each need.',
48
+ '`npx webjs source <Export>` then prints one export\'s signature and example.',
49
+ 'After each edit run `npx webjs check` and `npx webjs status`. The framework',
50
+ 'source is in `node_modules/@webjsdev/*` (no build, so it is the code that',
51
+ 'runs) and the human docs are at https://webjs.dev/docs.',
52
+ ].join('\n');
package/src/catalog.js ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The feature catalogue: what WebJs provides beyond the brief, grouped by what
3
+ * an app needs (auth, sessions, caching, uploads, realtime, ...). An agent that
4
+ * does not know an export's name cannot ask `source` for it, so before #1686 a
5
+ * feature the brief does not name (rate limiting, file storage, broadcast) was
6
+ * found only by reading the framework source, or hand-written instead.
7
+ *
8
+ * `npx webjs source` with no name prints it, and so does the MCP `source` tool
9
+ * with no arguments. Every name here resolves through `lookupExport`, so the
10
+ * second call (`npx webjs source <Name>`) returns the signature and example;
11
+ * `packages/mcp/test/catalog.test.mjs` holds that, and holds each name to be a
12
+ * real export of the module named for its group.
13
+ *
14
+ * Curated rather than derived: the export lists mix the app-facing surface
15
+ * with framework plumbing (`buildModuleGraph`, `jspmGenerate`), and a need is
16
+ * not a property any export carries. Add an entry when an app-facing export
17
+ * lands; the test fails on one that disappears.
18
+ *
19
+ * @module catalog
20
+ */
21
+
22
+ /**
23
+ * @typedef {{ need: string, names: string[], note?: string }} CatalogGroup
24
+ * @typedef {{ module: string, groups: CatalogGroup[] }} CatalogSection
25
+ */
26
+
27
+ /** @type {CatalogSection[]} */
28
+ export const FEATURE_CATALOG = [
29
+ {
30
+ module: '@webjsdev/server',
31
+ groups: [
32
+ { need: 'Auth', names: ['createAuth', 'Credentials', 'GitHub', 'Google', 'AuthError'], note: "Auth.js's API: auth(), signIn(), signOut(), handlers" },
33
+ { need: 'Sessions and cookies', names: ['session', 'getSession', 'cookies', 'headers'] },
34
+ { need: 'Redirects and errors', names: ['redirect', 'permanentRedirect', 'notFound', 'forbidden', 'unauthorized'] },
35
+ { need: 'Caching', names: ['cache', 'revalidatePath', 'revalidateTag', 'revalidateAll'] },
36
+ { need: 'Rate limits and CORS', names: ['rateLimit', 'cors', 'clientIp'] },
37
+ { need: 'File uploads', names: ['getFileStore', 'setFileStore', 'diskStore', 'generateKey', 'signedUrl', 'verifySignedUrl'] },
38
+ { need: 'Realtime', names: ['broadcast'], note: 'a route.ts exporting WS handles WebSocket connections' },
39
+ { need: 'Streaming responses', names: ['stream', 'streamResponse', 'acceptsStream'] },
40
+ { need: 'Route handlers', names: ['json', 'readBody', 'route'] },
41
+ { need: 'SEO', names: ['sitemap', 'sitemapIndex', 'robots'] },
42
+ { need: 'Env validation', names: ['validateEnv'], note: 'an env.ts schema checks process.env at boot' },
43
+ { need: 'Server tests', names: ['testRequest', 'submitForm', 'invokeActionForTest', 'loginAndGetCookies', 'createRequestHandler'] },
44
+ ],
45
+ },
46
+ {
47
+ module: '@webjsdev/core',
48
+ groups: [
49
+ { need: 'Components', names: ['WebComponent', 'html', 'css', 'nothing', 'unsafeHTML'] },
50
+ { need: 'Directives', names: ['classMap', 'styleMap', 'repeat', 'map', 'when', 'choose', 'ref', 'live', 'until', 'guard', 'keyed', 'ifDefined'] },
51
+ { need: 'State', names: ['signal', 'computed', 'effect', 'Task', 'createContext'] },
52
+ { need: 'Client navigation and data', names: ['navigate', 'refreshPage', 'revalidate', 'optimistic', 'connectWS'] },
53
+ { need: 'Streaming and assets', names: ['Suspense', 'asset', 'cspNonce'] },
54
+ ],
55
+ },
56
+ {
57
+ module: '@webjsdev/core/testing',
58
+ groups: [
59
+ { need: 'Component tests', names: ['fixture', 'ssrFixture', 'waitForUpdate', 'click'] },
60
+ ],
61
+ },
62
+ ];
63
+
64
+ /**
65
+ * The catalogue as the text `npx webjs source` and the MCP `source` tool print.
66
+ * @param {CatalogSection[]} [catalog]
67
+ * @returns {string}
68
+ */
69
+ export function featureCatalog(catalog = FEATURE_CATALOG) {
70
+ const out = [
71
+ 'What WebJs provides, by need. `npx webjs source <Name>` (or the MCP `source`',
72
+ 'tool with `export`) prints one name\'s signature and a working example. Use',
73
+ 'these before writing your own.',
74
+ ];
75
+ for (const section of catalog) {
76
+ out.push('', section.module);
77
+ for (const g of section.groups) {
78
+ out.push(` ${g.need}: ${g.names.join(', ')}${g.note ? ` (${g.note})` : ''}`);
79
+ }
80
+ }
81
+ return out.join('\n');
82
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * The `init` tool of `webjs mcp`: the one thing an agent reads before it writes
3
+ * WebJs code.
4
+ *
5
+ * It returns the WebJs brief (see `brief.js`) plus a map of the server's other
6
+ * tools. There is no docs corpus behind it any more. Lit and Next.js habits are
7
+ * meant to produce correct WebJs code, and where one cannot, `webjs check` and
8
+ * the runtime refuse it with the fix in the message, so the brief is the whole
9
+ * of the agent-facing documentation. For an exact contract the agent reads the
10
+ * framework source, which is no-build and therefore the code that runs (the
11
+ * `source` tool).
12
+ *
13
+ * Pure given its injected `{ appDir, serverVersion, readFile, exists }` deps, so
14
+ * it is testable in-process without touching the real filesystem.
15
+ *
16
+ * @module mcp-init
17
+ */
18
+
19
+ import { join } from 'node:path';
20
+
21
+ import { BRIEF, BRIEF_HEADING } from './brief.js';
22
+
23
+ /**
24
+ * Compare two `major.minor.patch` versions, returning `-1` / `0` / `1`.
25
+ *
26
+ * Dependency-free by design (this package ships zero dependencies), and
27
+ * deliberately coarse: any prerelease or build suffix is stripped before
28
+ * comparing, a missing segment reads as `0`, and a non-numeric segment reads as
29
+ * `0`. Its only consumer is the staleness warning, where the question is "is the
30
+ * app's copy meaningfully newer", so ranking two prereleases of one patch is not
31
+ * a case that needs an answer.
32
+ *
33
+ * @param {string} a
34
+ * @param {string} b
35
+ * @returns {-1 | 0 | 1}
36
+ */
37
+ export function compareVersions(a, b) {
38
+ const parts = (v) => String(v == null ? '' : v).trim().split(/[-+]/)[0].split('.');
39
+ const pa = parts(a);
40
+ const pb = parts(b);
41
+ for (let i = 0; i < 3; i++) {
42
+ const x = Number(pa[i]);
43
+ const y = Number(pb[i]);
44
+ const nx = Number.isFinite(x) ? x : 0;
45
+ const ny = Number.isFinite(y) ? y : 0;
46
+ if (nx !== ny) return nx < ny ? -1 : 1;
47
+ }
48
+ return 0;
49
+ }
50
+
51
+ /**
52
+ * The `@webjsdev/mcp` version installed in `appDir`, or `null` when that cannot
53
+ * be known (no app dir, no install, an unreadable or unparseable manifest, no
54
+ * `version` field).
55
+ *
56
+ * @param {string} [appDir]
57
+ * @param {{ exists?: (p: string) => boolean, readFile: (p: string, enc: string) => Promise<string> }} deps
58
+ * @returns {Promise<string | null>}
59
+ */
60
+ export async function readAppMcpVersion(appDir, deps) {
61
+ if (typeof appDir !== 'string' || !appDir) return null;
62
+ const manifest = join(appDir, 'node_modules', '@webjsdev', 'mcp', 'package.json');
63
+ try {
64
+ if (deps.exists && !deps.exists(manifest)) return null;
65
+ const version = JSON.parse(await deps.readFile(manifest, 'utf8')).version;
66
+ return typeof version === 'string' && version ? version : null;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * The staleness warning, or `null` when there is nothing to warn about.
74
+ *
75
+ * Fires only when the app's installed `@webjsdev/mcp` is strictly NEWER than the
76
+ * running server's (a globally installed server lags the app it is asked
77
+ * about, #1319). Equal or lower is silent. It warns rather than failing, since
78
+ * a stale brief and stale tools still beat no answer.
79
+ *
80
+ * It names every way the server may have been started rather than guessing one,
81
+ * because nothing here can observe it: a global install, the package cache
82
+ * behind `npx @webjsdev/mcp`, `@webjsdev/cli` when started as `webjs mcp`, or a
83
+ * checkout.
84
+ *
85
+ * @param {{ appDir?: string, serverVersion?: string, readFile: Function, exists?: Function }} deps
86
+ * @returns {Promise<string | null>}
87
+ */
88
+ export async function staleWarning(deps) {
89
+ const server = deps.serverVersion;
90
+ // '0.0.0' is the sentinel both entry points fall back to when they cannot read
91
+ // their own manifest, so it means "unknown", not "older than everything".
92
+ if (!server || server === '0.0.0') return null;
93
+ const app = await readAppMcpVersion(deps.appDir, deps);
94
+ if (!app || compareVersions(app, server) <= 0) return null;
95
+ return (
96
+ `Warning: this MCP server is @webjsdev/mcp@${server}, but this app has @webjsdev/mcp@${app}, ` +
97
+ 'so the brief and the tools below may be stale. ' +
98
+ 'Update whichever copy runs this server: a global install (npm i -g @webjsdev/mcp@latest, ' +
99
+ 'or bun add -g @webjsdev/mcp), the package cache behind npx @webjsdev/mcp, ' +
100
+ '@webjsdev/cli when the server is started as webjs mcp, or the checkout it runs from.'
101
+ );
102
+ }
103
+
104
+ /** What the server's other tools are for, so the agent reaches for the right one. */
105
+ const TOOL_MAP = [
106
+ '## The tools on this server',
107
+ '',
108
+ '- Before hand-writing auth, sessions, caching, rate limits, uploads, realtime',
109
+ ' or SEO, call `source` with no arguments: it lists what WebJs provides.',
110
+ '- For an exact contract, call `source` with `export` set to one export name',
111
+ ' (its signature plus doc comment), `path` to read a framework file, or',
112
+ ' `query` to grep the `@webjsdev/*` source.',
113
+ '- To see the live app, call `list_routes`, `list_actions` and',
114
+ ' `list_components`.',
115
+ '- To validate an edit, call `check`. Every violation carries its fix.',
116
+ '- Before hand-writing a page, a form, a dashboard or a chart, call `ui` to',
117
+ ' find a ready-made block in the `@webjsdev/ui` kit.',
118
+ ].join('\n');
119
+
120
+ /**
121
+ * The `init` tool output: an optional staleness warning, the brief, and the
122
+ * tool map.
123
+ *
124
+ * @param {{ appDir?: string, serverVersion?: string, readFile: Function, exists?: Function }} deps
125
+ * @returns {Promise<string>}
126
+ */
127
+ export async function initText(deps) {
128
+ const warning = await staleWarning(deps);
129
+ return [
130
+ '# WebJs: read first',
131
+ '',
132
+ ...(warning ? [warning, ''] : []),
133
+ BRIEF_HEADING,
134
+ '',
135
+ BRIEF,
136
+ '',
137
+ TOOL_MAP,
138
+ ].join('\n');
139
+ }
package/src/mcp-source.js CHANGED
@@ -27,6 +27,8 @@
27
27
 
28
28
  import { createRequire } from 'node:module';
29
29
  import { join, resolve, sep, relative } from 'node:path';
30
+ import { readdirSync, statSync } from 'node:fs';
31
+ import { featureCatalog } from './catalog.js';
30
32
 
31
33
  /** The published framework packages whose source an agent may want to read. */
32
34
  export const FRAMEWORK_PACKAGES = ['core', 'server', 'cli', 'intellisense', 'ui'];
@@ -91,6 +93,25 @@ export function resolveFrameworkRoots(cwd, fsDeps) {
91
93
  * @param {{ readdir: (d: string) => Array<{ name: string, isDir: boolean }> }} deps
92
94
  * @returns {string[]}
93
95
  */
96
+ /**
97
+ * The real directory listing `walkSource` and the source tool read, following
98
+ * a symlinked directory the way the module resolver does.
99
+ * @param {string} dir
100
+ * @returns {Array<{ name: string, isDir: boolean }>}
101
+ */
102
+ export function listDir(dir) {
103
+ return readdirSync(dir, { withFileTypes: true }).map((e) => {
104
+ let isDir = e.isDirectory();
105
+ // A symlinked directory is a directory: pnpm and workspace installs link
106
+ // a package (or its `src`) rather than copying it, and a Dirent of a link
107
+ // answers isDirectory() false, which skipped everything under it (#1686).
108
+ if (!isDir && e.isSymbolicLink()) {
109
+ try { isDir = statSync(join(dir, e.name)).isDirectory(); } catch { /* dangling */ }
110
+ }
111
+ return { name: e.name, isDir };
112
+ });
113
+ }
114
+
94
115
  export function walkSource(dir, deps) {
95
116
  /** @type {string[]} */
96
117
  const files = [];
@@ -114,21 +135,25 @@ export function walkSource(dir, deps) {
114
135
  }
115
136
 
116
137
  /**
117
- * No-args / `package` mode: list the resolved packages and their `src/`
118
- * top-level files (the entry points), so the agent has a map to grep or read.
138
+ * `package` mode: list the resolved packages and their `src/` top-level files
139
+ * (the entry points), so the agent has a map to grep or read. `readHint` is
140
+ * the next step the caller offers, since the MCP tool and the CLI read the
141
+ * listing with different commands.
119
142
  *
120
143
  * @param {{ roots: Array<{ pkg: string, root: string, src: string }>, readdir: Function }} deps
121
144
  * @param {string} [pkgFilter]
145
+ * @param {{ readHint?: string }} [opts]
122
146
  * @returns {string}
123
147
  */
124
- export function listSources(deps, pkgFilter) {
148
+ export function listSources(deps, pkgFilter, opts = {}) {
149
+ const readHint = opts.readHint ?? 'Read with `source({ path })` or search with `source({ query })`.';
125
150
  const roots = pkgFilter ? deps.roots.filter((r) => r.pkg === pkgFilter) : deps.roots;
126
151
  if (!roots.length) {
127
152
  return pkgFilter
128
153
  ? `@webjsdev/${pkgFilter} is not installed/resolvable here. Resolvable: ${deps.roots.map((r) => r.pkg).join(', ') || '(none)'}`
129
154
  : 'No @webjsdev/* packages resolvable from here (run inside a webjs app or the monorepo).';
130
155
  }
131
- 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 })`.', ''];
156
+ const lines = [`webjs framework authored source (buildless; server-side this runs directly, and core ships a built browser dist/ that is excluded here). ${readHint}`, ''];
132
157
  for (const r of roots) {
133
158
  const dirName = r.src.split(sep).pop(); // 'src' for most, 'lib' for cli
134
159
  let entries = [];
@@ -325,9 +350,10 @@ function declarationFrom(lines, line, isTypes) {
325
350
  * @param {{ roots: Array<{ pkg: string, root: string, src: string }>, readFile: Function, readdir: Function }} deps
326
351
  * @param {string} name an export name, e.g. `createAuth`
327
352
  * @param {string} [pkgFilter] one of the framework packages, e.g. `server`
353
+ * @param {boolean} [caseRetried] internal: the case-swapped retry is already this call
328
354
  * @returns {Promise<string>}
329
355
  */
330
- export async function lookupExport(deps, name, pkgFilter) {
356
+ export async function lookupExport(deps, name, pkgFilter, caseRetried = false) {
331
357
  const n = String(name || '').trim();
332
358
  if (!/^[A-Za-z_$][\w$]*$/.test(n)) return 'Pass an export name (letters, digits, _ or $), e.g. `createAuth`.';
333
359
  const roots = pkgFilter ? deps.roots.filter((r) => r.pkg === pkgFilter) : deps.roots;
@@ -360,6 +386,15 @@ export async function lookupExport(deps, name, pkgFilter) {
360
386
  }
361
387
  const searched = deps.roots.map((r) => r.pkg).join(', ');
362
388
  if (!hits.length) {
389
+ // The name as written with another case: a convention is written lower
390
+ // camel (`generateMetadata`) and its type upper (`GenerateMetadata`).
391
+ const other = n[0] === n[0].toLowerCase() ? n[0].toUpperCase() + n.slice(1) : n[0].toLowerCase() + n.slice(1);
392
+ if (other !== n && !caseRetried) {
393
+ const cased = await lookupExport(deps, other, pkgFilter, true);
394
+ if (cased.startsWith('@webjsdev/')) return `"${n}" is not an export; its type is ${other}:\n${cased}`;
395
+ }
396
+ const members = await lookupMember(deps, roots, n);
397
+ if (members) return members;
363
398
  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
399
  }
365
400
  // The typed declarations are the answer; the JS doc is added only when the
@@ -377,10 +412,60 @@ export async function lookupExport(deps, name, pkgFilter) {
377
412
  return out.join('\n').trimEnd();
378
413
  }
379
414
 
415
+ /**
416
+ * A name that is no export but a MEMBER of an exported interface or class
417
+ * (`actionData` on `PageProps`, `fieldErrors` on `FormActionData`). Agents look
418
+ * those up by the name they write (#1686: `actionData` was the most grepped
419
+ * term across the eval builds), so the member's doc and declaration print,
420
+ * headed by the type that owns it.
421
+ * @param {{ readFile: Function, readdir: Function }} deps
422
+ * @param {Array<{ pkg: string, root: string }>} roots
423
+ * @param {string} n
424
+ * @returns {Promise<string | null>}
425
+ */
426
+ async function lookupMember(deps, roots, n) {
427
+ const memberRe = new RegExp(`^\\s+(?:readonly\\s+)?(?:static\\s+)?${n.replace(/\$/g, '\\$')}\\??\\s*[:(<]`);
428
+ const ownerRe = /^export\s+(?:declare\s+)?(?:abstract\s+)?(?:interface|class|type)\s+([A-Za-z_$][\w$]*)/;
429
+ const out = [];
430
+ let count = 0;
431
+ for (const r of roots) {
432
+ for (const file of walkSource(r.root, deps).filter((f) => /\.d\.(?:ts|mts|cts)$/.test(f))) {
433
+ let text = '';
434
+ try { text = await deps.readFile(file, 'utf8'); } catch { continue; }
435
+ if (!text.includes(n)) continue;
436
+ const lines = text.split('\n');
437
+ const rel = relative(r.root, file).split(sep).join('/');
438
+ for (let i = 0; i < lines.length; i++) {
439
+ if (!memberRe.test(lines[i])) continue;
440
+ let owner = null;
441
+ for (let j = i - 1; j >= 0; j--) {
442
+ const m = ownerRe.exec(lines[j]);
443
+ if (m) { owner = m[1]; break; }
444
+ if (/^\S/.test(lines[j]) && !/^\s*(?:\*|\/\/|\/\*)/.test(lines[j]) && /^(?:}|export)/.test(lines[j])) break;
445
+ }
446
+ if (!owner) continue;
447
+ if (++count > MAX_EXPORT_HITS) continue;
448
+ const decl = [];
449
+ for (let k = i, depth = 0; k < lines.length && decl.length < MAX_DECL_LINES; k++) {
450
+ decl.push(lines[k]);
451
+ for (const ch of lines[k]) { if (ch === '{' || ch === '(') depth++; else if (ch === '}' || ch === ')') depth--; }
452
+ if (depth <= 0 && /[;,]\s*$/.test(lines[k])) break;
453
+ }
454
+ out.push(`"${n}" is a member of ${owner} (@webjsdev/${r.pkg} ${rel}:${i + 1}); \`source ${owner}\` prints the whole type.`);
455
+ out.push(...docAbove(lines, i), ...decl, '');
456
+ }
457
+ }
458
+ }
459
+ if (!out.length) return null;
460
+ if (count > MAX_EXPORT_HITS) out.push(`... (${count - MAX_EXPORT_HITS} more members named "${n}"; narrow with \`package\`)`);
461
+ return out.join('\n').trimEnd();
462
+ }
463
+
380
464
  /**
381
465
  * 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`
466
+ * export's signature and doc, `path` reads a file, `query` greps, `package`
467
+ * lists that package's entry points, and no args prints the feature catalogue
468
+ * (what WebJs provides, by need). PURE given `deps`
384
469
  * (`{ roots, readFile, readdir }`).
385
470
  *
386
471
  * @param {object} deps
@@ -392,5 +477,6 @@ export async function runSourceTool(deps, args) {
392
477
  if (a.export) return lookupExport(deps, a.export, a.package);
393
478
  if (a.path) return readSource(deps, a.path);
394
479
  if (a.query) return grepSources(deps, a.query);
395
- return listSources(deps, a.package);
480
+ if (a.package) return listSources(deps, a.package);
481
+ return featureCatalog();
396
482
  }