webmcp-codegen 0.3.1 → 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,25 +1,160 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  findSpecs,
4
- loadDataFile,
5
4
  resolveSetup,
6
- saveDataFile
7
- } from "./chunk-CCVNYNJ5.js";
5
+ saveDataFile,
6
+ startDevServer
7
+ } from "./chunk-MUTXYBL6.js";
8
8
  import {
9
+ CONFIG_FILE_NAMES,
9
10
  runGenerate
10
11
  } from "./chunk-MJQ5B6HB.js";
12
+ import "./chunk-EAKYM4YS.js";
11
13
  import "./chunk-3LTHWIAP.js";
12
14
  import "./chunk-FWSATV7C.js";
13
- import "./chunk-I4ZL527H.js";
14
15
  import "./chunk-KSQMJERY.js";
15
16
 
16
17
  // src/cli.ts
17
- import { existsSync, watch } from "fs";
18
+ import { existsSync } from "fs";
18
19
  import { writeFile as writeFile2 } from "fs/promises";
19
- import { join as join2, relative as relative2 } from "path";
20
- import "readline/promises";
20
+ import { join as join2 } from "path";
21
21
  import { parseArgs } from "util";
22
22
 
23
+ // src/cli-output.ts
24
+ var ESC = "\x1B[";
25
+ var RESET = `${ESC}0m`;
26
+ var BOLD = `${ESC}1m`;
27
+ var DIM = `${ESC}2m`;
28
+ var FG = {
29
+ red: `${ESC}31m`,
30
+ green: `${ESC}32m`,
31
+ yellow: `${ESC}33m`,
32
+ blue: `${ESC}34m`,
33
+ cyan: `${ESC}36m`,
34
+ gray: `${ESC}90m`
35
+ };
36
+ function c(color, text) {
37
+ return `${FG[color]}${text}${RESET}`;
38
+ }
39
+ function bold(text) {
40
+ return `${BOLD}${text}${RESET}`;
41
+ }
42
+ function dim(text) {
43
+ return `${DIM}${text}${RESET}`;
44
+ }
45
+ function summarizeFindings(findings) {
46
+ const counts = { auth: 0, admin: 0, pii: 0, postAsRead: 0, other: 0 };
47
+ for (const f of findings) {
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
+ }
60
+ }
61
+ return counts;
62
+ }
63
+ function renderSummary(result, setup, _cwd, wiring) {
64
+ const { tools, findings, skipped } = result;
65
+ const reads = tools.filter((t) => t.sideEffect === "read").length;
66
+ const writes = tools.filter((t) => t.sideEffect === "write").length;
67
+ const destructives = tools.filter((t) => t.sideEffect === "destructive").length;
68
+ const enabled = tools.filter((t) => t.enabledByDefault).length;
69
+ const findingCounts = summarizeFindings(findings);
70
+ const totalFindings = findings.length;
71
+ console.log("");
72
+ console.log(` ${bold("webmcp-codegen")}`);
73
+ console.log(dim(` ${setup.label}`));
74
+ console.log("");
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("");
97
+ }
98
+ const outDir = setup.config.generate[0]?.outDir ?? "src/webmcp";
99
+ console.log(` ${c("cyan", "\u2192")} ${bold("Files")} ${outDir}`);
100
+ if (wiring && !wiring.alreadyWired) {
101
+ console.log(` ${c("cyan", "\u2192")} ${bold("Registration")} wired into your app`);
102
+ }
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("");
109
+ }
110
+ function renderVerbose(result, setup, _cwd) {
111
+ const { tools, findings, skipped } = result;
112
+ console.log("");
113
+ console.log(bold(`webmcp-codegen`));
114
+ console.log(dim(`${tools.length} tools from ${setup.label}`));
115
+ console.log("");
116
+ if (skipped.length > 0) {
117
+ console.log(c("gray", "Skipped:"));
118
+ for (const s of skipped) {
119
+ console.log(` ${dim(s.ref)}`);
120
+ console.log(` ${dim(s.reason)}`);
121
+ }
122
+ console.log("");
123
+ }
124
+ const byRisk = {
125
+ read: tools.filter((t) => t.sideEffect === "read"),
126
+ write: tools.filter((t) => t.sideEffect === "write"),
127
+ destructive: tools.filter((t) => t.sideEffect === "destructive")
128
+ };
129
+ const riskLabels = {
130
+ read: c("green", "Read-only"),
131
+ write: c("yellow", "Write"),
132
+ destructive: c("red", "Destructive")
133
+ };
134
+ for (const [risk, group] of Object.entries(byRisk)) {
135
+ if (group.length === 0) continue;
136
+ console.log(riskLabels[risk] ?? risk);
137
+ for (const tool of group) {
138
+ const status = tool.enabledByDefault ? "" : dim(" (disabled)");
139
+ console.log(` ${tool.name}${status}`);
140
+ if (tool.description) console.log(` ${dim(tool.description)}`);
141
+ }
142
+ console.log("");
143
+ }
144
+ if (findings.length > 0) {
145
+ console.log(bold("Safety notes:"));
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(dim(`Files: ${setup.config.generate[0]?.outDir ?? "src/webmcp"}`));
154
+ console.log(dim(`Docs: https://webmcp-codegen.vercel.app/docs`));
155
+ console.log("");
156
+ }
157
+
23
158
  // src/wire.ts
24
159
  import { readFile, writeFile } from "fs/promises";
25
160
  import { dirname, join, relative } from "path";
@@ -151,38 +286,46 @@ async function firstExisting(paths) {
151
286
  }
152
287
 
153
288
  // src/cli.ts
154
- var HELP = `webmcp-codegen: generate WebMCP tools from the API contracts you already have
289
+ var HELP = `
290
+ webmcp-codegen \u2014 generate WebMCP tools from your OpenAPI spec
155
291
 
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
292
+ Usage
293
+ npx webmcp-codegen [command] [flags]
159
294
 
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)
295
+ Commands
296
+ generate Generate tool files from your spec (default when no command given)
297
+ dev Open the tools dashboard (list, describe, toggle, test)
298
+ init Write a codegen.config.mjs for full control
165
299
 
166
- Flags for generate:
300
+ Flags
167
301
  --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)
302
+ --out DIR Where tool files go (default: your web app's src/webmcp)
169
303
  --dry-run Preview what would be written, write nothing
170
- --skip-audit Skip the safety report
304
+ --verbose Show every tool, not just the summary
171
305
  --force Write files even when the audit reports errors
306
+ --skip-audit Skip the safety report
172
307
  --config PATH Use a config file at PATH
173
-
174
- Flags for dev:
175
308
  --port N Dashboard port (default: 4700)
309
+ --help Show this help
310
+
311
+ Examples
312
+ npx webmcp-codegen generate # detect spec, generate tools
313
+ npx webmcp-codegen generate --dry-run # preview without writing
314
+ npx webmcp-codegen dev # open the dashboard
315
+ npx webmcp-codegen generate --verbose # see all 72 tools listed
316
+
317
+ Docs
318
+ https://webmcp-codegen.vercel.app/docs
176
319
  `;
177
- var CONFIG_FILE = "codegen.config.mjs";
178
- var MAX_LISTED_TOOLS = 15;
179
320
  async function main() {
180
- const { positionals, values } = parseArgs({
321
+ const { values, positionals } = parseArgs({
322
+ args: process.argv.slice(2),
181
323
  allowPositionals: true,
182
324
  options: {
183
325
  "dry-run": { type: "boolean", default: false },
184
326
  "skip-audit": { type: "boolean", default: false },
185
327
  force: { type: "boolean", default: false },
328
+ verbose: { type: "boolean", default: false },
186
329
  watch: { type: "boolean", default: false },
187
330
  config: { type: "string" },
188
331
  spec: { type: "string" },
@@ -191,28 +334,32 @@ async function main() {
191
334
  help: { type: "boolean", default: false }
192
335
  }
193
336
  });
194
- const command = positionals[0];
195
- if (values.help || !command) {
337
+ const flags = {
338
+ dryRun: values["dry-run"] ?? false,
339
+ skipAudit: values["skip-audit"] ?? false,
340
+ force: values.force ?? false,
341
+ verbose: values.verbose ?? false,
342
+ watch: values.watch ?? false,
343
+ config: values.config,
344
+ spec: values.spec,
345
+ out: values.out,
346
+ port: values.port ? Number.parseInt(values.port, 10) : void 0,
347
+ help: values.help
348
+ };
349
+ if (flags.help) {
196
350
  console.log(HELP);
197
351
  return 0;
198
352
  }
353
+ const command = positionals[0] ?? "generate";
199
354
  switch (command) {
200
355
  case "init":
201
356
  return init();
202
357
  case "dev":
203
- return dev(Number.parseInt(values.port ?? "4700", 10));
358
+ return dev(flags.port ?? 4700);
204
359
  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
- });
360
+ return generate(flags);
214
361
  default:
215
- console.error(`Unknown command "${command}".
362
+ console.error(`Unknown command: ${command}
216
363
  `);
217
364
  console.log(HELP);
218
365
  return 1;
@@ -220,9 +367,12 @@ async function main() {
220
367
  }
221
368
  async function init() {
222
369
  const cwd = process.cwd();
223
- const configPath = join2(cwd, CONFIG_FILE);
370
+ const configFile = CONFIG_FILE_NAMES[0] ?? "codegen.config.mjs";
371
+ const configPath = join2(cwd, configFile);
224
372
  if (existsSync(configPath)) {
225
- console.error(`${CONFIG_FILE} already exists. Nothing to do.`);
373
+ console.error(`
374
+ \u2716 ${configFile} already exists. Nothing to do.
375
+ `);
226
376
  return 1;
227
377
  }
228
378
  const specs = await findSpecs(cwd);
@@ -245,40 +395,48 @@ export default defineConfig({
245
395
  });
246
396
  `
247
397
  );
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
- }
398
+ console.log(`
399
+ \u2714 Wrote ${configFile}
400
+ `);
401
+ console.log("Edit it to add sources, change the output directory, or set safety options.");
402
+ console.log("Docs: https://webmcp-codegen.vercel.app/docs/configuration\n");
258
403
  return 0;
259
404
  }
260
- async function generate(flags) {
405
+ async function dev(port) {
261
406
  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;
407
+ const server = await startDevServer({ cwd, port, open: true });
408
+ console.log(`
409
+ \u2714 Dashboard: http://localhost:${port}
410
+ `);
411
+ console.log("List, describe, enable, and test your tools. Ctrl+C to stop.\n");
412
+ await new Promise((resolveExit) => {
413
+ process.on("SIGINT", () => {
414
+ server.close();
415
+ resolveExit();
416
+ });
417
+ });
418
+ return 0;
268
419
  }
269
- async function runOnce(cwd, flags) {
270
- const setup = await resolveSetup(cwd, flags);
271
- const data = await loadDataFile(cwd);
420
+ async function generate(flags) {
421
+ const cwd = process.cwd();
422
+ const setup = await resolveSetup(cwd, {
423
+ dryRun: flags.dryRun,
424
+ skipAudit: flags.skipAudit,
425
+ force: flags.force,
426
+ watch: flags.watch,
427
+ spec: flags.spec,
428
+ out: flags.out,
429
+ configPath: flags.config
430
+ });
272
431
  const result = await runGenerate(setup.config, {
273
432
  cwd,
274
433
  dryRun: flags.dryRun,
275
- skipAudit: flags.skipAudit,
276
434
  force: flags.force,
277
- overrides: data.overrides
435
+ skipAudit: flags.skipAudit
278
436
  });
279
437
  let wiring = null;
280
438
  if (!result.blocked && setup.app) {
281
- const outDir = findOutDir(setup.config);
439
+ const outDir = setup.config.generate[0]?.outDir;
282
440
  if (outDir) {
283
441
  wiring = await planWiring(cwd, setup.app, outDir);
284
442
  if (wiring && wiring.edits.length > 0 && !flags.dryRun && result.wrote) {
@@ -289,143 +447,22 @@ async function runOnce(cwd, flags) {
289
447
  if (!setup.fromConfigFile && !flags.dryRun && result.wrote) {
290
448
  await saveDataFile(cwd, setup.remember);
291
449
  }
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;
450
+ if (flags.verbose) {
451
+ renderVerbose(result, setup, cwd);
452
+ } else {
453
+ renderSummary(result, setup, cwd, wiring);
360
454
  }
361
- if (flags.dryRun) {
362
- console.log("\nDry run: nothing written. Re-run without --dry-run to write these files.");
363
- return;
455
+ if (flags.watch && !flags.dryRun) {
456
+ console.log(dim("\n --watch is not implemented yet. Run generate again after changes.\n"));
364
457
  }
365
- printNextSteps(result);
458
+ return result.blocked ? 1 : 0;
366
459
  }
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)`);
460
+ main().then(
461
+ (code) => process.exit(code),
462
+ (error) => {
463
+ console.error("\n\u2716 Unexpected error:", error instanceof Error ? error.message : error);
464
+ console.error("\nPlease report this: https://github.com/SouravInsights/groundstate/issues\n");
465
+ process.exit(1);
374
466
  }
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.");
385
- }
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
- });
467
+ );
431
468
  //# sourceMappingURL=cli.js.map