webmcp-codegen 0.3.2 → 0.3.3

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,22 +1,23 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ findSpecs,
4
+ resolveSetup,
5
+ saveDataFile,
6
+ startDevServer
7
+ } from "./chunk-MUTXYBL6.js";
2
8
  import {
3
9
  CONFIG_FILE_NAMES,
4
- loadConfig,
5
10
  runGenerate
6
11
  } from "./chunk-MJQ5B6HB.js";
7
- import {
8
- js
9
- } from "./chunk-EAKYM4YS.js";
10
- import {
11
- openapi
12
- } from "./chunk-3LTHWIAP.js";
12
+ import "./chunk-EAKYM4YS.js";
13
+ import "./chunk-3LTHWIAP.js";
13
14
  import "./chunk-FWSATV7C.js";
14
15
  import "./chunk-KSQMJERY.js";
15
16
 
16
17
  // src/cli.ts
17
- import { existsSync as existsSync2 } from "fs";
18
- import { writeFile as writeFile3 } from "fs/promises";
19
- import { join as join6 } from "path";
18
+ import { existsSync } from "fs";
19
+ import { writeFile as writeFile2 } from "fs/promises";
20
+ import { join as join2 } from "path";
20
21
  import { parseArgs } from "util";
21
22
 
22
23
  // src/cli-output.ts
@@ -24,15 +25,12 @@ var ESC = "\x1B[";
24
25
  var RESET = `${ESC}0m`;
25
26
  var BOLD = `${ESC}1m`;
26
27
  var DIM = `${ESC}2m`;
27
- var ITALIC = `${ESC}3m`;
28
28
  var FG = {
29
29
  red: `${ESC}31m`,
30
30
  green: `${ESC}32m`,
31
31
  yellow: `${ESC}33m`,
32
32
  blue: `${ESC}34m`,
33
- magenta: `${ESC}35m`,
34
33
  cyan: `${ESC}36m`,
35
- white: `${ESC}37m`,
36
34
  gray: `${ESC}90m`
37
35
  };
38
36
  function c(color, text) {
@@ -44,85 +42,82 @@ function bold(text) {
44
42
  function dim(text) {
45
43
  return `${DIM}${text}${RESET}`;
46
44
  }
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 = {};
45
+ function summarizeFindings(findings) {
46
+ const counts = { auth: 0, admin: 0, pii: 0, postAsRead: 0, other: 0 };
71
47
  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;
48
+ const msg = f.message.toLowerCase();
49
+ if (msg.includes("sign-in") || msg.includes("auth") || msg.includes("session")) {
50
+ counts.auth++;
51
+ } else if (msg.includes("admin")) {
52
+ counts.admin++;
53
+ } else if (msg.includes("pii") || msg.includes("email")) {
54
+ counts.pii++;
55
+ } else if (msg.includes("post") && msg.includes("read")) {
56
+ counts.postAsRead++;
57
+ } else {
58
+ counts.other++;
59
+ }
74
60
  }
75
- return groups;
61
+ return counts;
76
62
  }
77
63
  function renderSummary(result, setup, _cwd, wiring) {
78
- const { tools, findings } = result;
64
+ const { tools, findings, skipped } = result;
79
65
  const reads = tools.filter((t) => t.sideEffect === "read").length;
80
66
  const writes = tools.filter((t) => t.sideEffect === "write").length;
81
67
  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;
68
+ const enabled = tools.filter((t) => t.enabledByDefault).length;
69
+ const findingCounts = summarizeFindings(findings);
70
+ const totalFindings = findings.length;
85
71
  console.log("");
86
- console.log(header("webmcp-codegen", `${tools.length} tools from ${setup.label}`));
72
+ console.log(` ${bold("webmcp-codegen")}`);
73
+ console.log(dim(` ${setup.label}`));
87
74
  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
- `));
75
+ console.log(` ${c("green", "\u2713")} ${bold(`${tools.length} tools generated`)}`);
76
+ console.log(dim(` ${enabled} ready to use, ${tools.length - enabled} start disabled`));
77
+ if (skipped.length > 0) {
78
+ console.log(dim(` ${skipped.length} skipped (webhooks and excluded endpoints)`));
79
+ }
80
+ console.log("");
81
+ if (totalFindings > 0) {
82
+ console.log(` ${c("yellow", "!")} ${bold(`${totalFindings} safety note${totalFindings === 1 ? "" : "s"}`)}`);
83
+ if (findingCounts.auth > 0) {
84
+ console.log(dim(` ${findingCounts.auth} auth endpoint${findingCounts.auth === 1 ? "" : "s"} disabled (agents shouldn't sign in)`));
85
+ }
86
+ if (findingCounts.admin > 0) {
87
+ console.log(dim(` ${findingCounts.admin} admin endpoint${findingCounts.admin === 1 ? "" : "s"} disabled (review each before enabling)`));
88
+ }
89
+ if (findingCounts.pii > 0) {
90
+ console.log(dim(` ${findingCounts.pii} endpoint${findingCounts.pii === 1 ? "" : "s"} may return personal data`));
91
+ }
92
+ if (findingCounts.postAsRead > 0) {
93
+ console.log(dim(` ${findingCounts.postAsRead} POST endpoint${findingCounts.postAsRead === 1 ? "" : "s"} treated as read-only (verify this is correct)`));
94
+ }
95
+ console.log(dim(` Run with --verbose to see all details`));
96
+ console.log("");
105
97
  }
106
98
  const outDir = setup.config.generate[0]?.outDir ?? "src/webmcp";
107
- console.log(` ${c("cyan", "\u2192")} ${bold(outDir)}
108
- `);
99
+ console.log(` ${c("cyan", "\u2192")} ${bold("Files")} ${outDir}`);
109
100
  if (wiring && !wiring.alreadyWired) {
110
- console.log(` ${c("green", "\u2714")} Registration wired into your app
111
- `);
101
+ console.log(` ${c("cyan", "\u2192")} ${bold("Registration")} wired into your app`);
112
102
  }
113
- console.log(` ${bold("Next:")} ${c("cyan", "npx webmcp-codegen dev")} to review and test
114
- `);
103
+ console.log("");
104
+ console.log(` ${bold("Next:")} ${c("cyan", "npx webmcp-codegen dev")}`);
105
+ console.log(dim(" Review your tools, edit descriptions, test them live"));
106
+ console.log("");
107
+ console.log(dim(` Docs: https://webmcp-codegen.vercel.app/docs`));
108
+ console.log("");
115
109
  }
116
110
  function renderVerbose(result, setup, _cwd) {
117
111
  const { tools, findings, skipped } = result;
118
112
  console.log("");
119
- console.log(bold(`webmcp-codegen: ${tools.length} tools from ${setup.label}`));
113
+ console.log(bold(`webmcp-codegen`));
114
+ console.log(dim(`${tools.length} tools from ${setup.label}`));
120
115
  console.log("");
121
116
  if (skipped.length > 0) {
122
117
  console.log(c("gray", "Skipped:"));
123
118
  for (const s of skipped) {
124
119
  console.log(` ${dim(s.ref)}`);
125
- console.log(` ${italic(s.reason)}`);
120
+ console.log(` ${dim(s.reason)}`);
126
121
  }
127
122
  console.log("");
128
123
  }
@@ -131,18 +126,23 @@ function renderVerbose(result, setup, _cwd) {
131
126
  write: tools.filter((t) => t.sideEffect === "write"),
132
127
  destructive: tools.filter((t) => t.sideEffect === "destructive")
133
128
  };
129
+ const riskLabels = {
130
+ read: c("green", "Read-only"),
131
+ write: c("yellow", "Write"),
132
+ destructive: c("red", "Destructive")
133
+ };
134
134
  for (const [risk, group] of Object.entries(byRisk)) {
135
135
  if (group.length === 0) continue;
136
- console.log(badge(risk));
136
+ console.log(riskLabels[risk] ?? risk);
137
137
  for (const tool of group) {
138
- const disabled = tool.enabledByDefault ? "" : dim(" (starts disabled)");
139
- console.log(` ${tool.name}${disabled}`);
138
+ const status = tool.enabledByDefault ? "" : dim(" (disabled)");
139
+ console.log(` ${tool.name}${status}`);
140
140
  if (tool.description) console.log(` ${dim(tool.description)}`);
141
141
  }
142
142
  console.log("");
143
143
  }
144
144
  if (findings.length > 0) {
145
- console.log(bold("Findings:"));
145
+ console.log(bold("Safety notes:"));
146
146
  for (const f of findings) {
147
147
  const icon = f.level === "error" ? c("red", "\u2716") : c("yellow", "\u26A0");
148
148
  const where = f.tool ? dim(` (${f.tool})`) : "";
@@ -150,1243 +150,14 @@ function renderVerbose(result, setup, _cwd) {
150
150
  }
151
151
  console.log("");
152
152
  }
153
- console.log(` ${c("cyan", "\u2192")} Files in ${setup.config.generate[0]?.outDir ?? "src/webmcp"}
154
- `);
155
- }
156
-
157
- // src/data-file.ts
158
- import { readFile, writeFile } from "fs/promises";
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();
153
+ console.log(dim(`Files: ${setup.config.generate[0]?.outDir ?? "src/webmcp"}`));
154
+ console.log(dim(`Docs: https://webmcp-codegen.vercel.app/docs`));
155
+ console.log("");
1385
156
  }
1386
157
 
1387
158
  // 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";
159
+ import { readFile, writeFile } from "fs/promises";
160
+ import { dirname, join, relative } from "path";
1390
161
  async function planWiring(cwd, app, outDir) {
1391
162
  switch (app.framework) {
1392
163
  case "next":
@@ -1399,28 +170,28 @@ async function planWiring(cwd, app, outDir) {
1399
170
  }
1400
171
  async function applyWiring(plan) {
1401
172
  for (const edit of plan.edits) {
1402
- await writeFile2(edit.path, edit.contents, "utf8");
173
+ await writeFile(edit.path, edit.contents, "utf8");
1403
174
  }
1404
175
  }
1405
176
  async function planNextWiring(cwd, app, outDir) {
1406
177
  const layoutCandidates = [
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")
178
+ join(cwd, app.dir, "src/app/layout.tsx"),
179
+ join(cwd, app.dir, "src/app/layout.jsx"),
180
+ join(cwd, app.dir, "app/layout.tsx"),
181
+ join(cwd, app.dir, "app/layout.jsx")
1411
182
  ];
1412
183
  const layoutPath = await firstExisting(layoutCandidates);
1413
184
  if (!layoutPath) return null;
1414
- const registerPath = join5(cwd, outDir, "register.tsx");
1415
- const layout = await readFile3(layoutPath, "utf8");
185
+ const registerPath = join(cwd, outDir, "register.tsx");
186
+ const layout = await readFile(layoutPath, "utf8");
1416
187
  if (layout.includes("WebMCPRegister")) return { edits: [], alreadyWired: true };
1417
- const importPath = withoutExtension(relative2(dirname(layoutPath), registerPath));
188
+ const importPath = withoutExtension(relative(dirname(layoutPath), registerPath));
1418
189
  const edits = [
1419
190
  {
1420
191
  path: registerPath,
1421
192
  action: "create",
1422
193
  contents: nextRegisterComponent(),
1423
- summary: `created ${relative2(cwd, registerPath)} (a client component that registers your tools on page load)`
194
+ summary: `created ${relative(cwd, registerPath)} (a client component that registers your tools on page load)`
1424
195
  }
1425
196
  ];
1426
197
  const withImport = insertAfterLastImport(
@@ -1437,7 +208,7 @@ async function planNextWiring(cwd, app, outDir) {
1437
208
  path: layoutPath,
1438
209
  action: "modify",
1439
210
  contents: withComponent,
1440
- summary: `added 2 lines to ${relative2(cwd, layoutPath)} (an import and <WebMCPRegister /> inside <body>)`
211
+ summary: `added 2 lines to ${relative(cwd, layoutPath)} (an import and <WebMCPRegister /> inside <body>)`
1441
212
  });
1442
213
  return { edits };
1443
214
  }
@@ -1461,16 +232,16 @@ export function WebMCPRegister() {
1461
232
  }
1462
233
  async function planViteWiring(cwd, app, outDir) {
1463
234
  const entryCandidates = [
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")
235
+ join(cwd, app.dir, "src/main.tsx"),
236
+ join(cwd, app.dir, "src/main.jsx"),
237
+ join(cwd, app.dir, "src/index.tsx"),
238
+ join(cwd, app.dir, "src/index.jsx")
1468
239
  ];
1469
240
  const entryPath = await firstExisting(entryCandidates);
1470
241
  if (!entryPath) return null;
1471
- const entry = await readFile3(entryPath, "utf8");
242
+ const entry = await readFile(entryPath, "utf8");
1472
243
  if (entry.includes("registerAllTools")) return { edits: [], alreadyWired: true };
1473
- const importPath = withoutExtension(relative2(dirname(entryPath), join5(cwd, outDir, "index")));
244
+ const importPath = withoutExtension(relative(dirname(entryPath), join(cwd, outDir, "index")));
1474
245
  const withWiring = insertAfterLastImport(
1475
246
  entry,
1476
247
  `import { registerAllTools } from "${importPath}";
@@ -1484,7 +255,7 @@ void registerAllTools();`
1484
255
  path: entryPath,
1485
256
  action: "modify",
1486
257
  contents: withWiring,
1487
- summary: `added 2 lines to ${relative2(cwd, entryPath)} (an import and a registerAllTools() call)`
258
+ summary: `added 2 lines to ${relative(cwd, entryPath)} (an import and a registerAllTools() call)`
1488
259
  }
1489
260
  ]
1490
261
  };
@@ -1506,7 +277,7 @@ function withoutExtension(path) {
1506
277
  async function firstExisting(paths) {
1507
278
  for (const path of paths) {
1508
279
  try {
1509
- await readFile3(path, "utf8");
280
+ await readFile(path, "utf8");
1510
281
  return path;
1511
282
  } catch {
1512
283
  }
@@ -1597,8 +368,8 @@ async function main() {
1597
368
  async function init() {
1598
369
  const cwd = process.cwd();
1599
370
  const configFile = CONFIG_FILE_NAMES[0] ?? "codegen.config.mjs";
1600
- const configPath = join6(cwd, configFile);
1601
- if (existsSync2(configPath)) {
371
+ const configPath = join2(cwd, configFile);
372
+ if (existsSync(configPath)) {
1602
373
  console.error(`
1603
374
  \u2716 ${configFile} already exists. Nothing to do.
1604
375
  `);
@@ -1606,7 +377,7 @@ async function init() {
1606
377
  }
1607
378
  const specs = await findSpecs(cwd);
1608
379
  const specPath = specs.length > 0 ? `./${specs[0]}` : "./openapi.yaml";
1609
- await writeFile3(
380
+ await writeFile2(
1610
381
  configPath,
1611
382
  `import { defineConfig } from "webmcp-codegen";
1612
383
  import { openapi } from "webmcp-codegen/sources";