@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 +30 -103
- package/package.json +5 -10
- package/src/brief.js +52 -0
- package/src/catalog.js +82 -0
- package/src/mcp-init.js +139 -0
- package/src/mcp-source.js +94 -8
- package/src/mcp.js +96 -164
- package/resources/AGENTS.md +0 -701
- package/resources/SKILL.md +0 -286
- package/resources/corpus.json +0 -6
- package/resources/references/auth-and-sessions.md +0 -300
- package/resources/references/built-ins.md +0 -336
- package/resources/references/client-router-and-streaming.md +0 -313
- package/resources/references/components.md +0 -469
- package/resources/references/data-and-actions.md +0 -319
- package/resources/references/module-structure.md +0 -229
- package/resources/references/muscle-memory-gotchas.md +0 -341
- package/resources/references/optimistic-ui.md +0 -179
- package/resources/references/routing-and-pages.md +0 -323
- package/resources/references/runtime.md +0 -108
- package/resources/references/service-worker.md +0 -80
- package/resources/references/styling.md +0 -295
- package/resources/references/testing.md +0 -223
- package/resources/references/typescript.md +0 -231
- package/resources/references/ui-kit.md +0 -127
- package/src/mcp-docs.js +0 -641
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# @webjsdev/mcp
|
|
2
2
|
|
|
3
|
-
The
|
|
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
|
|
6
|
-
editing a
|
|
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
|
|
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
|
-
`
|
|
32
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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.
|
|
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,
|
|
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.
|
|
29
|
-
"@webjsdev/ui": "^0.
|
|
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
|
+
}
|
package/src/mcp-init.js
ADDED
|
@@ -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
|
-
*
|
|
118
|
-
*
|
|
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 = [
|
|
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,
|
|
383
|
-
*
|
|
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
|
}
|