webmcp-codegen 0.3.1 → 0.3.2

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/dist/cli.js CHANGED
@@ -1,28 +1,1392 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- findSpecs,
4
- loadDataFile,
5
- resolveSetup,
6
- saveDataFile
7
- } from "./chunk-CCVNYNJ5.js";
8
- import {
3
+ CONFIG_FILE_NAMES,
4
+ loadConfig,
9
5
  runGenerate
10
6
  } from "./chunk-MJQ5B6HB.js";
11
- import "./chunk-3LTHWIAP.js";
7
+ import {
8
+ js
9
+ } from "./chunk-EAKYM4YS.js";
10
+ import {
11
+ openapi
12
+ } from "./chunk-3LTHWIAP.js";
12
13
  import "./chunk-FWSATV7C.js";
13
- import "./chunk-I4ZL527H.js";
14
14
  import "./chunk-KSQMJERY.js";
15
15
 
16
16
  // src/cli.ts
17
- import { existsSync, watch } from "fs";
18
- import { writeFile as writeFile2 } from "fs/promises";
19
- import { join as join2, relative as relative2 } from "path";
20
- import "readline/promises";
17
+ import { existsSync as existsSync2 } from "fs";
18
+ import { writeFile as writeFile3 } from "fs/promises";
19
+ import { join as join6 } from "path";
21
20
  import { parseArgs } from "util";
22
21
 
23
- // src/wire.ts
22
+ // src/cli-output.ts
23
+ var ESC = "\x1B[";
24
+ var RESET = `${ESC}0m`;
25
+ var BOLD = `${ESC}1m`;
26
+ var DIM = `${ESC}2m`;
27
+ var ITALIC = `${ESC}3m`;
28
+ var FG = {
29
+ red: `${ESC}31m`,
30
+ green: `${ESC}32m`,
31
+ yellow: `${ESC}33m`,
32
+ blue: `${ESC}34m`,
33
+ magenta: `${ESC}35m`,
34
+ cyan: `${ESC}36m`,
35
+ white: `${ESC}37m`,
36
+ gray: `${ESC}90m`
37
+ };
38
+ function c(color, text) {
39
+ return `${FG[color]}${text}${RESET}`;
40
+ }
41
+ function bold(text) {
42
+ return `${BOLD}${text}${RESET}`;
43
+ }
44
+ function dim(text) {
45
+ return `${DIM}${text}${RESET}`;
46
+ }
47
+ function italic(text) {
48
+ return `${ITALIC}${text}${RESET}`;
49
+ }
50
+ function header(title, subtitle) {
51
+ const line = "\u2500".repeat(Math.max(title.length, subtitle.length) + 4);
52
+ return `${c("cyan", line)}
53
+ ${bold(title)}
54
+ ${subtitle}
55
+ ${c("cyan", line)}`;
56
+ }
57
+ function badge(risk) {
58
+ switch (risk) {
59
+ case "read":
60
+ return c("green", "[read]");
61
+ case "write":
62
+ return c("yellow", "[write]");
63
+ case "destructive":
64
+ return c("red", "[destructive]");
65
+ default:
66
+ return `[${risk}]`;
67
+ }
68
+ }
69
+ function groupFindings(findings) {
70
+ const groups = {};
71
+ for (const f of findings) {
72
+ const kind = f.message.includes("sign-in") || f.message.includes("auth") ? "auth" : f.message.includes("Admin") ? "admin" : f.message.includes("PII") || f.message.includes("email") ? "PII" : f.message.includes("POST treated as a read") ? "POST as read" : "other";
73
+ groups[kind] = (groups[kind] ?? 0) + 1;
74
+ }
75
+ return groups;
76
+ }
77
+ function renderSummary(result, setup, _cwd, wiring) {
78
+ const { tools, findings } = result;
79
+ const reads = tools.filter((t) => t.sideEffect === "read").length;
80
+ const writes = tools.filter((t) => t.sideEffect === "write").length;
81
+ const destructives = tools.filter((t) => t.sideEffect === "destructive").length;
82
+ const skipped = result.skipped.length;
83
+ const warnings = findings.filter((f) => f.level === "warning").length;
84
+ const errors = findings.filter((f) => f.level === "error").length;
85
+ console.log("");
86
+ console.log(header("webmcp-codegen", `${tools.length} tools from ${setup.label}`));
87
+ console.log("");
88
+ const summaryParts = [
89
+ c("green", `${reads} read`),
90
+ c("yellow", `${writes} write`),
91
+ destructives > 0 ? c("red", `${destructives} destructive`) : null,
92
+ skipped > 0 ? dim(`${skipped} skipped`) : null
93
+ ].filter(Boolean);
94
+ console.log(` ${summaryParts.join(" ")}
95
+ `);
96
+ if (warnings > 0 || errors > 0) {
97
+ const groups = groupFindings(findings);
98
+ const parts = Object.entries(groups).map(([kind, count]) => {
99
+ const color = kind === "auth" || kind === "admin" ? "yellow" : "gray";
100
+ return `${c(color, kind)} ${count}`;
101
+ });
102
+ console.log(` ${c("yellow", "\u26A0")} ${warnings + errors} finding(s): ${parts.join(", ")}`);
103
+ console.log(dim(` Run with --verbose to see details
104
+ `));
105
+ }
106
+ const outDir = setup.config.generate[0]?.outDir ?? "src/webmcp";
107
+ console.log(` ${c("cyan", "\u2192")} ${bold(outDir)}
108
+ `);
109
+ if (wiring && !wiring.alreadyWired) {
110
+ console.log(` ${c("green", "\u2714")} Registration wired into your app
111
+ `);
112
+ }
113
+ console.log(` ${bold("Next:")} ${c("cyan", "npx webmcp-codegen dev")} to review and test
114
+ `);
115
+ }
116
+ function renderVerbose(result, setup, _cwd) {
117
+ const { tools, findings, skipped } = result;
118
+ console.log("");
119
+ console.log(bold(`webmcp-codegen: ${tools.length} tools from ${setup.label}`));
120
+ console.log("");
121
+ if (skipped.length > 0) {
122
+ console.log(c("gray", "Skipped:"));
123
+ for (const s of skipped) {
124
+ console.log(` ${dim(s.ref)}`);
125
+ console.log(` ${italic(s.reason)}`);
126
+ }
127
+ console.log("");
128
+ }
129
+ const byRisk = {
130
+ read: tools.filter((t) => t.sideEffect === "read"),
131
+ write: tools.filter((t) => t.sideEffect === "write"),
132
+ destructive: tools.filter((t) => t.sideEffect === "destructive")
133
+ };
134
+ for (const [risk, group] of Object.entries(byRisk)) {
135
+ if (group.length === 0) continue;
136
+ console.log(badge(risk));
137
+ for (const tool of group) {
138
+ const disabled = tool.enabledByDefault ? "" : dim(" (starts disabled)");
139
+ console.log(` ${tool.name}${disabled}`);
140
+ if (tool.description) console.log(` ${dim(tool.description)}`);
141
+ }
142
+ console.log("");
143
+ }
144
+ if (findings.length > 0) {
145
+ console.log(bold("Findings:"));
146
+ for (const f of findings) {
147
+ const icon = f.level === "error" ? c("red", "\u2716") : c("yellow", "\u26A0");
148
+ const where = f.tool ? dim(` (${f.tool})`) : "";
149
+ console.log(` ${icon} ${f.message}${where}`);
150
+ }
151
+ console.log("");
152
+ }
153
+ console.log(` ${c("cyan", "\u2192")} Files in ${setup.config.generate[0]?.outDir ?? "src/webmcp"}
154
+ `);
155
+ }
156
+
157
+ // src/data-file.ts
24
158
  import { readFile, writeFile } from "fs/promises";
25
- import { dirname, join, relative } from "path";
159
+ import { join } from "path";
160
+ var DATA_FILE_NAME = ".webmcp-codegen.json";
161
+ async function loadDataFile(cwd) {
162
+ try {
163
+ const parsed = JSON.parse(await readFile(join(cwd, DATA_FILE_NAME), "utf8"));
164
+ return parsed && typeof parsed === "object" ? parsed : {};
165
+ } catch {
166
+ return {};
167
+ }
168
+ }
169
+ async function saveDataFile(cwd, patch) {
170
+ const current = await loadDataFile(cwd);
171
+ const next = { ...current, ...patch };
172
+ if (JSON.stringify(next) === JSON.stringify(current)) return;
173
+ await writeFile(join(cwd, DATA_FILE_NAME), `${JSON.stringify(next, null, 2)}
174
+ `, "utf8");
175
+ }
176
+
177
+ // src/detect.ts
178
+ import { readdir } from "fs/promises";
179
+ import { join as join2, relative } from "path";
180
+ var SPEC_FILE_PATTERN = /^(openapi|swagger|api)\.(ya?ml|json)$/i;
181
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([
182
+ "node_modules",
183
+ ".git",
184
+ ".turbo",
185
+ ".next",
186
+ "dist",
187
+ "build",
188
+ "coverage"
189
+ ]);
190
+ var MAX_DEPTH = 5;
191
+ async function findSpecs(cwd) {
192
+ const found = [];
193
+ async function walk(dir, depth) {
194
+ if (depth > MAX_DEPTH) return;
195
+ let entries;
196
+ try {
197
+ entries = await readdir(dir, { withFileTypes: true });
198
+ } catch {
199
+ return;
200
+ }
201
+ for (const entry of entries) {
202
+ if (entry.isDirectory()) {
203
+ if (!IGNORED_DIRS.has(entry.name)) await walk(join2(dir, entry.name), depth + 1);
204
+ } else if (SPEC_FILE_PATTERN.test(entry.name)) {
205
+ found.push({ path: join2(dir, entry.name), depth });
206
+ }
207
+ }
208
+ }
209
+ await walk(cwd, 0);
210
+ return found.sort((a, b) => a.depth - b.depth).map((entry) => relative(cwd, entry.path));
211
+ }
212
+
213
+ // src/dev/server.ts
214
+ import { spawn } from "child_process";
215
+ import { createServer } from "http";
216
+
217
+ // src/setup.ts
218
+ import { existsSync } from "fs";
219
+ import { basename, join as join4 } from "path";
220
+ import { createInterface } from "readline/promises";
221
+
222
+ // src/detect-app.ts
223
+ import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
224
+ import { join as join3 } from "path";
225
+ var FRAMEWORKS = [
226
+ { dep: "next", framework: "next" },
227
+ { dep: "nuxt", framework: "nuxt" },
228
+ { dep: "@sveltejs/kit", framework: "sveltekit" }
229
+ ];
230
+ async function findWebApps(cwd) {
231
+ const packageDirs = await findPackageDirs(cwd);
232
+ const apps = [];
233
+ for (const dir of packageDirs) {
234
+ const pkg = await readPackageJson(join3(cwd, dir));
235
+ if (!pkg) continue;
236
+ const deps = {
237
+ ...pkg.dependencies,
238
+ ...pkg.devDependencies
239
+ };
240
+ const known = FRAMEWORKS.find(({ dep }) => deps[dep]);
241
+ const framework = known?.framework ?? (deps.react && deps.vite ? "vite-react" : void 0);
242
+ if (framework) apps.push({ dir, framework });
243
+ }
244
+ return apps.sort((a, b) => score(b) - score(a));
245
+ function score(app) {
246
+ return (app.framework === "unknown" ? 0 : 10) + (/(^|\/)(web|app|frontend|client)$/.test(app.dir) ? 2 : 0);
247
+ }
248
+ }
249
+ async function findPackageDirs(cwd) {
250
+ const dirs = [];
251
+ const root = await readPackageJson(join3(cwd, ""));
252
+ if (root) {
253
+ dirs.push(".");
254
+ for (const pattern of await workspaceGlobs(cwd, root)) {
255
+ dirs.push(...await expandShallowGlob(cwd, pattern));
256
+ }
257
+ }
258
+ return [...new Set(dirs)];
259
+ }
260
+ async function workspaceGlobs(cwd, rootPkg) {
261
+ const workspaces = rootPkg.workspaces;
262
+ if (Array.isArray(workspaces)) return workspaces;
263
+ if (workspaces && typeof workspaces === "object" && Array.isArray(workspaces.packages)) {
264
+ return workspaces.packages;
265
+ }
266
+ return readPnpmWorkspaceGlobs(cwd);
267
+ }
268
+ async function readPnpmWorkspaceGlobs(cwd) {
269
+ try {
270
+ const text = await readFile2(join3(cwd, "pnpm-workspace.yaml"), "utf8");
271
+ const packagesBlock = /^packages:\s*\n((?:\s+-\s+.+\n?)+)/m.exec(text);
272
+ if (!packagesBlock) return [];
273
+ return [...packagesBlock[1].matchAll(/^\s+-\s+['"]?([^'"\n]+?)['"]?\s*$/gm)].map(
274
+ (match) => match[1]
275
+ );
276
+ } catch {
277
+ return [];
278
+ }
279
+ }
280
+ async function expandShallowGlob(cwd, pattern) {
281
+ const starAt = pattern.indexOf("*");
282
+ const base = starAt === -1 ? pattern : pattern.slice(0, starAt).replace(/\/$/, "");
283
+ if (starAt === -1) return [base];
284
+ try {
285
+ const entries = await readdir2(join3(cwd, base), { withFileTypes: true });
286
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => `${base}/${entry.name}`);
287
+ } catch {
288
+ return [];
289
+ }
290
+ }
291
+ async function readPackageJson(dir) {
292
+ try {
293
+ return JSON.parse(await readFile2(join3(dir, "package.json"), "utf8"));
294
+ } catch {
295
+ return void 0;
296
+ }
297
+ }
298
+
299
+ // src/setup.ts
300
+ async function resolveSetup(cwd, flags) {
301
+ const hasConfigFile = flags.configPath ? existsSync(join4(cwd, flags.configPath)) : CONFIG_FILE_NAMES.some((name) => existsSync(join4(cwd, name)));
302
+ if (hasConfigFile) {
303
+ const { config, path } = await loadConfig(cwd, flags.configPath);
304
+ if (flags.spec || flags.out) {
305
+ console.warn(`Note: --spec/--out are ignored; ${basename(path)} is in charge here.`);
306
+ }
307
+ const data2 = await loadDataFile(cwd);
308
+ const apps = await findWebApps(cwd);
309
+ const app2 = apps.find((candidate) => candidate.dir === data2.app) ?? apps[0];
310
+ return { config, label: basename(path), app: app2, fromConfigFile: true, remember: {} };
311
+ }
312
+ if (flags.configPath) {
313
+ throw new Error(`No config file at "${flags.configPath}".`);
314
+ }
315
+ const data = await loadDataFile(cwd);
316
+ const spec = flags.spec ?? data.spec ?? await detectSpec(cwd);
317
+ let app;
318
+ if (!flags.out) {
319
+ const apps = await findWebApps(cwd);
320
+ const remembered = apps.find((candidate) => candidate.dir === data.app);
321
+ if (remembered) {
322
+ app = remembered;
323
+ } else if (apps.length === 1) {
324
+ app = apps[0];
325
+ console.log(`Found your web app: ${app?.dir} (${app?.framework})`);
326
+ } else if (apps.length > 1) {
327
+ app = await askWhichApp(apps);
328
+ }
329
+ }
330
+ const outDir = flags.out ?? (app && app.dir !== "." ? `${app.dir}/src/webmcp` : "./src/webmcp");
331
+ return {
332
+ config: { sources: [openapi({ spec })], generate: [js({ outDir })] },
333
+ label: flags.spec ? `--spec ${spec}` : `detected ${spec}`,
334
+ app,
335
+ fromConfigFile: false,
336
+ remember: { spec, app: app?.dir }
337
+ };
338
+ }
339
+ async function askWhichApp(apps) {
340
+ if (!process.stdin.isTTY) {
341
+ const first = apps[0];
342
+ console.log(`Several packages look like web apps; using ${first.dir}. Override with --out.`);
343
+ return first;
344
+ }
345
+ console.log("Several packages look like the web app. Which one should the tools live in?");
346
+ apps.forEach((app, index) => {
347
+ console.log(` ${index + 1}. ${app.dir} (${app.framework})${index === 0 ? " [default]" : ""}`);
348
+ });
349
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
350
+ try {
351
+ const answer = await rl.question("Choice [1]: ");
352
+ const picked = Number.parseInt(answer.trim() || "1", 10);
353
+ return apps[picked - 1] ?? apps[0];
354
+ } finally {
355
+ rl.close();
356
+ }
357
+ }
358
+ async function detectSpec(cwd) {
359
+ const specs = await findSpecs(cwd);
360
+ if (specs.length === 0) {
361
+ throw new Error(
362
+ "No OpenAPI spec found in this project.\nPoint at one: npx webmcp-codegen generate --spec path/to/openapi.json"
363
+ );
364
+ }
365
+ if (specs.length > 1) {
366
+ const list = specs.map((spec) => ` - ${spec}`).join("\n");
367
+ throw new Error(
368
+ `Found ${specs.length} API specs:
369
+ ${list}
370
+
371
+ Pick one: npx webmcp-codegen generate --spec ${specs[0]}`
372
+ );
373
+ }
374
+ console.log(`Detected ${specs[0]} (override with --spec)`);
375
+ return specs[0];
376
+ }
377
+
378
+ // src/dev/ui.ts
379
+ function dashboardHtml() {
380
+ return `<!DOCTYPE html>
381
+ <html lang="en">
382
+ <head>
383
+ <meta charset="utf-8" />
384
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
385
+ <title>webmcp-codegen</title>
386
+ <style>
387
+ :root {
388
+ --baseline: #0a0b0f;
389
+ --surface: #10131a;
390
+ --surface-raised: #161a23;
391
+ --line: #1e2330;
392
+ --line-subtle: #161a23;
393
+ --ink: #e9ecf2;
394
+ --dim: #9aa3b2;
395
+ --faint: #5d6575;
396
+ --ghost: #3b4150;
397
+ --accent: #58a6ff;
398
+ --accent-dim: rgba(88, 166, 255, 0.15);
399
+ --signal: #e3b341;
400
+ --signal-dim: rgba(227, 179, 65, 0.15);
401
+ --fault: #f47067;
402
+ --fault-dim: rgba(244, 112, 103, 0.15);
403
+ --sans: ui-sans-serif, system-ui, -apple-system, sans-serif;
404
+ --mono: ui-monospace, SFMono-Regular, Menlo, monospace;
405
+ }
406
+ * { box-sizing: border-box; }
407
+ html, body { margin: 0; height: 100%; }
408
+ body {
409
+ background: var(--baseline);
410
+ color: var(--ink);
411
+ font-family: var(--sans);
412
+ font-size: 14px;
413
+ -webkit-font-smoothing: antialiased;
414
+ overflow: hidden;
415
+ }
416
+ ::selection { background: var(--accent); color: var(--baseline); }
417
+
418
+ /* Layout */
419
+ .app { display: flex; height: 100vh; }
420
+ .sidebar {
421
+ width: 320px;
422
+ min-width: 320px;
423
+ border-right: 1px solid var(--line);
424
+ display: flex;
425
+ flex-direction: column;
426
+ background: var(--surface);
427
+ }
428
+ .main {
429
+ flex: 1;
430
+ overflow-y: auto;
431
+ background: var(--baseline);
432
+ }
433
+
434
+ /* Sidebar header */
435
+ .sidebar-header {
436
+ padding: 20px 20px 16px;
437
+ border-bottom: 1px solid var(--line-subtle);
438
+ }
439
+ .brand {
440
+ display: flex;
441
+ align-items: center;
442
+ gap: 10px;
443
+ font-weight: 600;
444
+ font-size: 15px;
445
+ margin-bottom: 4px;
446
+ }
447
+ .brand-mark {
448
+ width: 24px;
449
+ height: 24px;
450
+ background: linear-gradient(135deg, var(--accent), #7c3aed);
451
+ border-radius: 6px;
452
+ display: flex;
453
+ align-items: center;
454
+ justify-content: center;
455
+ font-size: 12px;
456
+ font-weight: 700;
457
+ color: white;
458
+ }
459
+ .brand-sub {
460
+ color: var(--faint);
461
+ font-size: 12px;
462
+ }
463
+
464
+ /* Search */
465
+ .search-wrap {
466
+ padding: 12px 16px;
467
+ border-bottom: 1px solid var(--line-subtle);
468
+ }
469
+ .search {
470
+ width: 100%;
471
+ background: var(--surface-raised);
472
+ border: 1px solid var(--line);
473
+ border-radius: 6px;
474
+ padding: 8px 12px 8px 32px;
475
+ color: var(--ink);
476
+ font-size: 13px;
477
+ font-family: inherit;
478
+ position: relative;
479
+ }
480
+ .search:focus {
481
+ outline: none;
482
+ border-color: var(--accent);
483
+ }
484
+ .search-icon {
485
+ position: absolute;
486
+ left: 28px;
487
+ top: 50%;
488
+ transform: translateY(-50%);
489
+ color: var(--faint);
490
+ pointer-events: none;
491
+ }
492
+ .search-wrap { position: relative; }
493
+
494
+ /* Tool list */
495
+ .tool-list {
496
+ flex: 1;
497
+ overflow-y: auto;
498
+ padding: 8px 0;
499
+ }
500
+ .tool-group {
501
+ padding: 8px 16px 4px;
502
+ font-size: 11px;
503
+ font-weight: 600;
504
+ text-transform: uppercase;
505
+ letter-spacing: 0.05em;
506
+ color: var(--faint);
507
+ }
508
+ .tool {
509
+ display: flex;
510
+ align-items: center;
511
+ gap: 10px;
512
+ width: 100%;
513
+ padding: 8px 16px;
514
+ border: none;
515
+ background: none;
516
+ color: var(--ink);
517
+ font-size: 13px;
518
+ font-family: var(--mono);
519
+ text-align: left;
520
+ cursor: pointer;
521
+ transition: background 0.1s;
522
+ }
523
+ .tool:hover { background: var(--surface-raised); }
524
+ .tool[aria-selected="true"] {
525
+ background: var(--accent-dim);
526
+ border-right: 2px solid var(--accent);
527
+ }
528
+ .tool-indicator {
529
+ width: 6px;
530
+ height: 6px;
531
+ border-radius: 50%;
532
+ flex-shrink: 0;
533
+ }
534
+ .tool-indicator.read { background: var(--accent); }
535
+ .tool-indicator.write { background: var(--signal); }
536
+ .tool-indicator.destructive { background: var(--fault); }
537
+ .tool-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
538
+ .tool-badge {
539
+ font-size: 10px;
540
+ padding: 2px 6px;
541
+ border-radius: 4px;
542
+ background: var(--surface-raised);
543
+ color: var(--dim);
544
+ text-transform: uppercase;
545
+ letter-spacing: 0.02em;
546
+ }
547
+ .tool-badge.disabled { color: var(--signal); }
548
+
549
+ /* Main content */
550
+ .detail {
551
+ max-width: 640px;
552
+ margin: 0 auto;
553
+ padding: 32px 40px;
554
+ }
555
+ .placeholder {
556
+ display: flex;
557
+ flex-direction: column;
558
+ align-items: center;
559
+ justify-content: center;
560
+ height: 100%;
561
+ color: var(--faint);
562
+ text-align: center;
563
+ padding: 40px;
564
+ }
565
+ .placeholder-icon {
566
+ width: 48px;
567
+ height: 48px;
568
+ border-radius: 12px;
569
+ background: var(--surface-raised);
570
+ display: flex;
571
+ align-items: center;
572
+ justify-content: center;
573
+ margin-bottom: 16px;
574
+ color: var(--ghost);
575
+ }
576
+ .placeholder kbd {
577
+ background: var(--surface-raised);
578
+ padding: 2px 6px;
579
+ border-radius: 4px;
580
+ font-family: var(--mono);
581
+ font-size: 12px;
582
+ }
583
+
584
+ /* Detail header */
585
+ .detail-header {
586
+ margin-bottom: 24px;
587
+ padding-bottom: 20px;
588
+ border-bottom: 1px solid var(--line-subtle);
589
+ }
590
+ .detail-crumb {
591
+ font-size: 12px;
592
+ color: var(--faint);
593
+ margin-bottom: 8px;
594
+ font-family: var(--mono);
595
+ }
596
+ .detail-title {
597
+ font-size: 24px;
598
+ font-weight: 600;
599
+ margin: 0 0 8px;
600
+ font-family: var(--mono);
601
+ }
602
+ .detail-route {
603
+ font-family: var(--mono);
604
+ font-size: 13px;
605
+ color: var(--dim);
606
+ display: flex;
607
+ align-items: center;
608
+ gap: 8px;
609
+ }
610
+ .verb {
611
+ font-weight: 600;
612
+ padding: 2px 6px;
613
+ border-radius: 4px;
614
+ font-size: 11px;
615
+ }
616
+ .verb.read { color: var(--accent); background: var(--accent-dim); }
617
+ .verb.write { color: var(--signal); background: var(--signal-dim); }
618
+ .verb.destructive { color: var(--fault); background: var(--fault-dim); }
619
+
620
+ /* Badges */
621
+ .badges {
622
+ display: flex;
623
+ gap: 8px;
624
+ margin-top: 12px;
625
+ flex-wrap: wrap;
626
+ }
627
+ .badge {
628
+ font-size: 11px;
629
+ padding: 3px 8px;
630
+ border-radius: 4px;
631
+ font-weight: 500;
632
+ }
633
+ .badge.read { color: var(--accent); background: var(--accent-dim); }
634
+ .badge.write { color: var(--signal); background: var(--signal-dim); }
635
+ .badge.destructive { color: var(--fault); background: var(--fault-dim); }
636
+ .badge.disabled { color: var(--signal); background: var(--signal-dim); }
637
+ .badge.auth { color: var(--fault); background: var(--fault-dim); }
638
+
639
+ /* Sections */
640
+ .section {
641
+ margin-bottom: 28px;
642
+ }
643
+ .section-label {
644
+ font-size: 11px;
645
+ font-weight: 600;
646
+ text-transform: uppercase;
647
+ letter-spacing: 0.05em;
648
+ color: var(--faint);
649
+ margin-bottom: 10px;
650
+ }
651
+
652
+ /* Description edit */
653
+ .description-edit {
654
+ width: 100%;
655
+ background: var(--surface);
656
+ border: 1px solid var(--line);
657
+ border-radius: 6px;
658
+ padding: 12px;
659
+ color: var(--ink);
660
+ font-size: 14px;
661
+ font-family: inherit;
662
+ line-height: 1.5;
663
+ resize: vertical;
664
+ min-height: 80px;
665
+ }
666
+ .description-edit:focus {
667
+ outline: none;
668
+ border-color: var(--accent);
669
+ }
670
+ .edit-actions {
671
+ display: flex;
672
+ align-items: center;
673
+ gap: 12px;
674
+ margin-top: 10px;
675
+ }
676
+ .btn {
677
+ padding: 8px 16px;
678
+ border-radius: 6px;
679
+ font-size: 13px;
680
+ font-weight: 500;
681
+ cursor: pointer;
682
+ transition: all 0.15s;
683
+ border: 1px solid var(--line);
684
+ background: var(--surface);
685
+ color: var(--ink);
686
+ }
687
+ .btn:hover { background: var(--surface-raised); border-color: var(--ghost); }
688
+ .btn-primary {
689
+ background: var(--accent);
690
+ border-color: var(--accent);
691
+ color: var(--baseline);
692
+ }
693
+ .btn-primary:hover { background: #4a95ee; border-color: #4a95ee; }
694
+ .saved-indicator {
695
+ font-size: 12px;
696
+ color: var(--accent);
697
+ opacity: 0;
698
+ transition: opacity 0.2s;
699
+ }
700
+ .saved-indicator.show { opacity: 1; }
701
+ .edit-hint {
702
+ font-size: 12px;
703
+ color: var(--faint);
704
+ margin-top: 8px;
705
+ line-height: 1.5;
706
+ }
707
+
708
+ /* Toggle */
709
+ .toggle-row {
710
+ display: flex;
711
+ align-items: center;
712
+ gap: 12px;
713
+ padding: 14px;
714
+ background: var(--surface);
715
+ border: 1px solid var(--line);
716
+ border-radius: 8px;
717
+ }
718
+ .switch {
719
+ width: 40px;
720
+ height: 22px;
721
+ border-radius: 11px;
722
+ background: var(--surface-raised);
723
+ border: 1px solid var(--line);
724
+ position: relative;
725
+ cursor: pointer;
726
+ transition: all 0.2s;
727
+ flex-shrink: 0;
728
+ }
729
+ .switch::after {
730
+ content: "";
731
+ position: absolute;
732
+ width: 16px;
733
+ height: 16px;
734
+ border-radius: 50%;
735
+ background: var(--dim);
736
+ top: 2px;
737
+ left: 2px;
738
+ transition: all 0.2s;
739
+ }
740
+ .switch[aria-checked="true"] {
741
+ background: var(--accent);
742
+ border-color: var(--accent);
743
+ }
744
+ .switch[aria-checked="true"]::after {
745
+ left: 20px;
746
+ background: white;
747
+ }
748
+ .toggle-copy { font-size: 13px; line-height: 1.5; }
749
+ .toggle-copy strong { display: block; margin-bottom: 2px; }
750
+
751
+ /* Try it */
752
+ .try-section {
753
+ background: var(--surface);
754
+ border: 1px solid var(--line);
755
+ border-radius: 8px;
756
+ overflow: hidden;
757
+ }
758
+ .try-header {
759
+ padding: 14px 16px;
760
+ border-bottom: 1px solid var(--line-subtle);
761
+ display: flex;
762
+ align-items: center;
763
+ justify-content: space-between;
764
+ }
765
+ .try-header h3 {
766
+ margin: 0;
767
+ font-size: 13px;
768
+ font-weight: 600;
769
+ }
770
+ .try-note {
771
+ font-size: 11px;
772
+ color: var(--faint);
773
+ }
774
+ .try-body { padding: 16px; }
775
+ .auth-note {
776
+ background: var(--signal-dim);
777
+ border: 1px solid var(--signal);
778
+ color: var(--signal);
779
+ padding: 10px 12px;
780
+ border-radius: 6px;
781
+ font-size: 12px;
782
+ margin-bottom: 14px;
783
+ line-height: 1.5;
784
+ }
785
+ .base-url-input {
786
+ width: 100%;
787
+ background: var(--baseline);
788
+ border: 1px solid var(--line);
789
+ border-radius: 6px;
790
+ padding: 8px 12px;
791
+ color: var(--ink);
792
+ font-size: 13px;
793
+ font-family: var(--mono);
794
+ margin-bottom: 14px;
795
+ }
796
+ .base-url-input:focus {
797
+ outline: none;
798
+ border-color: var(--accent);
799
+ }
800
+ .param-list { margin-bottom: 14px; }
801
+ .param {
802
+ margin-bottom: 12px;
803
+ }
804
+ .param-label {
805
+ display: block;
806
+ font-size: 12px;
807
+ font-weight: 500;
808
+ margin-bottom: 4px;
809
+ color: var(--dim);
810
+ }
811
+ .param-label .req { color: var(--fault); }
812
+ .param-hint {
813
+ font-size: 11px;
814
+ color: var(--faint);
815
+ margin-top: 2px;
816
+ }
817
+ .param-input {
818
+ width: 100%;
819
+ background: var(--baseline);
820
+ border: 1px solid var(--line);
821
+ border-radius: 6px;
822
+ padding: 8px 12px;
823
+ color: var(--ink);
824
+ font-size: 13px;
825
+ font-family: var(--mono);
826
+ }
827
+ .param-input:focus {
828
+ outline: none;
829
+ border-color: var(--accent);
830
+ }
831
+ .run-btn {
832
+ width: 100%;
833
+ padding: 10px;
834
+ background: var(--accent);
835
+ border: none;
836
+ border-radius: 6px;
837
+ color: var(--baseline);
838
+ font-size: 13px;
839
+ font-weight: 600;
840
+ cursor: pointer;
841
+ transition: background 0.15s;
842
+ }
843
+ .run-btn:hover { background: #4a95ee; }
844
+ .run-btn:disabled { opacity: 0.5; cursor: not-allowed; }
845
+ .result {
846
+ margin-top: 14px;
847
+ padding: 12px;
848
+ background: var(--baseline);
849
+ border: 1px solid var(--line);
850
+ border-radius: 6px;
851
+ font-family: var(--mono);
852
+ font-size: 12px;
853
+ white-space: pre-wrap;
854
+ word-break: break-all;
855
+ max-height: 300px;
856
+ overflow-y: auto;
857
+ }
858
+ .result.ok { border-color: var(--accent); }
859
+ .result.err { border-color: var(--fault); }
860
+
861
+ /* Findings */
862
+ .findings {
863
+ margin-bottom: 20px;
864
+ }
865
+ .finding {
866
+ display: flex;
867
+ gap: 8px;
868
+ padding: 10px 12px;
869
+ background: var(--surface);
870
+ border: 1px solid var(--line);
871
+ border-radius: 6px;
872
+ margin-bottom: 8px;
873
+ font-size: 13px;
874
+ line-height: 1.5;
875
+ }
876
+ .finding.warning { border-left: 3px solid var(--signal); }
877
+ .finding.error { border-left: 3px solid var(--fault); }
878
+ .finding-icon { flex-shrink: 0; }
879
+
880
+ /* Scrollbar */
881
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
882
+ ::-webkit-scrollbar-track { background: transparent; }
883
+ ::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }
884
+ ::-webkit-scrollbar-thumb:hover { background: var(--ghost); }
885
+ </style>
886
+ </head>
887
+ <body>
888
+ <div class="app">
889
+ <aside class="sidebar">
890
+ <div class="sidebar-header">
891
+ <div class="brand">
892
+ <div class="brand-mark">W</div>
893
+ <span>webmcp-codegen</span>
894
+ </div>
895
+ <div class="brand-sub" id="tool-count"></div>
896
+ </div>
897
+ <div class="search-wrap">
898
+ <svg class="search-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
899
+ <circle cx="11" cy="11" r="8"></circle>
900
+ <path d="m21 21-4.35-4.35"></path>
901
+ </svg>
902
+ <input type="text" class="search" id="search" placeholder="Search tools..." spellcheck="false" />
903
+ </div>
904
+ <div class="tool-list" id="tool-list"></div>
905
+ </aside>
906
+ <main class="main" id="main">
907
+ <div class="placeholder" id="placeholder">
908
+ <div class="placeholder-icon">
909
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
910
+ <path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
911
+ </svg>
912
+ </div>
913
+ <p>Select a tool to view details</p>
914
+ <p style="font-size: 12px; margin-top: 8px;">
915
+ <kbd>\u2191</kbd> <kbd>\u2193</kbd> to navigate &nbsp;\xB7&nbsp; <kbd>\u2318K</kbd> to search
916
+ </p>
917
+ </div>
918
+ <div class="detail" id="detail" hidden></div>
919
+ </main>
920
+ </div>
921
+
922
+ <script>
923
+ (function () {
924
+ var state = null;
925
+ var selected = null;
926
+ var filter = "";
927
+
928
+ var listEl = document.getElementById("tool-list");
929
+ var detailEl = document.getElementById("detail");
930
+ var placeholderEl = document.getElementById("placeholder");
931
+ var searchEl = document.getElementById("search");
932
+ var countEl = document.getElementById("tool-count");
933
+
934
+ function esc(text) {
935
+ var div = document.createElement("div");
936
+ div.textContent = text == null ? "" : String(text);
937
+ return div.innerHTML;
938
+ }
939
+
940
+ function api(path, options) {
941
+ return fetch(path, options).then(function (res) {
942
+ if (!res.ok) throw new Error("Request failed: " + res.status);
943
+ return res.json();
944
+ });
945
+ }
946
+
947
+ function load() {
948
+ api("/api/state").then(function (data) {
949
+ state = data;
950
+ countEl.textContent = data.tools.length + " tools from " + data.label;
951
+ renderList();
952
+ renderDetail();
953
+ });
954
+ }
955
+
956
+ function visibleTools() {
957
+ if (!state) return [];
958
+ var f = filter.toLowerCase();
959
+ return state.tools.filter(function (tool) {
960
+ return tool.name.toLowerCase().indexOf(f) !== -1 ||
961
+ (tool.description && tool.description.toLowerCase().indexOf(f) !== -1);
962
+ });
963
+ }
964
+
965
+ function groupTools(tools) {
966
+ var groups = { read: [], write: [], destructive: [] };
967
+ tools.forEach(function (tool) {
968
+ var key = tool.sideEffect || "read";
969
+ if (!groups[key]) groups[key] = [];
970
+ groups[key].push(tool);
971
+ });
972
+ return groups;
973
+ }
974
+
975
+ function renderList() {
976
+ var tools = visibleTools();
977
+ var groups = groupTools(tools);
978
+ var html = "";
979
+
980
+ ["read", "write", "destructive"].forEach(function (risk) {
981
+ var group = groups[risk];
982
+ if (!group || group.length === 0) return;
983
+ html += '<div class="tool-group">' + risk + ' (' + group.length + ')</div>';
984
+ group.forEach(function (tool) {
985
+ var isSelected = tool.name === selected;
986
+ html += '<button class="tool" data-name="' + esc(tool.name) + '" aria-selected="' + isSelected + '">' +
987
+ '<span class="tool-indicator ' + risk + '"></span>' +
988
+ '<span class="tool-name">' + esc(tool.name) + "</span>" +
989
+ (!tool.enabled ? '<span class="tool-badge disabled">off</span>' : "") +
990
+ "</button>";
991
+ });
992
+ });
993
+
994
+ if (tools.length === 0) {
995
+ html = '<div style="padding: 20px; text-align: center; color: var(--faint);">No tools match your search</div>';
996
+ }
997
+
998
+ listEl.innerHTML = html;
999
+
1000
+ Array.prototype.forEach.call(listEl.querySelectorAll(".tool"), function (btn) {
1001
+ btn.addEventListener("click", function () {
1002
+ selected = btn.getAttribute("data-name");
1003
+ renderList();
1004
+ renderDetail();
1005
+ });
1006
+ });
1007
+ }
1008
+
1009
+ function currentTool() {
1010
+ if (!state || !selected) return null;
1011
+ return state.tools.find(function (tool) { return tool.name === selected; });
1012
+ }
1013
+
1014
+ function renderDetail() {
1015
+ var tool = currentTool();
1016
+ if (!tool) {
1017
+ detailEl.hidden = true;
1018
+ placeholderEl.hidden = false;
1019
+ return;
1020
+ }
1021
+
1022
+ placeholderEl.hidden = true;
1023
+ detailEl.hidden = false;
1024
+
1025
+ var badges = [
1026
+ '<span class="badge ' + tool.sideEffect + '">' + tool.sideEffect + "</span>",
1027
+ !tool.enabled ? '<span class="badge disabled">starts disabled</span>' : "",
1028
+ tool.endpointRole !== "endpoint" ? '<span class="badge auth">' + tool.endpointRole + "</span>" : "",
1029
+ tool.piiInOutput.length > 0 ? '<span class="badge write">pii: ' + esc(tool.piiInOutput.join(", ")) + "</span>" : "",
1030
+ ].filter(Boolean).join("");
1031
+
1032
+ var findings = tool.findings.map(function (finding) {
1033
+ var icon = finding.level === "error" ? "\u2716" : "\u26A0";
1034
+ return '<div class="finding ' + finding.level + '"><span class="finding-icon">' + icon + "</span><span>" + esc(finding.message) + "</span></div>";
1035
+ }).join("");
1036
+
1037
+ var schema = tool.inputSchema || {};
1038
+ var properties = schema.properties || {};
1039
+ var required = schema.required || [];
1040
+ var fields = Object.keys(properties).map(function (key) {
1041
+ var field = properties[key];
1042
+ var type = field.type === "number" || field.type === "integer" ? "number" : "text";
1043
+ var req = required.indexOf(key) !== -1 ? ' <span class="req">*</span>' : "";
1044
+ var hint = field.description ? '<div class="param-hint">' + esc(field.description) + "</div>" : "";
1045
+ return '<div class="param"><label class="param-label">' + esc(key) + req + '</label>' +
1046
+ '<input class="param-input" data-field="' + esc(key) + '" data-type="' + esc(field.type || "string") + '" type="' + type + '" spellcheck="false" />' +
1047
+ hint + "</div>";
1048
+ }).join("");
1049
+
1050
+ var baseUrl = "";
1051
+ try { baseUrl = localStorage.getItem("webmcp-codegen:baseUrl") || tool.serverUrl || ""; } catch (e) {}
1052
+
1053
+ detailEl.innerHTML =
1054
+ '<div class="detail-header">' +
1055
+ '<div class="detail-crumb">' + esc(state.label) + (state.outDir ? " \u2192 " + esc(state.outDir) : "") + "</div>" +
1056
+ '<h1 class="detail-title">' + esc(tool.name) + "</h1>" +
1057
+ '<div class="detail-route">' +
1058
+ '<span class="verb ' + tool.sideEffect + '">' + esc(tool.verb || "GET") + "</span>" +
1059
+ "<span>" + esc(tool.path || "") + "</span>" +
1060
+ "</div>" +
1061
+ '<div class="badges">' + badges + "</div>" +
1062
+ "</div>" +
1063
+
1064
+ (findings ? '<div class="section"><div class="section-label">Audit findings</div>' + findings + "</div>" : "") +
1065
+
1066
+ '<div class="section">' +
1067
+ '<div class="section-label">Description</div>' +
1068
+ '<textarea class="description-edit" id="desc" spellcheck="false">' + esc(tool.description) + "</textarea>" +
1069
+ '<div class="edit-actions">' +
1070
+ '<button class="btn btn-primary" id="save-desc">Save</button>' +
1071
+ '<span class="saved-indicator" id="saved">Saved</span>' +
1072
+ "</div>" +
1073
+ '<div class="edit-hint">Agents pick tools by this text. Saved to .webmcp-codegen.json, so it survives regeneration. \u2318S to save.</div>' +
1074
+ "</div>" +
1075
+
1076
+ '<div class="section">' +
1077
+ '<div class="section-label">Status</div>' +
1078
+ '<div class="toggle-row">' +
1079
+ '<button class="switch" id="toggle-enabled" role="switch" aria-checked="' + tool.enabled + '" aria-label="Enabled"></button>' +
1080
+ '<div class="toggle-copy"><strong>' + (tool.enabled ? "Enabled" : "Disabled") + "</strong>" +
1081
+ (tool.enabled
1082
+ ? "This tool works as soon as the app registers it."
1083
+ : "The generated code is there, commented out. Flipping this regenerates it enabled on the next run.") +
1084
+ "</div></div>" +
1085
+ "</div>" +
1086
+
1087
+ '<div class="section">' +
1088
+ '<div class="section-label">Test</div>' +
1089
+ '<div class="try-section">' +
1090
+ '<div class="try-header"><h3>Run this tool</h3><span class="try-note">server-side, no browser session</span></div>' +
1091
+ '<div class="try-body">' +
1092
+ (tool.requiresAuth
1093
+ ? '<div class="auth-note">\u26A0 This endpoint requires a browser session. The dashboard runs server-side, so you will get a 401. Test it in Chrome DevTools where you are signed in.</div>'
1094
+ : "") +
1095
+ '<input class="base-url-input" id="base-url" type="text" placeholder="Base URL (e.g. http://localhost:3000)" value="' + esc(baseUrl) + '" spellcheck="false" />' +
1096
+ (fields || '<div style="color: var(--faint); font-size: 13px; margin-bottom: 14px;">This tool takes no inputs.</div>') +
1097
+ '<button class="run-btn" id="run">Run tool</button>' +
1098
+ '<pre class="result" id="result" hidden></pre>' +
1099
+ "</div></div>" +
1100
+ "</div>";
1101
+
1102
+ document.getElementById("save-desc").addEventListener("click", saveDescription);
1103
+ document.getElementById("toggle-enabled").addEventListener("click", toggleEnabled);
1104
+ document.getElementById("run").addEventListener("click", runTool);
1105
+ document.getElementById("base-url").addEventListener("change", function (event) {
1106
+ try { localStorage.setItem("webmcp-codegen:baseUrl", event.target.value); } catch (e) {}
1107
+ });
1108
+ }
1109
+
1110
+ function saveDescription() {
1111
+ var tool = currentTool();
1112
+ var desc = document.getElementById("desc").value.trim();
1113
+ if (!tool || !desc) return;
1114
+ api("/api/override", {
1115
+ method: "POST",
1116
+ headers: { "content-type": "application/json" },
1117
+ body: JSON.stringify({ name: tool.name, description: desc }),
1118
+ }).then(function () {
1119
+ tool.description = desc;
1120
+ var saved = document.getElementById("saved");
1121
+ saved.classList.add("show");
1122
+ setTimeout(function () { saved.classList.remove("show"); }, 2000);
1123
+ }).catch(function (error) { alert(error.message); });
1124
+ }
1125
+
1126
+ function toggleEnabled() {
1127
+ var tool = currentTool();
1128
+ if (!tool) return;
1129
+ var next = !tool.enabled;
1130
+ api("/api/override", {
1131
+ method: "POST",
1132
+ headers: { "content-type": "application/json" },
1133
+ body: JSON.stringify({ name: tool.name, enabled: next }),
1134
+ }).then(function () {
1135
+ tool.enabled = next;
1136
+ renderList();
1137
+ renderDetail();
1138
+ }).catch(function (error) { alert(error.message); });
1139
+ }
1140
+
1141
+ function runTool() {
1142
+ var tool = currentTool();
1143
+ if (!tool) return;
1144
+ var input = {};
1145
+ Array.prototype.forEach.call(document.querySelectorAll("[data-field]"), function (field) {
1146
+ var value = field.value;
1147
+ if (value === "") return;
1148
+ var type = field.getAttribute("data-type");
1149
+ if (type === "number" || type === "integer") value = Number(value);
1150
+ if (type === "boolean") value = value === "true";
1151
+ if (type === "object" || type === "array") {
1152
+ try { value = JSON.parse(value); } catch (e) { /* keep as string */ }
1153
+ }
1154
+ input[field.getAttribute("data-field")] = value;
1155
+ });
1156
+ var baseUrl = document.getElementById("base-url").value.trim();
1157
+ var resultEl = document.getElementById("result");
1158
+ var runEl = document.getElementById("run");
1159
+ runEl.disabled = true;
1160
+ runEl.textContent = "Running...";
1161
+ resultEl.hidden = true;
1162
+ api("/api/run", {
1163
+ method: "POST",
1164
+ headers: { "content-type": "application/json" },
1165
+ body: JSON.stringify({ name: tool.name, input: input, baseUrl: baseUrl || undefined }),
1166
+ }).then(function (result) {
1167
+ resultEl.hidden = false;
1168
+ resultEl.className = "result " + (result.ok ? "ok" : "err");
1169
+ resultEl.textContent =
1170
+ (result.status ? "HTTP " + result.status + "
1171
+
1172
+ " : "") +
1173
+ (result.error ? result.error : JSON.stringify(result.body, null, 2));
1174
+ }).catch(function (error) {
1175
+ resultEl.hidden = false;
1176
+ resultEl.className = "result err";
1177
+ resultEl.textContent = error.message;
1178
+ }).finally(function () {
1179
+ runEl.disabled = false;
1180
+ runEl.textContent = "Run tool";
1181
+ });
1182
+ }
1183
+
1184
+ /* Keyboard navigation */
1185
+ document.addEventListener("keydown", function (event) {
1186
+ if ((event.metaKey || event.ctrlKey) && event.key === "k") {
1187
+ event.preventDefault();
1188
+ searchEl.focus();
1189
+ return;
1190
+ }
1191
+ if ((event.metaKey || event.ctrlKey) && event.key === "s") {
1192
+ event.preventDefault();
1193
+ saveDescription();
1194
+ return;
1195
+ }
1196
+ if (event.target === searchEl || event.target.tagName === "TEXTAREA" || event.target.tagName === "INPUT") {
1197
+ return;
1198
+ }
1199
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
1200
+ var tools = visibleTools();
1201
+ var index = tools.findIndex(function (tool) { return tool.name === selected; });
1202
+ var next = event.key === "ArrowDown" ? index + 1 : index - 1;
1203
+ if (next < 0 || next >= tools.length) return;
1204
+ event.preventDefault();
1205
+ selected = tools[next].name;
1206
+ renderList();
1207
+ renderDetail();
1208
+ var button = listEl.querySelector('[aria-selected="true"]');
1209
+ if (button) button.scrollIntoView({ block: "nearest" });
1210
+ });
1211
+
1212
+ searchEl.addEventListener("input", function (event) {
1213
+ filter = event.target.value;
1214
+ renderList();
1215
+ });
1216
+
1217
+ load();
1218
+ })();
1219
+ </script>
1220
+ </body>
1221
+ </html>`;
1222
+ }
1223
+
1224
+ // src/dev/server.ts
1225
+ async function startDevServer(options) {
1226
+ const setup = await resolveSetup(options.cwd, {
1227
+ dryRun: true,
1228
+ skipAudit: false,
1229
+ force: false,
1230
+ watch: false
1231
+ });
1232
+ async function currentState() {
1233
+ const data = await loadDataFile(options.cwd);
1234
+ const result = await runGenerate(setup.config, {
1235
+ cwd: options.cwd,
1236
+ dryRun: true,
1237
+ overrides: data.overrides
1238
+ });
1239
+ return {
1240
+ label: setup.label,
1241
+ outDir: setup.config.generate[0]?.outDir,
1242
+ tools: result.tools.map((tool) => toUiTool(tool, result.findings)),
1243
+ skipped: result.skipped,
1244
+ notes: result.notes
1245
+ };
1246
+ }
1247
+ const server = createServer(async (request, response) => {
1248
+ try {
1249
+ await route(request, response);
1250
+ } catch (error) {
1251
+ sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
1252
+ }
1253
+ });
1254
+ async function route(request, response) {
1255
+ const url = new URL(request.url ?? "/", "http://localhost");
1256
+ if (request.method === "GET" && url.pathname === "/") {
1257
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
1258
+ response.end(dashboardHtml());
1259
+ return;
1260
+ }
1261
+ if (request.method === "GET" && url.pathname === "/api/state") {
1262
+ sendJson(response, 200, await currentState());
1263
+ return;
1264
+ }
1265
+ if (request.method === "POST" && url.pathname === "/api/override") {
1266
+ const body = await readJson(request);
1267
+ if (!body.name) {
1268
+ sendJson(response, 400, { error: "Missing tool name." });
1269
+ return;
1270
+ }
1271
+ const data = await loadDataFile(options.cwd);
1272
+ const overrides = { ...data.overrides ?? {} };
1273
+ const existing = overrides[body.name] ?? {};
1274
+ overrides[body.name] = {
1275
+ ...existing,
1276
+ ...body.description !== void 0 ? { description: body.description } : {},
1277
+ ...body.enabled !== void 0 ? { enabled: body.enabled } : {}
1278
+ };
1279
+ await saveDataFile(options.cwd, { overrides });
1280
+ sendJson(response, 200, { ok: true, saved: `.webmcp-codegen.json` });
1281
+ return;
1282
+ }
1283
+ if (request.method === "POST" && url.pathname === "/api/run") {
1284
+ const body = await readJson(request);
1285
+ const state = await currentState();
1286
+ const tool = state.tools.find((candidate) => candidate.name === body.name);
1287
+ if (!tool) {
1288
+ sendJson(response, 404, { error: `No tool named "${body.name}".` });
1289
+ return;
1290
+ }
1291
+ const result = await runEndpoint(tool, body.input ?? {}, body.baseUrl);
1292
+ sendJson(response, result.ok ? 200 : 502, result);
1293
+ return;
1294
+ }
1295
+ sendJson(response, 404, { error: "Not found" });
1296
+ }
1297
+ await new Promise(
1298
+ (resolveListen) => server.listen(options.port, "127.0.0.1", resolveListen)
1299
+ );
1300
+ if (options.open !== false) openBrowser(`http://localhost:${options.port}`);
1301
+ return server;
1302
+ }
1303
+ function toUiTool(tool, findings) {
1304
+ const [verb, ...rest] = tool.source.ref.split(" ");
1305
+ return {
1306
+ name: tool.name,
1307
+ verb,
1308
+ path: rest.join(" "),
1309
+ description: tool.description,
1310
+ sideEffect: tool.sideEffect,
1311
+ riskTier: tool.riskTier,
1312
+ enabled: tool.enabledByDefault,
1313
+ endpointRole: tool.endpointRole,
1314
+ piiInOutput: tool.piiInOutput,
1315
+ inputSchema: tool.inputSchema,
1316
+ ...tool.pathTemplate ? { pathTemplate: tool.pathTemplate } : {},
1317
+ ...tool.paramLocations ? { paramLocations: tool.paramLocations } : {},
1318
+ ...tool.serverUrl ? { serverUrl: tool.serverUrl } : {},
1319
+ requiresAuth: tool.requiresAuth,
1320
+ findings: findings.filter((finding) => finding.tool === tool.name).map((finding) => ({ level: finding.level, message: finding.message }))
1321
+ };
1322
+ }
1323
+ async function runEndpoint(tool, input, baseUrlOverride) {
1324
+ const base = baseUrlOverride ?? tool.serverUrl;
1325
+ if (!base) {
1326
+ return {
1327
+ ok: false,
1328
+ error: `No base URL: the spec lists no absolute server. Type your app's URL (e.g. http://localhost:3000) in the "base URL" field and run again.`
1329
+ };
1330
+ }
1331
+ if (!tool.pathTemplate || !tool.verb) {
1332
+ return { ok: false, error: "This tool has no route to call." };
1333
+ }
1334
+ let path = tool.pathTemplate;
1335
+ for (const param of tool.paramLocations?.path ?? []) {
1336
+ path = path.replace(`{${param}}`, encodeURIComponent(String(input[param] ?? "")));
1337
+ }
1338
+ const url = new URL(path, base);
1339
+ for (const param of tool.paramLocations?.query ?? []) {
1340
+ const value = input[param];
1341
+ if (value !== void 0 && value !== null) url.searchParams.set(param, String(value));
1342
+ }
1343
+ const bodyFields = tool.paramLocations?.body ?? [];
1344
+ const body = bodyFields.length === 1 && bodyFields[0] === "body" ? input.body : bodyFields.length > 0 ? Object.fromEntries(bodyFields.map((field) => [field, input[field]])) : void 0;
1345
+ try {
1346
+ const response = await fetch(url, {
1347
+ method: tool.verb,
1348
+ headers: body !== void 0 ? { "content-type": "application/json" } : void 0,
1349
+ body: body !== void 0 ? JSON.stringify(body) : void 0
1350
+ });
1351
+ const text = await response.text();
1352
+ let parsed = text;
1353
+ try {
1354
+ parsed = JSON.parse(text);
1355
+ } catch {
1356
+ }
1357
+ return { ok: response.ok, status: response.status, body: parsed };
1358
+ } catch (error) {
1359
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
1360
+ }
1361
+ }
1362
+ function sendJson(response, status, body) {
1363
+ response.writeHead(status, { "content-type": "application/json" });
1364
+ response.end(JSON.stringify(body));
1365
+ }
1366
+ function readJson(request) {
1367
+ return new Promise((resolveRead, reject) => {
1368
+ let text = "";
1369
+ request.on("data", (chunk) => {
1370
+ text += chunk.toString("utf8");
1371
+ });
1372
+ request.on("end", () => {
1373
+ try {
1374
+ resolveRead(text ? JSON.parse(text) : {});
1375
+ } catch {
1376
+ reject(new Error("Invalid JSON body"));
1377
+ }
1378
+ });
1379
+ request.on("error", reject);
1380
+ });
1381
+ }
1382
+ function openBrowser(url) {
1383
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1384
+ spawn(command, [url], { stdio: "ignore", shell: process.platform === "win32" }).unref();
1385
+ }
1386
+
1387
+ // src/wire.ts
1388
+ import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1389
+ import { dirname, join as join5, relative as relative2 } from "path";
26
1390
  async function planWiring(cwd, app, outDir) {
27
1391
  switch (app.framework) {
28
1392
  case "next":
@@ -35,28 +1399,28 @@ async function planWiring(cwd, app, outDir) {
35
1399
  }
36
1400
  async function applyWiring(plan) {
37
1401
  for (const edit of plan.edits) {
38
- await writeFile(edit.path, edit.contents, "utf8");
1402
+ await writeFile2(edit.path, edit.contents, "utf8");
39
1403
  }
40
1404
  }
41
1405
  async function planNextWiring(cwd, app, outDir) {
42
1406
  const layoutCandidates = [
43
- join(cwd, app.dir, "src/app/layout.tsx"),
44
- join(cwd, app.dir, "src/app/layout.jsx"),
45
- join(cwd, app.dir, "app/layout.tsx"),
46
- join(cwd, app.dir, "app/layout.jsx")
1407
+ join5(cwd, app.dir, "src/app/layout.tsx"),
1408
+ join5(cwd, app.dir, "src/app/layout.jsx"),
1409
+ join5(cwd, app.dir, "app/layout.tsx"),
1410
+ join5(cwd, app.dir, "app/layout.jsx")
47
1411
  ];
48
1412
  const layoutPath = await firstExisting(layoutCandidates);
49
1413
  if (!layoutPath) return null;
50
- const registerPath = join(cwd, outDir, "register.tsx");
51
- const layout = await readFile(layoutPath, "utf8");
1414
+ const registerPath = join5(cwd, outDir, "register.tsx");
1415
+ const layout = await readFile3(layoutPath, "utf8");
52
1416
  if (layout.includes("WebMCPRegister")) return { edits: [], alreadyWired: true };
53
- const importPath = withoutExtension(relative(dirname(layoutPath), registerPath));
1417
+ const importPath = withoutExtension(relative2(dirname(layoutPath), registerPath));
54
1418
  const edits = [
55
1419
  {
56
1420
  path: registerPath,
57
1421
  action: "create",
58
1422
  contents: nextRegisterComponent(),
59
- summary: `created ${relative(cwd, registerPath)} (a client component that registers your tools on page load)`
1423
+ summary: `created ${relative2(cwd, registerPath)} (a client component that registers your tools on page load)`
60
1424
  }
61
1425
  ];
62
1426
  const withImport = insertAfterLastImport(
@@ -73,7 +1437,7 @@ async function planNextWiring(cwd, app, outDir) {
73
1437
  path: layoutPath,
74
1438
  action: "modify",
75
1439
  contents: withComponent,
76
- summary: `added 2 lines to ${relative(cwd, layoutPath)} (an import and <WebMCPRegister /> inside <body>)`
1440
+ summary: `added 2 lines to ${relative2(cwd, layoutPath)} (an import and <WebMCPRegister /> inside <body>)`
77
1441
  });
78
1442
  return { edits };
79
1443
  }
@@ -97,16 +1461,16 @@ export function WebMCPRegister() {
97
1461
  }
98
1462
  async function planViteWiring(cwd, app, outDir) {
99
1463
  const entryCandidates = [
100
- join(cwd, app.dir, "src/main.tsx"),
101
- join(cwd, app.dir, "src/main.jsx"),
102
- join(cwd, app.dir, "src/index.tsx"),
103
- join(cwd, app.dir, "src/index.jsx")
1464
+ join5(cwd, app.dir, "src/main.tsx"),
1465
+ join5(cwd, app.dir, "src/main.jsx"),
1466
+ join5(cwd, app.dir, "src/index.tsx"),
1467
+ join5(cwd, app.dir, "src/index.jsx")
104
1468
  ];
105
1469
  const entryPath = await firstExisting(entryCandidates);
106
1470
  if (!entryPath) return null;
107
- const entry = await readFile(entryPath, "utf8");
1471
+ const entry = await readFile3(entryPath, "utf8");
108
1472
  if (entry.includes("registerAllTools")) return { edits: [], alreadyWired: true };
109
- const importPath = withoutExtension(relative(dirname(entryPath), join(cwd, outDir, "index")));
1473
+ const importPath = withoutExtension(relative2(dirname(entryPath), join5(cwd, outDir, "index")));
110
1474
  const withWiring = insertAfterLastImport(
111
1475
  entry,
112
1476
  `import { registerAllTools } from "${importPath}";
@@ -120,7 +1484,7 @@ void registerAllTools();`
120
1484
  path: entryPath,
121
1485
  action: "modify",
122
1486
  contents: withWiring,
123
- summary: `added 2 lines to ${relative(cwd, entryPath)} (an import and a registerAllTools() call)`
1487
+ summary: `added 2 lines to ${relative2(cwd, entryPath)} (an import and a registerAllTools() call)`
124
1488
  }
125
1489
  ]
126
1490
  };
@@ -142,7 +1506,7 @@ function withoutExtension(path) {
142
1506
  async function firstExisting(paths) {
143
1507
  for (const path of paths) {
144
1508
  try {
145
- await readFile(path, "utf8");
1509
+ await readFile3(path, "utf8");
146
1510
  return path;
147
1511
  } catch {
148
1512
  }
@@ -151,38 +1515,46 @@ async function firstExisting(paths) {
151
1515
  }
152
1516
 
153
1517
  // src/cli.ts
154
- var HELP = `webmcp-codegen: generate WebMCP tools from the API contracts you already have
1518
+ var HELP = `
1519
+ webmcp-codegen \u2014 generate WebMCP tools from your OpenAPI spec
155
1520
 
156
- Fastest start (no install, no config):
157
- npx webmcp-codegen generate --dry-run Detect your spec, preview the tools
158
- npx webmcp-codegen generate Write the tool files, wire them up
1521
+ Usage
1522
+ npx webmcp-codegen [command] [flags]
159
1523
 
160
- Commands:
161
- init Write a codegen.config.mjs for full control
162
- generate Generate (or update) your WebMCP tools
163
- generate --watch Re-generate when files change
164
- dev Open the tools dashboard (list, edit, try tools)
1524
+ Commands
1525
+ generate Generate tool files from your spec (default when no command given)
1526
+ dev Open the tools dashboard (list, describe, toggle, test)
1527
+ init Write a codegen.config.mjs for full control
165
1528
 
166
- Flags for generate:
1529
+ Flags
167
1530
  --spec PATH Which OpenAPI spec to use (auto-detected when omitted)
168
- --out DIR Where the tool files go (default: your web app's src/webmcp)
1531
+ --out DIR Where tool files go (default: your web app's src/webmcp)
169
1532
  --dry-run Preview what would be written, write nothing
170
- --skip-audit Skip the safety report
1533
+ --verbose Show every tool, not just the summary
171
1534
  --force Write files even when the audit reports errors
1535
+ --skip-audit Skip the safety report
172
1536
  --config PATH Use a config file at PATH
173
-
174
- Flags for dev:
175
1537
  --port N Dashboard port (default: 4700)
1538
+ --help Show this help
1539
+
1540
+ Examples
1541
+ npx webmcp-codegen generate # detect spec, generate tools
1542
+ npx webmcp-codegen generate --dry-run # preview without writing
1543
+ npx webmcp-codegen dev # open the dashboard
1544
+ npx webmcp-codegen generate --verbose # see all 72 tools listed
1545
+
1546
+ Docs
1547
+ https://webmcp-codegen.vercel.app/docs
176
1548
  `;
177
- var CONFIG_FILE = "codegen.config.mjs";
178
- var MAX_LISTED_TOOLS = 15;
179
1549
  async function main() {
180
- const { positionals, values } = parseArgs({
1550
+ const { values, positionals } = parseArgs({
1551
+ args: process.argv.slice(2),
181
1552
  allowPositionals: true,
182
1553
  options: {
183
1554
  "dry-run": { type: "boolean", default: false },
184
1555
  "skip-audit": { type: "boolean", default: false },
185
1556
  force: { type: "boolean", default: false },
1557
+ verbose: { type: "boolean", default: false },
186
1558
  watch: { type: "boolean", default: false },
187
1559
  config: { type: "string" },
188
1560
  spec: { type: "string" },
@@ -191,28 +1563,32 @@ async function main() {
191
1563
  help: { type: "boolean", default: false }
192
1564
  }
193
1565
  });
194
- const command = positionals[0];
195
- if (values.help || !command) {
1566
+ const flags = {
1567
+ dryRun: values["dry-run"] ?? false,
1568
+ skipAudit: values["skip-audit"] ?? false,
1569
+ force: values.force ?? false,
1570
+ verbose: values.verbose ?? false,
1571
+ watch: values.watch ?? false,
1572
+ config: values.config,
1573
+ spec: values.spec,
1574
+ out: values.out,
1575
+ port: values.port ? Number.parseInt(values.port, 10) : void 0,
1576
+ help: values.help
1577
+ };
1578
+ if (flags.help) {
196
1579
  console.log(HELP);
197
1580
  return 0;
198
1581
  }
1582
+ const command = positionals[0] ?? "generate";
199
1583
  switch (command) {
200
1584
  case "init":
201
1585
  return init();
202
1586
  case "dev":
203
- return dev(Number.parseInt(values.port ?? "4700", 10));
1587
+ return dev(flags.port ?? 4700);
204
1588
  case "generate":
205
- return generate({
206
- dryRun: values["dry-run"],
207
- skipAudit: values["skip-audit"],
208
- force: values.force,
209
- watch: values.watch,
210
- configPath: values.config,
211
- spec: values.spec,
212
- out: values.out
213
- });
1589
+ return generate(flags);
214
1590
  default:
215
- console.error(`Unknown command "${command}".
1591
+ console.error(`Unknown command: ${command}
216
1592
  `);
217
1593
  console.log(HELP);
218
1594
  return 1;
@@ -220,14 +1596,17 @@ async function main() {
220
1596
  }
221
1597
  async function init() {
222
1598
  const cwd = process.cwd();
223
- const configPath = join2(cwd, CONFIG_FILE);
224
- if (existsSync(configPath)) {
225
- console.error(`${CONFIG_FILE} already exists. Nothing to do.`);
1599
+ const configFile = CONFIG_FILE_NAMES[0] ?? "codegen.config.mjs";
1600
+ const configPath = join6(cwd, configFile);
1601
+ if (existsSync2(configPath)) {
1602
+ console.error(`
1603
+ \u2716 ${configFile} already exists. Nothing to do.
1604
+ `);
226
1605
  return 1;
227
1606
  }
228
1607
  const specs = await findSpecs(cwd);
229
1608
  const specPath = specs.length > 0 ? `./${specs[0]}` : "./openapi.yaml";
230
- await writeFile2(
1609
+ await writeFile3(
231
1610
  configPath,
232
1611
  `import { defineConfig } from "webmcp-codegen";
233
1612
  import { openapi } from "webmcp-codegen/sources";
@@ -245,40 +1624,48 @@ export default defineConfig({
245
1624
  });
246
1625
  `
247
1626
  );
248
- console.log("Installed the package? A config file needs it:");
249
- console.log(" npm install -D webmcp-codegen\n");
250
- if (specs.length > 0) {
251
- console.log(`Found ${specs[0]}. Wrote ${CONFIG_FILE}.`);
252
- console.log("\nNext: npx webmcp-codegen generate --dry-run");
253
- } else {
254
- console.log(`No OpenAPI spec found, so ${CONFIG_FILE} points at ./openapi.yaml.`);
255
- console.log("Edit the `spec` path to point at your spec, then run:");
256
- console.log("\n npx webmcp-codegen generate --dry-run");
257
- }
1627
+ console.log(`
1628
+ \u2714 Wrote ${configFile}
1629
+ `);
1630
+ console.log("Edit it to add sources, change the output directory, or set safety options.");
1631
+ console.log("Docs: https://webmcp-codegen.vercel.app/docs/configuration\n");
258
1632
  return 0;
259
1633
  }
260
- async function generate(flags) {
1634
+ async function dev(port) {
261
1635
  const cwd = process.cwd();
262
- if (flags.watch) {
263
- await watchLoop(cwd, flags);
264
- return 0;
265
- }
266
- const result = await runOnce(cwd, flags);
267
- return result.blocked ? 1 : 0;
1636
+ const server = await startDevServer({ cwd, port, open: true });
1637
+ console.log(`
1638
+ \u2714 Dashboard: http://localhost:${port}
1639
+ `);
1640
+ console.log("List, describe, enable, and test your tools. Ctrl+C to stop.\n");
1641
+ await new Promise((resolveExit) => {
1642
+ process.on("SIGINT", () => {
1643
+ server.close();
1644
+ resolveExit();
1645
+ });
1646
+ });
1647
+ return 0;
268
1648
  }
269
- async function runOnce(cwd, flags) {
270
- const setup = await resolveSetup(cwd, flags);
271
- const data = await loadDataFile(cwd);
1649
+ async function generate(flags) {
1650
+ const cwd = process.cwd();
1651
+ const setup = await resolveSetup(cwd, {
1652
+ dryRun: flags.dryRun,
1653
+ skipAudit: flags.skipAudit,
1654
+ force: flags.force,
1655
+ watch: flags.watch,
1656
+ spec: flags.spec,
1657
+ out: flags.out,
1658
+ configPath: flags.config
1659
+ });
272
1660
  const result = await runGenerate(setup.config, {
273
1661
  cwd,
274
1662
  dryRun: flags.dryRun,
275
- skipAudit: flags.skipAudit,
276
1663
  force: flags.force,
277
- overrides: data.overrides
1664
+ skipAudit: flags.skipAudit
278
1665
  });
279
1666
  let wiring = null;
280
1667
  if (!result.blocked && setup.app) {
281
- const outDir = findOutDir(setup.config);
1668
+ const outDir = setup.config.generate[0]?.outDir;
282
1669
  if (outDir) {
283
1670
  wiring = await planWiring(cwd, setup.app, outDir);
284
1671
  if (wiring && wiring.edits.length > 0 && !flags.dryRun && result.wrote) {
@@ -289,143 +1676,22 @@ async function runOnce(cwd, flags) {
289
1676
  if (!setup.fromConfigFile && !flags.dryRun && result.wrote) {
290
1677
  await saveDataFile(cwd, setup.remember);
291
1678
  }
292
- printReport(result, flags, setup, wiring, cwd);
293
- return result;
294
- }
295
- function findOutDir(config) {
296
- return config.generate[0]?.outDir;
297
- }
298
- function printReport(result, flags, setup, wiring, cwd) {
299
- const { tools, skipped, findings, files, notes, blocked } = result;
300
- console.log(`
301
- webmcp-codegen (${setup.label}): ${tools.length} tool(s)`);
302
- for (const note of notes) {
303
- console.log(`
304
- note: ${note}`);
305
- }
306
- if (skipped.length > 0) {
307
- console.log(`
308
- ${skipped.length} endpoint(s) skipped:`);
309
- for (const entry of skipped) {
310
- console.log(` ${entry.ref}: ${entry.reason}`);
311
- }
312
- }
313
- console.log("");
314
- const listed = tools.slice(0, MAX_LISTED_TOOLS);
315
- for (const tool of listed) {
316
- const state = tool.enabledByDefault ? "" : " starts disabled";
317
- console.log(` ${tool.name} [${tool.sideEffect}]${state} \u2190 ${tool.source.ref}`);
318
- }
319
- if (tools.length > listed.length) {
320
- console.log(` \u2026and ${tools.length - listed.length} more`);
321
- }
322
- if (findings.length > 0) {
323
- console.log("");
324
- for (const finding of findings) {
325
- const icon = finding.level === "error" ? "\u2716" : "\u26A0";
326
- const where = finding.tool ? ` (${finding.tool})` : "";
327
- console.log(` ${icon} ${finding.message}${where}`);
328
- }
329
- }
330
- if (files.length > 0) {
331
- console.log("");
332
- for (const file of files) {
333
- if (file.action === "unchanged" && !file.conflict) continue;
334
- const shown = file.conflict ? `conflict \u2192 wrote ${relative2(cwd, file.conflict)}` : file.action;
335
- console.log(` ${shown}: ${relative2(cwd, file.path)}`);
336
- }
337
- }
338
- if (wiring) {
339
- if (wiring.alreadyWired) {
340
- console.log("\n registration: already wired into your app");
341
- } else if (wiring.edits.length > 0) {
342
- console.log(flags.dryRun ? "\n registration (would do):" : "\n registration:");
343
- for (const edit of wiring.edits) {
344
- console.log(` ${edit.summary}`);
345
- }
346
- if (!flags.dryRun) {
347
- console.log(" undo: delete the added lines (nothing else was touched)");
348
- }
349
- }
350
- } else if (!blocked && setup.app) {
351
- console.log("\n registration: could not find your app's entry file, so add this by hand:");
352
- console.log(' import { registerAllTools } from "<path-to>/src/webmcp";');
353
- console.log(" void registerAllTools();");
354
- }
355
- if (blocked) {
356
- console.log(
357
- "\nGeneration blocked by audit errors. Fix them, or re-run with --force to write anyway."
358
- );
359
- return;
1679
+ if (flags.verbose) {
1680
+ renderVerbose(result, setup, cwd);
1681
+ } else {
1682
+ renderSummary(result, setup, cwd, wiring);
360
1683
  }
361
- if (flags.dryRun) {
362
- console.log("\nDry run: nothing written. Re-run without --dry-run to write these files.");
363
- return;
1684
+ if (flags.watch && !flags.dryRun) {
1685
+ console.log(dim("\n --watch is not implemented yet. Run generate again after changes.\n"));
364
1686
  }
365
- printNextSteps(result);
1687
+ return result.blocked ? 1 : 0;
366
1688
  }
367
- function printNextSteps(result) {
368
- const enabled = result.tools.filter((tool) => tool.enabledByDefault);
369
- const disabled = result.tools.filter((tool) => !tool.enabledByDefault);
370
- const parts = [];
371
- if (enabled.length > 0) parts.push(`${enabled.length} read tool(s) work out of the box`);
372
- if (disabled.length > 0) {
373
- parts.push(`${disabled.length} tool(s) start disabled (open the file and uncomment to enable)`);
374
- }
375
- console.log(`
376
- Done. ${parts.join("; ")}.`);
377
- const suggestion = pickSuggestedTool(result.tools);
378
- console.log("\nTry it:");
379
- console.log(" 1. Start your app and open it in Chrome.");
380
- console.log(" 2. Turn on chrome://flags/#enable-webmcp-testing and reload the page.");
381
- if (suggestion) {
382
- console.log(` 3. Ask the agent: "${suggestion}"`);
383
- } else {
384
- console.log(" 3. Ask the agent to use one of your tools.");
1689
+ main().then(
1690
+ (code) => process.exit(code),
1691
+ (error) => {
1692
+ console.error("\n\u2716 Unexpected error:", error instanceof Error ? error.message : error);
1693
+ console.error("\nPlease report this: https://github.com/SouravInsights/groundstate/issues\n");
1694
+ process.exit(1);
385
1695
  }
386
- }
387
- function pickSuggestedTool(tools) {
388
- const reads = tools.filter((tool2) => tool2.enabledByDefault && tool2.endpointRole === "endpoint");
389
- if (reads.length === 0) return void 0;
390
- const tool = reads.find((candidate) => /^(list|get|search|find|fetch|recent)-/.test(candidate.name)) ?? reads[0];
391
- if (!tool) return void 0;
392
- const description = tool.description.trim().replace(/\.$/, "");
393
- const looksTemplated = /^(GET|POST|PUT|PATCH|DELETE)\s/.test(description);
394
- const phrase = looksTemplated ? tool.name.replace(/-/g, " ") : description.charAt(0).toLowerCase() + description.slice(1);
395
- return phrase;
396
- }
397
- async function dev(port) {
398
- const { startDevServer } = await import("./server-DWP6IHCS.js");
399
- const server = await startDevServer({ cwd: process.cwd(), port });
400
- console.log(`
401
- webmcp-codegen dashboard: http://localhost:${port}`);
402
- console.log("List, describe, enable, and try your tools. Ctrl+C to stop.\n");
403
- await new Promise((resolveExit) => {
404
- process.on("SIGINT", () => {
405
- server.close();
406
- resolveExit();
407
- });
408
- });
409
- return 0;
410
- }
411
- async function watchLoop(cwd, flags) {
412
- await runOnce(cwd, { ...flags, dryRun: false });
413
- console.log("\nWatching for changes\u2026 (Ctrl+C to stop)");
414
- let timer;
415
- watch(cwd, { recursive: true }, (_event, filename) => {
416
- if (!filename) return;
417
- if (/node_modules|\.git|\/dist|\/src\/webmcp|\.webmcp-codegen\.json/.test(filename)) return;
418
- if (!/\.(ya?ml|json|ts|tsx|mts|mjs)$/.test(filename)) return;
419
- clearTimeout(timer);
420
- timer = setTimeout(() => {
421
- runOnce(cwd, { ...flags, dryRun: false }).catch((error) => {
422
- console.error(error instanceof Error ? error.message : error);
423
- });
424
- }, 300);
425
- });
426
- }
427
- main().then((code) => process.exit(code)).catch((error) => {
428
- console.error(error instanceof Error ? error.message : error);
429
- process.exit(1);
430
- });
1696
+ );
431
1697
  //# sourceMappingURL=cli.js.map