@webjsdev/cli 0.10.11 → 0.10.13
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 +1 -1
- package/bin/webjs.js +19 -13
- package/lib/create.js +24 -18
- package/package.json +2 -1
- package/templates/.claude.json +1 -1
- package/templates/AGENTS.md +17 -15
- package/templates/CONVENTIONS.md +5 -5
- package/lib/check-json.js +0 -47
- package/lib/mcp.js +0 -408
package/README.md
CHANGED
package/bin/webjs.js
CHANGED
|
@@ -141,7 +141,7 @@ async function main() {
|
|
|
141
141
|
}
|
|
142
142
|
case 'ui': {
|
|
143
143
|
// Delegate to @webjsdev/ui. Bundled as a hard dependency of
|
|
144
|
-
// @webjsdev/cli, so `npm install -g
|
|
144
|
+
// @webjsdev/cli, so `npm install -g webjsdev` pulls it in
|
|
145
145
|
// automatically, and `webjs ui add button` works out of the box
|
|
146
146
|
// without an extra install in user projects.
|
|
147
147
|
const { createRequire } = await import('node:module');
|
|
@@ -157,7 +157,7 @@ async function main() {
|
|
|
157
157
|
entry = userReq.resolve('@webjsdev/ui/bin/webjsui.js');
|
|
158
158
|
} catch {
|
|
159
159
|
console.error('@webjsdev/ui could not be resolved.');
|
|
160
|
-
console.error('Reinstall the CLI: npm install -g
|
|
160
|
+
console.error('Reinstall the CLI: npm install -g webjsdev');
|
|
161
161
|
process.exit(1);
|
|
162
162
|
}
|
|
163
163
|
}
|
|
@@ -275,7 +275,9 @@ async function main() {
|
|
|
275
275
|
// identical to the MCP `check` tool. The non-zero exit on violations is
|
|
276
276
|
// preserved (an agent gates on the exit code AND parses the report).
|
|
277
277
|
if (rest.includes('--json')) {
|
|
278
|
-
|
|
278
|
+
// The projector lives in @webjsdev/mcp (the MCP `check` tool's home),
|
|
279
|
+
// so `check --json` and the MCP tool stay byte-identical (#415).
|
|
280
|
+
const { projectCheck } = await import('@webjsdev/mcp/check-report');
|
|
279
281
|
console.log(JSON.stringify(projectCheck(violations)));
|
|
280
282
|
if (violations.length > 0) process.exit(1);
|
|
281
283
|
break;
|
|
@@ -647,19 +649,23 @@ Full docs: https://docs.webjs.com`);
|
|
|
647
649
|
process.exit(1);
|
|
648
650
|
}
|
|
649
651
|
case 'mcp': {
|
|
650
|
-
// Read-only MCP server (#262) over stdio. STDOUT is the JSON-RPC
|
|
651
|
-
// so nothing here may write to stdout: the data functions are
|
|
652
|
-
// and `runMcpServer` routes all diagnostics to stderr. The
|
|
653
|
-
//
|
|
654
|
-
|
|
652
|
+
// Read-only MCP server (#262, #415) over stdio. STDOUT is the JSON-RPC
|
|
653
|
+
// channel, so nothing here may write to stdout: the data functions are
|
|
654
|
+
// read-only and `runMcpServer` routes all diagnostics to stderr. The
|
|
655
|
+
// implementation lives in the standalone `@webjsdev/mcp` package (also
|
|
656
|
+
// runnable directly as `npx @webjsdev/mcp`); `webjs mcp` delegates to it
|
|
657
|
+
// for back-compat. The version advertised in the initialize handshake is
|
|
658
|
+
// @webjsdev/mcp's own, resolved by its bin, so this passes none.
|
|
659
|
+
const { runMcpServer } = await import('@webjsdev/mcp');
|
|
660
|
+
const { createRequire } = await import('node:module');
|
|
661
|
+
const require = createRequire(import.meta.url);
|
|
655
662
|
let version = '0.0.0';
|
|
656
663
|
try {
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
664
|
+
const { readFileSync } = await import('node:fs');
|
|
665
|
+
version = JSON.parse(
|
|
666
|
+
readFileSync(require.resolve('@webjsdev/mcp/package.json'), 'utf8'),
|
|
667
|
+
).version || version;
|
|
661
668
|
} catch {}
|
|
662
|
-
const { runMcpServer } = await import('../lib/mcp.js');
|
|
663
669
|
await runMcpServer({
|
|
664
670
|
stdin: process.stdin,
|
|
665
671
|
stdout: process.stdout,
|
package/lib/create.js
CHANGED
|
@@ -308,6 +308,7 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
308
308
|
// tsc --noEmit). Not needed at runtime (Node strips types in place), only
|
|
309
309
|
// to type-check the app.
|
|
310
310
|
typescript: '^5.6.0',
|
|
311
|
+
'@types/node': '^24.0.0',
|
|
311
312
|
'@web/test-runner': '^0.20.0',
|
|
312
313
|
'@web/test-runner-playwright': '^0.11.0',
|
|
313
314
|
'playwright': '^1.59.0',
|
|
@@ -315,14 +316,19 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
315
316
|
// assertNoA11yViolations() test helper from @webjsdev/core/testing.
|
|
316
317
|
// Test-only: dynamically imported, never shipped to the app runtime.
|
|
317
318
|
'axe-core': '^4.10.0',
|
|
318
|
-
// tsserver plugin
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
|
|
319
|
+
// tsserver plugin, wired into tsconfig below. Gives the language
|
|
320
|
+
// INTELLIGENCE (go-to-def, completions, diagnostics, hover inside html``
|
|
321
|
+
// templates) in any tsserver editor with NO editor plugin installed,
|
|
322
|
+
// because editors load tsconfig plugins from node_modules. The `webjs`
|
|
323
|
+
// VS Code extension and webjs.nvim ALSO bundle this plugin (so it works
|
|
324
|
+
// before `npm install` too, and adds template HIGHLIGHTING, which a
|
|
325
|
+
// tsserver plugin can't provide); tsserver dedupes by name, so loading
|
|
326
|
+
// it both ways is a no-op. Standalone, no Lit dependency. Editor-only.
|
|
327
|
+
'@webjsdev/intellisense': 'latest',
|
|
328
|
+
// NOTE: @webjsdev/ui is intentionally NOT pinned. The UI kit is
|
|
329
|
+
// shadcn-style copy-in: `webjs ui add <name>` copies component source
|
|
330
|
+
// into components/ui/ (they import @webjsdev/core, not the kit), and the
|
|
331
|
+
// CLI resolves @webjsdev/ui from its own install.
|
|
326
332
|
},
|
|
327
333
|
}, null, 2) + '\n');
|
|
328
334
|
|
|
@@ -332,6 +338,7 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
332
338
|
module: 'NodeNext',
|
|
333
339
|
moduleResolution: 'NodeNext',
|
|
334
340
|
lib: ['ES2022', 'DOM', 'DOM.Iterable'],
|
|
341
|
+
types: ['node'],
|
|
335
342
|
strict: true,
|
|
336
343
|
noEmit: true,
|
|
337
344
|
allowImportingTsExtensions: true,
|
|
@@ -347,17 +354,16 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
347
354
|
// SYNTAX errors. Use a `const` object + union for enum-shaped
|
|
348
355
|
// values; write fields + constructor assignments explicitly.
|
|
349
356
|
erasableSyntaxOnly: true,
|
|
350
|
-
// @webjsdev/
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
// •
|
|
354
|
-
// •
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
// Editor-only. The framework runs without it.
|
|
357
|
+
// @webjsdev/intellisense (standalone, no Lit dependency) gives the editor,
|
|
358
|
+
// inside html`` templates:
|
|
359
|
+
// • go-to-definition on custom-element tags, attributes, and CSS classes
|
|
360
|
+
// • binding-aware completions (tag names, .prop / ?bool / plain attrs)
|
|
361
|
+
// • diagnostics (value type-checks, unquoted-binding, expressionless .prop)
|
|
362
|
+
// • hover showing the component class / declared member type
|
|
363
|
+
// Editor-only. The framework runs without it. For VS Code / Cursor /
|
|
364
|
+
// Windsurf, the `webjs` extension bundles this automatically.
|
|
359
365
|
plugins: [
|
|
360
|
-
{ name: '@webjsdev/
|
|
366
|
+
{ name: '@webjsdev/intellisense' },
|
|
361
367
|
],
|
|
362
368
|
},
|
|
363
369
|
// `.webjs/routes.d.ts` is the OPT-IN generated route-types overlay (#258):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webjsdev/cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "webjs CLI - dev, start, create, db",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
|
+
"@webjsdev/mcp": "^0.1.0",
|
|
16
17
|
"@webjsdev/server": "^0.8.0",
|
|
17
18
|
"@webjsdev/ui": "^0.3.1"
|
|
18
19
|
},
|
package/templates/.claude.json
CHANGED
package/templates/AGENTS.md
CHANGED
|
@@ -89,14 +89,14 @@ node_modules/@webjsdev/
|
|
|
89
89
|
src/actions.js ← .server.ts scanner, RPC, expose()
|
|
90
90
|
src/auth.js, session.js, cache.js, rate-limit.js, csrf.js
|
|
91
91
|
cli/ webjs CLI (dev / start / build / test / check / create / db)
|
|
92
|
-
|
|
92
|
+
intellisense/ tsserver plugin: go-to-definition + diagnostic suppression
|
|
93
93
|
+ attribute auto-complete for Class.register('tag') elements
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
Reaching straight for the source is the fastest way to resolve "why
|
|
97
97
|
doesn't X work?" with no documentation guesswork and no stale blog posts.
|
|
98
98
|
|
|
99
|
-
## Editor TS plugin: `@webjsdev/
|
|
99
|
+
## Editor TS plugin: `@webjsdev/intellisense`
|
|
100
100
|
|
|
101
101
|
This scaffold's `tsconfig.json` lists a single tsserver plugin. It is
|
|
102
102
|
editor-only, not required for the framework to run.
|
|
@@ -104,22 +104,24 @@ editor-only, not required for the framework to run.
|
|
|
104
104
|
```jsonc
|
|
105
105
|
// tsconfig.json (already wired by the scaffold)
|
|
106
106
|
"plugins": [
|
|
107
|
-
{ "name": "@webjsdev/
|
|
107
|
+
{ "name": "@webjsdev/intellisense" }
|
|
108
108
|
]
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
`@webjsdev/
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
111
|
+
`@webjsdev/intellisense` is **standalone** (no Lit dependency): one plugin
|
|
112
|
+
entry, its own template parser. Inside `` html`…` `` templates you get:
|
|
113
|
+
|
|
114
|
+
- Go-to-definition on custom-element tags, attribute / property / event
|
|
115
|
+
names, and CSS classes in `class="…"`.
|
|
116
|
+
- Binding-aware completions: reachable tag names after `<`, and
|
|
117
|
+
prefix-keyed attributes (`.prop` property names, `?bool` / plain
|
|
118
|
+
hyphenated attribute names).
|
|
119
|
+
- Diagnostics: value type-checks against `declare propName: T`, unquoted
|
|
120
|
+
`@`/`.`/`?` bindings, and expressionless `.prop` bindings.
|
|
121
|
+
- Hover showing the component class / declared member type.
|
|
122
|
+
|
|
123
|
+
In VS Code / Cursor / Windsurf, the **`webjs` extension** bundles this
|
|
124
|
+
automatically (no `tsconfig.json` edit, no separate Lit extension).
|
|
123
125
|
|
|
124
126
|
See [docs.webjs.com → Editor setup](https://docs.webjs.com/docs/editor-setup)
|
|
125
127
|
for the full walkthrough.
|
package/templates/CONVENTIONS.md
CHANGED
|
@@ -654,11 +654,11 @@ attribute coercion, reflection). `declare` types the field for
|
|
|
654
654
|
TypeScript without emitting a class-field initializer that would
|
|
655
655
|
clobber the reactive accessor at construction time. The two
|
|
656
656
|
declarations together give you full intelligence in any tsserver-backed
|
|
657
|
-
editor. See the Editor Setup docs for the `
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
657
|
+
editor. See the Editor Setup docs for the standalone `@webjsdev/intellisense`
|
|
658
|
+
(no Lit dependency) that extends this to tag / attribute intelligence
|
|
659
|
+
inside `html\`…\`` templates (go-to-definition, binding-aware completions,
|
|
660
|
+
value/binding diagnostics, hover); in VS Code / Cursor / Windsurf the
|
|
661
|
+
`webjs` extension bundles it automatically.
|
|
662
662
|
|
|
663
663
|
**Rules:**
|
|
664
664
|
- One component per file
|
package/lib/check-json.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared JSON projector for `webjs check` violations (#262).
|
|
3
|
-
*
|
|
4
|
-
* `webjs check --json` and the `webjs mcp` server's `check` tool BOTH return
|
|
5
|
-
* the identical shape, so the projection lives here once. The input is the raw
|
|
6
|
-
* `Violation[]` from `checkConventions(appDir)` (each `{ rule, file, message,
|
|
7
|
-
* fix }`); the output adds a `summary` count plus a per-rule breakdown so an
|
|
8
|
-
* agent consuming the structured output never has to regex-scrape stdout.
|
|
9
|
-
*
|
|
10
|
-
* Pure and side-effect-free: it neither reads files nor prints. The caller owns
|
|
11
|
-
* running `checkConventions` and (for the CLI) the non-zero exit when there are
|
|
12
|
-
* violations.
|
|
13
|
-
*
|
|
14
|
-
* @module check-json
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* @typedef {{ rule: string, file: string, message: string, fix: string }} Violation
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @typedef {{
|
|
23
|
-
* violations: Violation[],
|
|
24
|
-
* summary: { count: number, byRule: Record<string, number> },
|
|
25
|
-
* }} CheckReport
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Project a raw `Violation[]` into the structured `{ violations, summary }`
|
|
30
|
-
* report shared by `check --json` and the MCP `check` tool. `violations` is
|
|
31
|
-
* passed through verbatim (the `{ rule, file, message, fix }` shape), and
|
|
32
|
-
* `summary.byRule` tallies how many violations each rule produced.
|
|
33
|
-
*
|
|
34
|
-
* @param {Violation[]} violations
|
|
35
|
-
* @returns {CheckReport}
|
|
36
|
-
*/
|
|
37
|
-
export function projectCheck(violations) {
|
|
38
|
-
/** @type {Record<string, number>} */
|
|
39
|
-
const byRule = {};
|
|
40
|
-
for (const v of violations) {
|
|
41
|
-
byRule[v.rule] = (byRule[v.rule] || 0) + 1;
|
|
42
|
-
}
|
|
43
|
-
return {
|
|
44
|
-
violations,
|
|
45
|
-
summary: { count: violations.length, byRule },
|
|
46
|
-
};
|
|
47
|
-
}
|
package/lib/mcp.js
DELETED
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `webjs mcp`: a minimal, READ-ONLY Model Context Protocol server (#262).
|
|
3
|
-
*
|
|
4
|
-
* Exposes the live introspection surface an AI agent needs while editing a
|
|
5
|
-
* webjs app (the route table, registered server actions with their RPC
|
|
6
|
-
* endpoints, registered custom-element tags, and the structured `webjs check`
|
|
7
|
-
* violations) over MCP's stdio transport. Every tool REUSES an existing
|
|
8
|
-
* `@webjsdev/server` data function and MUTATES NOTHING. The prior art is
|
|
9
|
-
* Next.js's `next-devtools-mcp` (get_routes / get_server_action_by_id /
|
|
10
|
-
* get_errors); this is deliberately tiny.
|
|
11
|
-
*
|
|
12
|
-
* Transport: MCP stdio is newline-delimited JSON-RPC 2.0. One JSON object per
|
|
13
|
-
* line arrives on stdin; exactly one JSON response line per request is written
|
|
14
|
-
* to stdout. STDOUT IS THE PROTOCOL CHANNEL, so this module writes ONLY
|
|
15
|
-
* JSON-RPC frames there and routes every diagnostic to stderr. A malformed
|
|
16
|
-
* input line is answered with a JSON-RPC parse error and never crashes the
|
|
17
|
-
* loop. Hand-rolled with zero new dependency (webjs is buildless +
|
|
18
|
-
* minimal-deps).
|
|
19
|
-
*
|
|
20
|
-
* @module mcp
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { createInterface } from 'node:readline';
|
|
24
|
-
import { relative } from 'node:path';
|
|
25
|
-
|
|
26
|
-
const PROTOCOL_VERSION = '2024-11-05';
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* The four read-only tools. Each takes an optional `{ appDir }` (default the
|
|
30
|
-
* server's cwd) and projects an EXISTING `@webjsdev/server` function's output
|
|
31
|
-
* into an agent-friendly shape. Descriptions are crisp so a model picks the
|
|
32
|
-
* right tool without reading source.
|
|
33
|
-
*/
|
|
34
|
-
const TOOL_DEFS = [
|
|
35
|
-
{
|
|
36
|
-
name: 'list_routes',
|
|
37
|
-
description:
|
|
38
|
-
'List the app route table: SSR pages (path, file, dynamic flag, param names) and route.{js,ts} API handlers (path, file, HTTP methods). Read-only.',
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
name: 'list_actions',
|
|
42
|
-
description:
|
|
43
|
-
'List registered server actions (the .server.{js,ts} files with "use server"): file, exported function name, and the /__webjs/action/<hash>/<fn> RPC endpoint. Read-only.',
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
name: 'list_components',
|
|
47
|
-
description:
|
|
48
|
-
'List registered custom-element tags: tag name, defining file, and class name. Read-only.',
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
name: 'check',
|
|
52
|
-
description:
|
|
53
|
-
'Run webjs check (correctness rules) and return the structured violations { rule, file, message, fix } plus a summary count and per-rule breakdown. Read-only.',
|
|
54
|
-
},
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
/** The shared input schema: every tool takes an optional appDir override. */
|
|
58
|
-
const TOOL_INPUT_SCHEMA = {
|
|
59
|
-
type: 'object',
|
|
60
|
-
properties: {
|
|
61
|
-
appDir: {
|
|
62
|
-
type: 'string',
|
|
63
|
-
description: 'App directory to introspect. Defaults to the server cwd.',
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
required: [],
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Lexically extract the names exported from a module source. Recognises the
|
|
71
|
-
* common forms a server-action / route file uses without LOADING the module
|
|
72
|
-
* (loading would run its top-level side effects: Prisma init, DB connects).
|
|
73
|
-
*
|
|
74
|
-
* export async function foo() {} export function foo() {}
|
|
75
|
-
* export const foo = ... export let/var foo = ...
|
|
76
|
-
* export default ... (recorded as 'default')
|
|
77
|
-
* export { a, b as c } (the EXPORTED name, so `c`)
|
|
78
|
-
*
|
|
79
|
-
* @param {string} src
|
|
80
|
-
* @returns {string[]} unique export names, in source order
|
|
81
|
-
*/
|
|
82
|
-
export function extractExportNames(src) {
|
|
83
|
-
/** @type {string[]} */
|
|
84
|
-
const names = [];
|
|
85
|
-
const add = (n) => { if (n && !names.includes(n)) names.push(n); };
|
|
86
|
-
|
|
87
|
-
// export [async] function NAME / export const|let|var NAME / export class NAME
|
|
88
|
-
const declRe =
|
|
89
|
-
/\bexport\s+(?:async\s+)?(?:function\*?|const|let|var|class)\s+([A-Za-z_$][\w$]*)/g;
|
|
90
|
-
let m;
|
|
91
|
-
while ((m = declRe.exec(src)) !== null) add(m[1]);
|
|
92
|
-
|
|
93
|
-
// export default ...
|
|
94
|
-
if (/\bexport\s+default\b/.test(src)) add('default');
|
|
95
|
-
|
|
96
|
-
// export { a, b as c, default as d }
|
|
97
|
-
const namedRe = /\bexport\s*\{([^}]*)\}/g;
|
|
98
|
-
while ((m = namedRe.exec(src)) !== null) {
|
|
99
|
-
for (const part of m[1].split(',')) {
|
|
100
|
-
const seg = part.trim();
|
|
101
|
-
if (!seg) continue;
|
|
102
|
-
// `local as exported` -> the exported name is what callers import.
|
|
103
|
-
const asMatch = /\bas\s+([A-Za-z_$][\w$]*)\s*$/.exec(seg);
|
|
104
|
-
if (asMatch) add(asMatch[1]);
|
|
105
|
-
else {
|
|
106
|
-
const idMatch = /^([A-Za-z_$][\w$]*)$/.exec(seg);
|
|
107
|
-
if (idMatch) add(idMatch[1]);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return names;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Lexically extract the HTTP method exports of a route.{js,ts} file. The webjs
|
|
116
|
-
* API router (`api.js`) dispatches the five standard verbs, plus `WS` for a
|
|
117
|
-
* WebSocket upgrade. We report exactly those that are exported, NOT `HEAD` /
|
|
118
|
-
* `OPTIONS` (the router does not dispatch a named handler for them, so listing
|
|
119
|
-
* them would imply a route the framework ignores). Read-only: no module load.
|
|
120
|
-
*
|
|
121
|
-
* @param {string} src
|
|
122
|
-
* @returns {string[]}
|
|
123
|
-
*/
|
|
124
|
-
export function extractRouteMethods(src) {
|
|
125
|
-
const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'WS'];
|
|
126
|
-
const exported = new Set(extractExportNames(src));
|
|
127
|
-
return METHODS.filter((m) => exported.has(m));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* The literal URL path for a page/api directory: `blog/[slug]` -> `/blog/[slug]`,
|
|
132
|
-
* the root `.` -> `/`. Route groups `(group)` and `_private` segments drop, the
|
|
133
|
-
* same normalization `buildRouteTable` uses for matching.
|
|
134
|
-
*
|
|
135
|
-
* @param {string} routeDir POSIX-style, `.` for the app root.
|
|
136
|
-
* @returns {string}
|
|
137
|
-
*/
|
|
138
|
-
function routePathFromDir(routeDir) {
|
|
139
|
-
if (!routeDir || routeDir === '.') return '/';
|
|
140
|
-
const segs = routeDir
|
|
141
|
-
.split('/')
|
|
142
|
-
.filter((s) => !(s.startsWith('(') && s.endsWith(')')) && !s.startsWith('_'));
|
|
143
|
-
return segs.length ? '/' + segs.join('/') : '/';
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* The tool runners. Each is async, takes `(appDir)`, and returns a plain
|
|
148
|
-
* JSON-serialisable projection of an existing server data function. All are
|
|
149
|
-
* read-only.
|
|
150
|
-
*
|
|
151
|
-
* @param {{ buildRouteTable: Function, buildActionIndex: Function, hashFile: Function, scanComponents: Function, checkConventions: Function, projectCheck: Function, readFile: Function }} deps
|
|
152
|
-
*/
|
|
153
|
-
export function makeToolRunners(deps) {
|
|
154
|
-
const {
|
|
155
|
-
buildRouteTable,
|
|
156
|
-
buildActionIndex,
|
|
157
|
-
hashFile,
|
|
158
|
-
scanComponents,
|
|
159
|
-
checkConventions,
|
|
160
|
-
projectCheck,
|
|
161
|
-
readFile,
|
|
162
|
-
} = deps;
|
|
163
|
-
|
|
164
|
-
return {
|
|
165
|
-
async list_routes(appDir) {
|
|
166
|
-
const table = await buildRouteTable(appDir);
|
|
167
|
-
const pages = await Promise.all(
|
|
168
|
-
table.pages.map(async (r) => {
|
|
169
|
-
/** @type {{ path: string, file: string, dynamic?: boolean, params?: string[] }} */
|
|
170
|
-
const out = {
|
|
171
|
-
path: routePathFromDir(r.routeDir),
|
|
172
|
-
file: relative(appDir, r.file),
|
|
173
|
-
};
|
|
174
|
-
if (r.paramNames && r.paramNames.length) {
|
|
175
|
-
out.dynamic = true;
|
|
176
|
-
out.params = r.paramNames;
|
|
177
|
-
}
|
|
178
|
-
return out;
|
|
179
|
-
}),
|
|
180
|
-
);
|
|
181
|
-
const apis = await Promise.all(
|
|
182
|
-
table.apis.map(async (r) => {
|
|
183
|
-
let methods = [];
|
|
184
|
-
try {
|
|
185
|
-
methods = extractRouteMethods(await readFile(r.file, 'utf8'));
|
|
186
|
-
} catch {}
|
|
187
|
-
return {
|
|
188
|
-
path: routePathFromDir(r.routeDir),
|
|
189
|
-
file: relative(appDir, r.file),
|
|
190
|
-
methods,
|
|
191
|
-
};
|
|
192
|
-
}),
|
|
193
|
-
);
|
|
194
|
-
return { pages, apis };
|
|
195
|
-
},
|
|
196
|
-
|
|
197
|
-
async list_actions(appDir) {
|
|
198
|
-
// `skipExposeLoad` builds the file -> hash maps WITHOUT importing any
|
|
199
|
-
// `expose()` module, so this stays truly read-only (no Prisma/DB init, and
|
|
200
|
-
// no stray stdout from a loaded module corrupting the JSON-RPC channel).
|
|
201
|
-
// The RPC hash is over the file path only, so no module load is needed.
|
|
202
|
-
const idx = await buildActionIndex(appDir, false, { skipExposeLoad: true });
|
|
203
|
-
/** @type {Array<{ file: string, fn: string, endpoint: string }>} */
|
|
204
|
-
const actions = [];
|
|
205
|
-
for (const [file, hash] of idx.fileToHash) {
|
|
206
|
-
let names = [];
|
|
207
|
-
try {
|
|
208
|
-
names = extractExportNames(await readFile(file, 'utf8'));
|
|
209
|
-
} catch {}
|
|
210
|
-
for (const fn of names) {
|
|
211
|
-
actions.push({
|
|
212
|
-
file: relative(appDir, file),
|
|
213
|
-
fn,
|
|
214
|
-
endpoint: `/__webjs/action/${hash}/${fn}`,
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
// Stable order for deterministic output.
|
|
219
|
-
actions.sort((a, b) =>
|
|
220
|
-
a.file === b.file ? a.fn.localeCompare(b.fn) : a.file.localeCompare(b.file),
|
|
221
|
-
);
|
|
222
|
-
return actions;
|
|
223
|
-
},
|
|
224
|
-
|
|
225
|
-
async list_components(appDir) {
|
|
226
|
-
const comps = await scanComponents(appDir);
|
|
227
|
-
return comps
|
|
228
|
-
.map((c) => ({
|
|
229
|
-
tag: c.tag,
|
|
230
|
-
file: relative(appDir, c.file),
|
|
231
|
-
className: c.className,
|
|
232
|
-
}))
|
|
233
|
-
.sort((a, b) => a.tag.localeCompare(b.tag));
|
|
234
|
-
},
|
|
235
|
-
|
|
236
|
-
async check(appDir) {
|
|
237
|
-
const violations = await checkConventions(appDir);
|
|
238
|
-
return projectCheck(violations);
|
|
239
|
-
},
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* A JSON-RPC 2.0 result frame.
|
|
245
|
-
* @param {string|number|null} id
|
|
246
|
-
* @param {any} result
|
|
247
|
-
*/
|
|
248
|
-
function rpcResult(id, result) {
|
|
249
|
-
return { jsonrpc: '2.0', id, result };
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* A JSON-RPC 2.0 error frame.
|
|
254
|
-
* @param {string|number|null} id
|
|
255
|
-
* @param {number} code
|
|
256
|
-
* @param {string} message
|
|
257
|
-
*/
|
|
258
|
-
function rpcError(id, code, message) {
|
|
259
|
-
return { jsonrpc: '2.0', id, error: { code, message } };
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Run the read-only webjs MCP server over the given streams. Reads
|
|
264
|
-
* newline-delimited JSON-RPC from `stdin`, writes one response line per request
|
|
265
|
-
* to `stdout`, and logs diagnostics to `stderr` ONLY (stdout is the protocol
|
|
266
|
-
* channel). Resolves when stdin ends (clean shutdown).
|
|
267
|
-
*
|
|
268
|
-
* Injectable streams + `cwd` keep it testable in-process with PassThrough
|
|
269
|
-
* streams; the bin passes the real `process.std*` + `process.cwd()`.
|
|
270
|
-
*
|
|
271
|
-
* @param {{
|
|
272
|
-
* stdin: NodeJS.ReadableStream,
|
|
273
|
-
* stdout: NodeJS.WritableStream,
|
|
274
|
-
* stderr: NodeJS.WritableStream,
|
|
275
|
-
* cwd: string,
|
|
276
|
-
* version?: string,
|
|
277
|
-
* deps?: object,
|
|
278
|
-
* }} opts
|
|
279
|
-
* @returns {Promise<void>}
|
|
280
|
-
*/
|
|
281
|
-
export async function runMcpServer(opts) {
|
|
282
|
-
const { stdin, stdout, stderr, cwd } = opts;
|
|
283
|
-
const version = opts.version || '0.0.0';
|
|
284
|
-
|
|
285
|
-
// Resolve the data functions from @webjsdev/server (reading them is
|
|
286
|
-
// read-only). Injectable for tests so they need not boot the real server.
|
|
287
|
-
let deps = opts.deps;
|
|
288
|
-
if (!deps) {
|
|
289
|
-
const server = await import('@webjsdev/server');
|
|
290
|
-
const check = await import('@webjsdev/server/check');
|
|
291
|
-
const { readFile } = await import('node:fs/promises');
|
|
292
|
-
const { projectCheck } = await import('./check-json.js');
|
|
293
|
-
deps = {
|
|
294
|
-
buildRouteTable: server.buildRouteTable,
|
|
295
|
-
buildActionIndex: server.buildActionIndex,
|
|
296
|
-
hashFile: server.hashFile,
|
|
297
|
-
scanComponents: server.scanComponents,
|
|
298
|
-
checkConventions: check.checkConventions,
|
|
299
|
-
projectCheck,
|
|
300
|
-
readFile,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
const runners = makeToolRunners(deps);
|
|
304
|
-
|
|
305
|
-
/** Write one JSON-RPC frame as a single line to stdout. */
|
|
306
|
-
const send = (frame) => {
|
|
307
|
-
stdout.write(JSON.stringify(frame) + '\n');
|
|
308
|
-
};
|
|
309
|
-
/** Diagnostics go to stderr only, never stdout. */
|
|
310
|
-
const logErr = (msg) => {
|
|
311
|
-
try { stderr.write(`[webjs mcp] ${msg}\n`); } catch {}
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Dispatch one parsed JSON-RPC message. Returns a frame to send, or null
|
|
316
|
-
* for a notification (no `id`) which gets no response.
|
|
317
|
-
*/
|
|
318
|
-
const dispatch = async (msg) => {
|
|
319
|
-
const id = msg && Object.prototype.hasOwnProperty.call(msg, 'id') ? msg.id : null;
|
|
320
|
-
const isNotification = id === null || id === undefined;
|
|
321
|
-
const method = msg && msg.method;
|
|
322
|
-
|
|
323
|
-
if (method === 'initialize') {
|
|
324
|
-
return rpcResult(id, {
|
|
325
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
326
|
-
capabilities: { tools: {} },
|
|
327
|
-
serverInfo: { name: 'webjs', version },
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// `notifications/initialized` (and any other notification) gets no reply.
|
|
332
|
-
if (isNotification) return null;
|
|
333
|
-
|
|
334
|
-
if (method === 'tools/list') {
|
|
335
|
-
return rpcResult(id, {
|
|
336
|
-
tools: TOOL_DEFS.map((t) => ({
|
|
337
|
-
name: t.name,
|
|
338
|
-
description: t.description,
|
|
339
|
-
inputSchema: TOOL_INPUT_SCHEMA,
|
|
340
|
-
})),
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (method === 'tools/call') {
|
|
345
|
-
const params = (msg && msg.params) || {};
|
|
346
|
-
const name = params.name;
|
|
347
|
-
const args = params.arguments || {};
|
|
348
|
-
const runner = runners[name];
|
|
349
|
-
if (!runner) {
|
|
350
|
-
return rpcError(id, -32602, `Unknown tool: ${String(name)}`);
|
|
351
|
-
}
|
|
352
|
-
const appDir = typeof args.appDir === 'string' && args.appDir ? args.appDir : cwd;
|
|
353
|
-
try {
|
|
354
|
-
const result = await runner(appDir);
|
|
355
|
-
return rpcResult(id, {
|
|
356
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
357
|
-
});
|
|
358
|
-
} catch (e) {
|
|
359
|
-
// A tool failure is an MCP tool-result error (isError), not a transport
|
|
360
|
-
// error, so the agent sees the message in the content channel.
|
|
361
|
-
logErr(`tool ${name} failed: ${e && e.message ? e.message : e}`);
|
|
362
|
-
return rpcResult(id, {
|
|
363
|
-
isError: true,
|
|
364
|
-
content: [
|
|
365
|
-
{ type: 'text', text: `Error running ${name}: ${e && e.message ? e.message : String(e)}` },
|
|
366
|
-
],
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return rpcError(id, -32601, `Method not found: ${String(method)}`);
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
const rl = createInterface({ input: stdin, crlfDelay: Infinity });
|
|
375
|
-
|
|
376
|
-
await new Promise((resolveRun) => {
|
|
377
|
-
// Serialise line handling so responses preserve request order even though
|
|
378
|
-
// dispatch is async.
|
|
379
|
-
let chain = Promise.resolve();
|
|
380
|
-
rl.on('line', (line) => {
|
|
381
|
-
const trimmed = line.trim();
|
|
382
|
-
if (!trimmed) return;
|
|
383
|
-
chain = chain.then(async () => {
|
|
384
|
-
let msg;
|
|
385
|
-
try {
|
|
386
|
-
msg = JSON.parse(trimmed);
|
|
387
|
-
} catch {
|
|
388
|
-
// Malformed line: a JSON-RPC parse error, never a crash.
|
|
389
|
-
send(rpcError(null, -32700, 'Parse error'));
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
try {
|
|
393
|
-
const frame = await dispatch(msg);
|
|
394
|
-
if (frame) send(frame);
|
|
395
|
-
} catch (e) {
|
|
396
|
-
logErr(`dispatch error: ${e && e.message ? e.message : e}`);
|
|
397
|
-
const id =
|
|
398
|
-
msg && Object.prototype.hasOwnProperty.call(msg, 'id') ? msg.id : null;
|
|
399
|
-
send(rpcError(id, -32603, 'Internal error'));
|
|
400
|
-
}
|
|
401
|
-
});
|
|
402
|
-
});
|
|
403
|
-
rl.on('close', () => {
|
|
404
|
-
// Drain the in-flight chain, then resolve (clean shutdown on stdin end).
|
|
405
|
-
chain.then(() => resolveRun()).catch(() => resolveRun());
|
|
406
|
-
});
|
|
407
|
-
});
|
|
408
|
-
}
|