@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 CHANGED
@@ -10,7 +10,7 @@ Installing this package gives you the `webjs` command.
10
10
  Install once, globally:
11
11
 
12
12
  ```sh
13
- npm i -g @webjsdev/cli
13
+ npm i -g webjsdev
14
14
  ```
15
15
 
16
16
  Then scaffold a new app anywhere:
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 @webjsdev/cli` pulls it in
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 @webjsdev/cli');
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
- const { projectCheck } = await import('../lib/check-json.js');
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 channel,
651
- // so nothing here may write to stdout: the data functions are read-only
652
- // and `runMcpServer` routes all diagnostics to stderr. The CLI version is
653
- // advertised in the initialize handshake's serverInfo.
654
- const { readFileSync } = await import('node:fs');
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 pkg = JSON.parse(
658
- readFileSync(join(__dirname, '..', 'package.json'), 'utf8'),
659
- );
660
- version = pkg.version || version;
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 for editor intelligence inside html`` templates.
319
- // @webjsdev/ts-plugin bundles ts-lit-plugin internally, so just one
320
- // plugin entry is needed in tsconfig (see below).
321
- '@webjsdev/ts-plugin': 'latest',
322
- // AI-first component library CLI, preinstalled so `webjs ui add button`
323
- // works immediately after scaffold. Users can remove if they prefer
324
- // to add it later.
325
- '@webjsdev/ui': 'latest',
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/ts-plugin gives the editor:
351
- // • type-check + diagnostics inside html`` templates (via the
352
- // ts-lit-plugin it bundles internally)
353
- // • webjs-aware go-to-definition on custom-element tags
354
- // • "Unknown tag/attribute" suppression for elements registered
355
- // via Class.register('tag-name')
356
- // • attribute auto-complete sourced from `static properties`
357
- // • attribute-value type-check against `declare` annotations
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/ts-plugin' },
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.11",
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
  },
@@ -8,7 +8,7 @@
8
8
  "webjs": {
9
9
  "type": "stdio",
10
10
  "command": "npx",
11
- "args": ["@webjsdev/cli", "mcp"]
11
+ "args": ["@webjsdev/mcp"]
12
12
  }
13
13
  }
14
14
  }
@@ -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
- ts-plugin/ tsserver plugin: go-to-definition + diagnostic suppression
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/ts-plugin`
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/ts-plugin" }
107
+ { "name": "@webjsdev/intellisense" }
108
108
  ]
109
109
  ```
110
110
 
111
- `@webjsdev/ts-plugin` bundles `ts-lit-plugin` internally (it's a runtime
112
- dependency of the plugin) and loads it programmatically, so users
113
- list one entry, not two. You get the full stack of template-literal
114
- intelligence (type-checking, diagnostics, go-to-def inside
115
- `` html`…` `` and `` css`…` `` templates) **plus** webjs-aware behaviour
116
- layered on top:
117
-
118
- - "Unknown tag/attribute" diagnostics are silenced for elements
119
- registered via `Class.register('tag-name')`.
120
- - Attribute auto-complete sourced from each component's
121
- `static properties`.
122
- - Attribute-value type-check against `declare propName: T` annotations.
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.
@@ -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 `ts-lit-plugin` +
658
- `@webjsdev/ts-plugin` setup that extends this to tag / attribute
659
- intelligence inside `html\`…\`` templates (go-to-definition, attribute
660
- auto-complete from `static properties`, no "Unknown tag" red-squiggle on
661
- registered webjs elements).
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
- }