webmcp-codegen 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -35
- package/dist/{chunk-N245GNCB.js → chunk-3LTHWIAP.js} +35 -2
- package/dist/chunk-3LTHWIAP.js.map +1 -0
- package/dist/chunk-CCVNYNJ5.js +235 -0
- package/dist/chunk-CCVNYNJ5.js.map +1 -0
- package/dist/{chunk-BIKKPCRT.js → chunk-FWSATV7C.js} +26 -2
- package/dist/chunk-FWSATV7C.js.map +1 -0
- package/dist/{chunk-OILNQ2HE.js → chunk-I4ZL527H.js} +168 -34
- package/dist/chunk-I4ZL527H.js.map +1 -0
- package/dist/{chunk-LYHLGSAI.js → chunk-MJQ5B6HB.js} +105 -15
- package/dist/chunk-MJQ5B6HB.js.map +1 -0
- package/dist/cli.js +258 -97
- package/dist/cli.js.map +1 -1
- package/dist/generators/index.d.ts +1 -1
- package/dist/generators/index.js +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.js +2 -2
- package/dist/server-DWP6IHCS.js +638 -0
- package/dist/server-DWP6IHCS.js.map +1 -0
- package/dist/sources/index.d.ts +1 -1
- package/dist/sources/index.js +2 -2
- package/dist/{types-DRCQtX0w.d.ts → types-DWUum51l.d.ts} +54 -1
- package/package.json +1 -1
- package/dist/chunk-BIKKPCRT.js.map +0 -1
- package/dist/chunk-LYHLGSAI.js.map +0 -1
- package/dist/chunk-N245GNCB.js.map +0 -1
- package/dist/chunk-OILNQ2HE.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,58 +1,153 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
js
|
|
9
|
-
} from "./chunk-OILNQ2HE.js";
|
|
3
|
+
findSpecs,
|
|
4
|
+
loadDataFile,
|
|
5
|
+
resolveSetup,
|
|
6
|
+
saveDataFile
|
|
7
|
+
} from "./chunk-CCVNYNJ5.js";
|
|
10
8
|
import {
|
|
11
|
-
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
9
|
+
runGenerate
|
|
10
|
+
} from "./chunk-MJQ5B6HB.js";
|
|
11
|
+
import "./chunk-3LTHWIAP.js";
|
|
12
|
+
import "./chunk-FWSATV7C.js";
|
|
13
|
+
import "./chunk-I4ZL527H.js";
|
|
14
14
|
import "./chunk-KSQMJERY.js";
|
|
15
15
|
|
|
16
16
|
// src/cli.ts
|
|
17
17
|
import { existsSync, watch } from "fs";
|
|
18
|
-
import { writeFile } from "fs/promises";
|
|
19
|
-
import {
|
|
18
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
19
|
+
import { join as join2, relative as relative2 } from "path";
|
|
20
|
+
import "readline/promises";
|
|
20
21
|
import { parseArgs } from "util";
|
|
21
22
|
|
|
22
|
-
// src/
|
|
23
|
-
import {
|
|
24
|
-
import { join, relative } from "path";
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
23
|
+
// src/wire.ts
|
|
24
|
+
import { readFile, writeFile } from "fs/promises";
|
|
25
|
+
import { dirname, join, relative } from "path";
|
|
26
|
+
async function planWiring(cwd, app, outDir) {
|
|
27
|
+
switch (app.framework) {
|
|
28
|
+
case "next":
|
|
29
|
+
return planNextWiring(cwd, app, outDir);
|
|
30
|
+
case "vite-react":
|
|
31
|
+
return planViteWiring(cwd, app, outDir);
|
|
32
|
+
default:
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function applyWiring(plan) {
|
|
37
|
+
for (const edit of plan.edits) {
|
|
38
|
+
await writeFile(edit.path, edit.contents, "utf8");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function planNextWiring(cwd, app, outDir) {
|
|
42
|
+
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")
|
|
47
|
+
];
|
|
48
|
+
const layoutPath = await firstExisting(layoutCandidates);
|
|
49
|
+
if (!layoutPath) return null;
|
|
50
|
+
const registerPath = join(cwd, outDir, "register.tsx");
|
|
51
|
+
const layout = await readFile(layoutPath, "utf8");
|
|
52
|
+
if (layout.includes("WebMCPRegister")) return { edits: [], alreadyWired: true };
|
|
53
|
+
const importPath = withoutExtension(relative(dirname(layoutPath), registerPath));
|
|
54
|
+
const edits = [
|
|
55
|
+
{
|
|
56
|
+
path: registerPath,
|
|
57
|
+
action: "create",
|
|
58
|
+
contents: nextRegisterComponent(),
|
|
59
|
+
summary: `created ${relative(cwd, registerPath)} (a client component that registers your tools on page load)`
|
|
45
60
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
61
|
+
];
|
|
62
|
+
const withImport = insertAfterLastImport(
|
|
63
|
+
layout,
|
|
64
|
+
`import { WebMCPRegister } from "${importPath}";`
|
|
65
|
+
);
|
|
66
|
+
if (!withImport) return null;
|
|
67
|
+
const withComponent = withImport.replace(
|
|
68
|
+
/<body([^>]*)>\s*/,
|
|
69
|
+
"<body$1>\n <WebMCPRegister />\n "
|
|
70
|
+
);
|
|
71
|
+
if (withComponent === withImport) return null;
|
|
72
|
+
edits.push({
|
|
73
|
+
path: layoutPath,
|
|
74
|
+
action: "modify",
|
|
75
|
+
contents: withComponent,
|
|
76
|
+
summary: `added 2 lines to ${relative(cwd, layoutPath)} (an import and <WebMCPRegister /> inside <body>)`
|
|
77
|
+
});
|
|
78
|
+
return { edits };
|
|
79
|
+
}
|
|
80
|
+
function nextRegisterComponent() {
|
|
81
|
+
return `"use client";
|
|
82
|
+
|
|
83
|
+
import { useEffect } from "react";
|
|
84
|
+
import { registerAllTools } from "./index";
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Registers the generated WebMCP tools once, on page load.
|
|
88
|
+
* Generated by webmcp-codegen. Safe to move; keep it mounted near the root.
|
|
89
|
+
*/
|
|
90
|
+
export function WebMCPRegister() {
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
void registerAllTools();
|
|
93
|
+
}, []);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
async function planViteWiring(cwd, app, outDir) {
|
|
99
|
+
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")
|
|
104
|
+
];
|
|
105
|
+
const entryPath = await firstExisting(entryCandidates);
|
|
106
|
+
if (!entryPath) return null;
|
|
107
|
+
const entry = await readFile(entryPath, "utf8");
|
|
108
|
+
if (entry.includes("registerAllTools")) return { edits: [], alreadyWired: true };
|
|
109
|
+
const importPath = withoutExtension(relative(dirname(entryPath), join(cwd, outDir, "index")));
|
|
110
|
+
const withWiring = insertAfterLastImport(
|
|
111
|
+
entry,
|
|
112
|
+
`import { registerAllTools } from "${importPath}";
|
|
113
|
+
|
|
114
|
+
void registerAllTools();`
|
|
115
|
+
);
|
|
116
|
+
if (!withWiring) return null;
|
|
117
|
+
return {
|
|
118
|
+
edits: [
|
|
119
|
+
{
|
|
120
|
+
path: entryPath,
|
|
121
|
+
action: "modify",
|
|
122
|
+
contents: withWiring,
|
|
123
|
+
summary: `added 2 lines to ${relative(cwd, entryPath)} (an import and a registerAllTools() call)`
|
|
51
124
|
}
|
|
125
|
+
]
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function insertAfterLastImport(source, line) {
|
|
129
|
+
const lines = source.split("\n");
|
|
130
|
+
let lastImport = -1;
|
|
131
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
132
|
+
if (/^import\s/.test(lines[index])) lastImport = index;
|
|
133
|
+
}
|
|
134
|
+
if (lastImport === -1) return null;
|
|
135
|
+
lines.splice(lastImport + 1, 0, line);
|
|
136
|
+
return lines.join("\n");
|
|
137
|
+
}
|
|
138
|
+
function withoutExtension(path) {
|
|
139
|
+
const bare = path.replace(/\.(tsx?|jsx?)$/, "").replace(/\/index$/, "");
|
|
140
|
+
return bare.startsWith(".") ? bare : `./${bare}`;
|
|
141
|
+
}
|
|
142
|
+
async function firstExisting(paths) {
|
|
143
|
+
for (const path of paths) {
|
|
144
|
+
try {
|
|
145
|
+
await readFile(path, "utf8");
|
|
146
|
+
return path;
|
|
147
|
+
} catch {
|
|
52
148
|
}
|
|
53
149
|
}
|
|
54
|
-
|
|
55
|
-
return found.sort((a, b) => a.depth - b.depth).map((entry) => relative(cwd, entry.path));
|
|
150
|
+
return void 0;
|
|
56
151
|
}
|
|
57
152
|
|
|
58
153
|
// src/cli.ts
|
|
@@ -60,22 +155,27 @@ var HELP = `webmcp-codegen: generate WebMCP tools from the API contracts you alr
|
|
|
60
155
|
|
|
61
156
|
Fastest start (no install, no config):
|
|
62
157
|
npx webmcp-codegen generate --dry-run Detect your spec, preview the tools
|
|
63
|
-
npx webmcp-codegen generate Write the tool files
|
|
158
|
+
npx webmcp-codegen generate Write the tool files, wire them up
|
|
64
159
|
|
|
65
160
|
Commands:
|
|
66
161
|
init Write a codegen.config.mjs for full control
|
|
67
162
|
generate Generate (or update) your WebMCP tools
|
|
68
163
|
generate --watch Re-generate when files change
|
|
164
|
+
dev Open the tools dashboard (list, edit, try tools)
|
|
69
165
|
|
|
70
166
|
Flags for generate:
|
|
71
167
|
--spec PATH Which OpenAPI spec to use (auto-detected when omitted)
|
|
72
|
-
--out DIR Where the tool files go (default:
|
|
168
|
+
--out DIR Where the tool files go (default: your web app's src/webmcp)
|
|
73
169
|
--dry-run Preview what would be written, write nothing
|
|
74
170
|
--skip-audit Skip the safety report
|
|
75
171
|
--force Write files even when the audit reports errors
|
|
76
172
|
--config PATH Use a config file at PATH
|
|
173
|
+
|
|
174
|
+
Flags for dev:
|
|
175
|
+
--port N Dashboard port (default: 4700)
|
|
77
176
|
`;
|
|
78
177
|
var CONFIG_FILE = "codegen.config.mjs";
|
|
178
|
+
var MAX_LISTED_TOOLS = 15;
|
|
79
179
|
async function main() {
|
|
80
180
|
const { positionals, values } = parseArgs({
|
|
81
181
|
allowPositionals: true,
|
|
@@ -87,6 +187,7 @@ async function main() {
|
|
|
87
187
|
config: { type: "string" },
|
|
88
188
|
spec: { type: "string" },
|
|
89
189
|
out: { type: "string" },
|
|
190
|
+
port: { type: "string" },
|
|
90
191
|
help: { type: "boolean", default: false }
|
|
91
192
|
}
|
|
92
193
|
});
|
|
@@ -98,6 +199,8 @@ async function main() {
|
|
|
98
199
|
switch (command) {
|
|
99
200
|
case "init":
|
|
100
201
|
return init();
|
|
202
|
+
case "dev":
|
|
203
|
+
return dev(Number.parseInt(values.port ?? "4700", 10));
|
|
101
204
|
case "generate":
|
|
102
205
|
return generate({
|
|
103
206
|
dryRun: values["dry-run"],
|
|
@@ -124,7 +227,7 @@ async function init() {
|
|
|
124
227
|
}
|
|
125
228
|
const specs = await findSpecs(cwd);
|
|
126
229
|
const specPath = specs.length > 0 ? `./${specs[0]}` : "./openapi.yaml";
|
|
127
|
-
await
|
|
230
|
+
await writeFile2(
|
|
128
231
|
configPath,
|
|
129
232
|
`import { defineConfig } from "webmcp-codegen";
|
|
130
233
|
import { openapi } from "webmcp-codegen/sources";
|
|
@@ -163,63 +266,58 @@ async function generate(flags) {
|
|
|
163
266
|
const result = await runOnce(cwd, flags);
|
|
164
267
|
return result.blocked ? 1 : 0;
|
|
165
268
|
}
|
|
166
|
-
async function resolveConfig(cwd, flags) {
|
|
167
|
-
const hasConfigFile = flags.configPath ? existsSync(join2(cwd, flags.configPath)) : CONFIG_FILE_NAMES.some((name) => existsSync(join2(cwd, name)));
|
|
168
|
-
if (hasConfigFile) {
|
|
169
|
-
const { config, path } = await loadConfig(cwd, flags.configPath);
|
|
170
|
-
if (flags.spec || flags.out) {
|
|
171
|
-
console.warn(`Note: --spec/--out are ignored; ${basename(path)} is in charge here.`);
|
|
172
|
-
}
|
|
173
|
-
return { config, label: basename(path) };
|
|
174
|
-
}
|
|
175
|
-
if (flags.configPath) {
|
|
176
|
-
throw new Error(`No config file at "${flags.configPath}".`);
|
|
177
|
-
}
|
|
178
|
-
const spec = flags.spec ?? await detectSpec(cwd);
|
|
179
|
-
const outDir = flags.out ?? "./src/webmcp";
|
|
180
|
-
return {
|
|
181
|
-
config: { sources: [openapi({ spec })], generate: [js({ outDir })] },
|
|
182
|
-
label: flags.spec ? `--spec ${spec}` : `detected ${spec}`
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
async function detectSpec(cwd) {
|
|
186
|
-
const specs = await findSpecs(cwd);
|
|
187
|
-
if (specs.length === 0) {
|
|
188
|
-
throw new Error(
|
|
189
|
-
"No OpenAPI spec found in this project.\nPoint at one: npx webmcp-codegen generate --spec path/to/openapi.json"
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
if (specs.length > 1) {
|
|
193
|
-
const list = specs.map((spec) => ` - ${spec}`).join("\n");
|
|
194
|
-
throw new Error(
|
|
195
|
-
`Found ${specs.length} API specs:
|
|
196
|
-
${list}
|
|
197
|
-
|
|
198
|
-
Pick one: npx webmcp-codegen generate --spec ${specs[0]}`
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
console.log(`Detected ${specs[0]} (override with --spec)
|
|
202
|
-
`);
|
|
203
|
-
return specs[0];
|
|
204
|
-
}
|
|
205
269
|
async function runOnce(cwd, flags) {
|
|
206
|
-
const
|
|
207
|
-
const
|
|
270
|
+
const setup = await resolveSetup(cwd, flags);
|
|
271
|
+
const data = await loadDataFile(cwd);
|
|
272
|
+
const result = await runGenerate(setup.config, {
|
|
208
273
|
cwd,
|
|
209
274
|
dryRun: flags.dryRun,
|
|
210
275
|
skipAudit: flags.skipAudit,
|
|
211
|
-
force: flags.force
|
|
276
|
+
force: flags.force,
|
|
277
|
+
overrides: data.overrides
|
|
212
278
|
});
|
|
213
|
-
|
|
279
|
+
let wiring = null;
|
|
280
|
+
if (!result.blocked && setup.app) {
|
|
281
|
+
const outDir = findOutDir(setup.config);
|
|
282
|
+
if (outDir) {
|
|
283
|
+
wiring = await planWiring(cwd, setup.app, outDir);
|
|
284
|
+
if (wiring && wiring.edits.length > 0 && !flags.dryRun && result.wrote) {
|
|
285
|
+
await applyWiring(wiring);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (!setup.fromConfigFile && !flags.dryRun && result.wrote) {
|
|
290
|
+
await saveDataFile(cwd, setup.remember);
|
|
291
|
+
}
|
|
292
|
+
printReport(result, flags, setup, wiring, cwd);
|
|
214
293
|
return result;
|
|
215
294
|
}
|
|
216
|
-
function
|
|
217
|
-
|
|
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;
|
|
218
300
|
console.log(`
|
|
219
|
-
webmcp-codegen (${
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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`);
|
|
223
321
|
}
|
|
224
322
|
if (findings.length > 0) {
|
|
225
323
|
console.log("");
|
|
@@ -237,23 +335,86 @@ webmcp-codegen (${configName}): ${tools.length} tool(s)
|
|
|
237
335
|
console.log(` ${shown}: ${relative2(cwd, file.path)}`);
|
|
238
336
|
}
|
|
239
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
|
+
}
|
|
240
355
|
if (blocked) {
|
|
241
356
|
console.log(
|
|
242
357
|
"\nGeneration blocked by audit errors. Fix them, or re-run with --force to write anyway."
|
|
243
358
|
);
|
|
244
|
-
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (flags.dryRun) {
|
|
245
362
|
console.log("\nDry run: nothing written. Re-run without --dry-run to write these files.");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
printNextSteps(result);
|
|
366
|
+
}
|
|
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}"`);
|
|
246
383
|
} else {
|
|
247
|
-
console.log("
|
|
384
|
+
console.log(" 3. Ask the agent to use one of your tools.");
|
|
248
385
|
}
|
|
249
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
|
+
}
|
|
250
411
|
async function watchLoop(cwd, flags) {
|
|
251
412
|
await runOnce(cwd, { ...flags, dryRun: false });
|
|
252
413
|
console.log("\nWatching for changes\u2026 (Ctrl+C to stop)");
|
|
253
414
|
let timer;
|
|
254
415
|
watch(cwd, { recursive: true }, (_event, filename) => {
|
|
255
416
|
if (!filename) return;
|
|
256
|
-
if (/node_modules|\.git|\/dist|\/src\/webmcp/.test(filename)) return;
|
|
417
|
+
if (/node_modules|\.git|\/dist|\/src\/webmcp|\.webmcp-codegen\.json/.test(filename)) return;
|
|
257
418
|
if (!/\.(ya?ml|json|ts|tsx|mts|mjs)$/.test(filename)) return;
|
|
258
419
|
clearTimeout(timer);
|
|
259
420
|
timer = setTimeout(() => {
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/detect.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * The webmcp-codegen CLI.\n *\n * The path we optimize for is the zero-everything first run:\n *\n * npx webmcp-codegen generate\n *\n * No install, no config file, no flags. The CLI detects your API spec and\n * generates into ./src/webmcp. When you outgrow the defaults:\n *\n * --spec/--out quick overrides without a config file\n * init writes codegen.config.mjs for full control (needs the\n * package installed, since the config imports from it)\n *\n * Plus the flags you'd expect on a codegen tool: --dry-run to preview,\n * --watch to re-run on change, --skip-audit to bypass the safety report,\n * --force to write through audit errors, --config to point at a config\n * file somewhere else.\n */\n\nimport { existsSync, watch } from \"node:fs\";\nimport { writeFile } from \"node:fs/promises\";\nimport { basename, join, relative } from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport { CONFIG_FILE_NAMES, loadConfig } from \"./config.js\";\nimport { findSpecs } from \"./detect.js\";\nimport { js } from \"./generators/js.js\";\nimport { type GenerateResult, runGenerate } from \"./pipeline.js\";\nimport { openapi } from \"./sources/openapi.js\";\nimport type { CodegenConfig } from \"./types.js\";\n\nconst HELP = `webmcp-codegen: generate WebMCP tools from the API contracts you already have\n\nFastest start (no install, no config):\n npx webmcp-codegen generate --dry-run Detect your spec, preview the tools\n npx webmcp-codegen generate Write the tool files\n\nCommands:\n init Write a codegen.config.mjs for full control\n generate Generate (or update) your WebMCP tools\n generate --watch Re-generate when files change\n\nFlags for generate:\n --spec PATH Which OpenAPI spec to use (auto-detected when omitted)\n --out DIR Where the tool files go (default: ./src/webmcp)\n --dry-run Preview what would be written, write nothing\n --skip-audit Skip the safety report\n --force Write files even when the audit reports errors\n --config PATH Use a config file at PATH\n`;\n\nconst CONFIG_FILE = \"codegen.config.mjs\";\n\nasync function main(): Promise<number> {\n const { positionals, values } = parseArgs({\n allowPositionals: true,\n options: {\n \"dry-run\": { type: \"boolean\", default: false },\n \"skip-audit\": { type: \"boolean\", default: false },\n force: { type: \"boolean\", default: false },\n watch: { type: \"boolean\", default: false },\n config: { type: \"string\" },\n spec: { type: \"string\" },\n out: { type: \"string\" },\n help: { type: \"boolean\", default: false },\n },\n });\n\n const command = positionals[0];\n if (values.help || !command) {\n console.log(HELP);\n return 0;\n }\n\n switch (command) {\n case \"init\":\n return init();\n case \"generate\":\n return generate({\n dryRun: values[\"dry-run\"],\n skipAudit: values[\"skip-audit\"],\n force: values.force,\n watch: values.watch,\n configPath: values.config,\n spec: values.spec,\n out: values.out,\n });\n default:\n console.error(`Unknown command \"${command}\".\\n`);\n console.log(HELP);\n return 1;\n }\n}\n\n/** Detect the project's API spec and write a starter config. */\nasync function init(): Promise<number> {\n const cwd = process.cwd();\n const configPath = join(cwd, CONFIG_FILE);\n\n if (existsSync(configPath)) {\n console.error(`${CONFIG_FILE} already exists. Nothing to do.`);\n return 1;\n }\n\n const specs = await findSpecs(cwd);\n const specPath = specs.length > 0 ? `./${specs[0]}` : \"./openapi.yaml\";\n\n await writeFile(\n configPath,\n `import { defineConfig } from \"webmcp-codegen\";\nimport { openapi } from \"webmcp-codegen/sources\";\nimport { js } from \"webmcp-codegen/generators\";\n\nexport default defineConfig({\n sources: [openapi({ spec: \"${specPath}\" })],\n generate: [js({ outDir: \"./src/webmcp\" })],\n safety: {\n // Extra field names to treat as PII, on top of the built-in list:\n // piiFields: [\"internalId\"],\n // Tools to skip entirely (matched against name and route):\n // exclude: [\"internal\"],\n },\n});\n`,\n );\n\n // The config imports from the package, so keeping it means installing it.\n console.log(\"Installed the package? A config file needs it:\");\n console.log(\" npm install -D webmcp-codegen\\n\");\n if (specs.length > 0) {\n console.log(`Found ${specs[0]}. Wrote ${CONFIG_FILE}.`);\n console.log(\"\\nNext: npx webmcp-codegen generate --dry-run\");\n } else {\n console.log(`No OpenAPI spec found, so ${CONFIG_FILE} points at ./openapi.yaml.`);\n console.log(\"Edit the `spec` path to point at your spec, then run:\");\n console.log(\"\\n npx webmcp-codegen generate --dry-run\");\n }\n return 0;\n}\n\ninterface GenerateFlags {\n dryRun: boolean;\n skipAudit: boolean;\n force: boolean;\n watch: boolean;\n configPath?: string;\n spec?: string;\n out?: string;\n}\n\nasync function generate(flags: GenerateFlags): Promise<number> {\n const cwd = process.cwd();\n\n if (flags.watch) {\n // Watch mode never exits; it re-runs generate on every relevant change.\n await watchLoop(cwd, flags);\n return 0;\n }\n\n const result = await runOnce(cwd, flags);\n return result.blocked ? 1 : 0;\n}\n\n/**\n * Where the tools come from, in priority order:\n *\n * 1. a config file (codegen.config.mjs or --config) for full control\n * 2. --spec/--out flags as quick overrides, no config needed\n * 3. auto-detection, on the zero-argument npx run\n *\n * Branches 2 and 3 build the config right here inside the CLI, which is\n * what makes `npx webmcp-codegen generate` work without installing the\n * package: the user's project never has to resolve a webmcp-codegen import.\n */\nasync function resolveConfig(\n cwd: string,\n flags: GenerateFlags,\n): Promise<{ config: CodegenConfig; label: string }> {\n const hasConfigFile = flags.configPath\n ? existsSync(join(cwd, flags.configPath))\n : CONFIG_FILE_NAMES.some((name) => existsSync(join(cwd, name)));\n\n if (hasConfigFile) {\n const { config, path } = await loadConfig(cwd, flags.configPath);\n if (flags.spec || flags.out) {\n console.warn(`Note: --spec/--out are ignored; ${basename(path)} is in charge here.`);\n }\n return { config, label: basename(path) };\n }\n if (flags.configPath) {\n throw new Error(`No config file at \"${flags.configPath}\".`);\n }\n\n const spec = flags.spec ?? (await detectSpec(cwd));\n const outDir = flags.out ?? \"./src/webmcp\";\n return {\n config: { sources: [openapi({ spec })], generate: [js({ outDir })] },\n label: flags.spec ? `--spec ${spec}` : `detected ${spec}`,\n };\n}\n\n/**\n * Find the project's API spec. One candidate: use it and say so. Several:\n * list them and make the human pick. None: say exactly what to do next.\n */\nasync function detectSpec(cwd: string): Promise<string> {\n const specs = await findSpecs(cwd);\n\n if (specs.length === 0) {\n throw new Error(\n \"No OpenAPI spec found in this project.\\n\" +\n \"Point at one: npx webmcp-codegen generate --spec path/to/openapi.json\",\n );\n }\n if (specs.length > 1) {\n const list = specs.map((spec) => ` - ${spec}`).join(\"\\n\");\n throw new Error(\n `Found ${specs.length} API specs:\\n${list}\\n\\n` +\n `Pick one: npx webmcp-codegen generate --spec ${specs[0]}`,\n );\n }\n\n console.log(`Detected ${specs[0]} (override with --spec)\\n`);\n return specs[0] as string;\n}\n\n/** One generate pass: resolve the config, run the pipeline, print the report. */\nasync function runOnce(cwd: string, flags: GenerateFlags): Promise<GenerateResult> {\n const { config, label } = await resolveConfig(cwd, flags);\n const result = await runGenerate(config, {\n cwd,\n dryRun: flags.dryRun,\n skipAudit: flags.skipAudit,\n force: flags.force,\n });\n printReport(result, flags, label, cwd);\n return result;\n}\n\n/**\n * The report is the product's voice: plain language, one line per file,\n * findings grouped by severity, and a summary that says what to do next.\n */\nfunction printReport(\n result: GenerateResult,\n flags: GenerateFlags,\n configName: string,\n cwd: string,\n): void {\n const { tools, findings, files, blocked } = result;\n\n console.log(`\\nwebmcp-codegen (${configName}): ${tools.length} tool(s)\\n`);\n\n for (const tool of tools) {\n console.log(` ${tool.name} [${tool.sideEffect}] ← ${tool.source.ref}`);\n }\n\n if (findings.length > 0) {\n console.log(\"\");\n for (const finding of findings) {\n const icon = finding.level === \"error\" ? \"✖\" : \"⚠\";\n const where = finding.tool ? ` (${finding.tool})` : \"\";\n console.log(` ${icon} ${finding.message}${where}`);\n }\n }\n\n if (files.length > 0) {\n console.log(\"\");\n for (const file of files) {\n if (file.action === \"unchanged\" && !file.conflict) continue;\n const shown = file.conflict\n ? `conflict → wrote ${relative(cwd, file.conflict)}`\n : file.action;\n console.log(` ${shown}: ${relative(cwd, file.path)}`);\n }\n }\n\n if (blocked) {\n console.log(\n \"\\nGeneration blocked by audit errors. Fix them, or re-run with --force to write anyway.\",\n );\n } else if (flags.dryRun) {\n console.log(\"\\nDry run: nothing written. Re-run without --dry-run to write these files.\");\n } else {\n console.log(\"\\nDone. Fill in each execute() below the marker, then registerAllTools().\");\n }\n}\n\n/**\n * Re-run generate when anything relevant changes. Node's recursive watcher\n * covers Linux/macOS/Windows on Node 20+, which is our engine floor anyway.\n */\nasync function watchLoop(cwd: string, flags: GenerateFlags): Promise<void> {\n await runOnce(cwd, { ...flags, dryRun: false });\n console.log(\"\\nWatching for changes… (Ctrl+C to stop)\");\n\n let timer: NodeJS.Timeout | undefined;\n watch(cwd, { recursive: true }, (_event, filename) => {\n if (!filename) return;\n // Only source-ish changes are worth regenerating for.\n if (/node_modules|\\.git|\\/dist|\\/src\\/webmcp/.test(filename)) return;\n if (!/\\.(ya?ml|json|ts|tsx|mts|mjs)$/.test(filename)) return;\n clearTimeout(timer);\n timer = setTimeout(() => {\n runOnce(cwd, { ...flags, dryRun: false }).catch((error: unknown) => {\n console.error(error instanceof Error ? error.message : error);\n });\n }, 300);\n });\n}\n\nmain()\n .then((code) => process.exit(code))\n .catch((error: unknown) => {\n console.error(error instanceof Error ? error.message : error);\n process.exit(1);\n });\n","/**\n * Spec auto-detection: the reason `npx webmcp-codegen generate` works with\n * zero arguments, zero config, and zero install.\n *\n * The rule is deliberately boring: walk the project (skipping the obvious\n * noise), recognize the usual spec filenames, and return what we find\n * shallowest-first. When exactly one spec exists we just use it; the CLI\n * layer decides what to do about zero or several.\n */\n\nimport type { Dirent } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join, relative } from \"node:path\";\n\n/** Filenames we recognize as API specs. */\nexport const SPEC_FILE_PATTERN = /^(openapi|swagger|api)\\.(ya?ml|json)$/i;\n\n/** Directories never worth descending into. */\nconst IGNORED_DIRS = new Set([\n \"node_modules\",\n \".git\",\n \".turbo\",\n \".next\",\n \"dist\",\n \"build\",\n \"coverage\",\n]);\n\n/**\n * How deep we look. Enough for monorepo layouts like\n * apps/server/openapi/openapi.json (depth 3) without wandering forever.\n */\nconst MAX_DEPTH = 5;\n\n/**\n * Find API spec files under `cwd`, returned as paths relative to `cwd`,\n * shallowest first. A root-level spec is a likelier intent than one\n * buried six folders deep.\n */\nexport async function findSpecs(cwd: string): Promise<string[]> {\n const found: { path: string; depth: number }[] = [];\n\n async function walk(dir: string, depth: number): Promise<void> {\n if (depth > MAX_DEPTH) return;\n let entries: Dirent[];\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return; // Unreadable directory. Skip it, never die on detection.\n }\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (!IGNORED_DIRS.has(entry.name)) await walk(join(dir, entry.name), depth + 1);\n } else if (SPEC_FILE_PATTERN.test(entry.name)) {\n found.push({ path: join(dir, entry.name), depth });\n }\n }\n }\n\n await walk(cwd, 0);\n return found.sort((a, b) => a.depth - b.depth).map((entry) => relative(cwd, entry.path));\n}\n"],"mappings":"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","names":["join","relative","join","relative"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/wire.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * The webmcp-codegen CLI.\n *\n * The path we optimize for is the zero-everything first run:\n *\n * npx webmcp-codegen generate\n *\n * No install, no config file, no flags. The CLI finds your API spec, finds\n * the package that is your web app, writes working tools into it, wires the\n * registration into your app's entry file, and tells you how to see it all\n * working. Choices we had to ask for are remembered in .webmcp-codegen.json\n * so we never ask twice.\n *\n * When you outgrow the defaults:\n *\n * --spec/--out quick overrides without a config file\n * init writes codegen.config.mjs for full control (needs the\n * package installed, since the config imports from it)\n *\n * Plus the flags you'd expect on a codegen tool: --dry-run to preview,\n * --watch to re-run on change, --skip-audit to bypass the safety report,\n * --force to write through audit errors, --config to point at a config\n * file somewhere else.\n */\n\nimport { existsSync, watch } from \"node:fs\";\nimport { writeFile } from \"node:fs/promises\";\nimport { basename, join, relative } from \"node:path\";\nimport { createInterface } from \"node:readline/promises\";\nimport { parseArgs } from \"node:util\";\nimport { CONFIG_FILE_NAMES, loadConfig } from \"./config.js\";\nimport { loadDataFile, saveDataFile } from \"./data-file.js\";\nimport { findSpecs } from \"./detect.js\";\nimport { js } from \"./generators/js.js\";\nimport { type GenerateResult, runGenerate } from \"./pipeline.js\";\nimport { type GenerateFlags, resolveSetup, type Setup } from \"./setup.js\";\nimport { openapi } from \"./sources/openapi.js\";\nimport type { CodegenConfig, ReviewedTool } from \"./types.js\";\nimport { applyWiring, planWiring, type WirePlan } from \"./wire.js\";\n\nconst HELP = `webmcp-codegen: generate WebMCP tools from the API contracts you already have\n\nFastest start (no install, no config):\n npx webmcp-codegen generate --dry-run Detect your spec, preview the tools\n npx webmcp-codegen generate Write the tool files, wire them up\n\nCommands:\n init Write a codegen.config.mjs for full control\n generate Generate (or update) your WebMCP tools\n generate --watch Re-generate when files change\n dev Open the tools dashboard (list, edit, try tools)\n\nFlags for generate:\n --spec PATH Which OpenAPI spec to use (auto-detected when omitted)\n --out DIR Where the tool files go (default: your web app's src/webmcp)\n --dry-run Preview what would be written, write nothing\n --skip-audit Skip the safety report\n --force Write files even when the audit reports errors\n --config PATH Use a config file at PATH\n\nFlags for dev:\n --port N Dashboard port (default: 4700)\n`;\n\nconst CONFIG_FILE = \"codegen.config.mjs\";\n\n/** How many tools to list before folding the rest into a count. */\nconst MAX_LISTED_TOOLS = 15;\n\nasync function main(): Promise<number> {\n const { positionals, values } = parseArgs({\n allowPositionals: true,\n options: {\n \"dry-run\": { type: \"boolean\", default: false },\n \"skip-audit\": { type: \"boolean\", default: false },\n force: { type: \"boolean\", default: false },\n watch: { type: \"boolean\", default: false },\n config: { type: \"string\" },\n spec: { type: \"string\" },\n out: { type: \"string\" },\n port: { type: \"string\" },\n help: { type: \"boolean\", default: false },\n },\n });\n\n const command = positionals[0];\n if (values.help || !command) {\n console.log(HELP);\n return 0;\n }\n\n switch (command) {\n case \"init\":\n return init();\n case \"dev\":\n return dev(Number.parseInt(values.port ?? \"4700\", 10));\n case \"generate\":\n return generate({\n dryRun: values[\"dry-run\"],\n skipAudit: values[\"skip-audit\"],\n force: values.force,\n watch: values.watch,\n configPath: values.config,\n spec: values.spec,\n out: values.out,\n });\n default:\n console.error(`Unknown command \"${command}\".\\n`);\n console.log(HELP);\n return 1;\n }\n}\n\n/** Detect the project's API spec and write a starter config. */\nasync function init(): Promise<number> {\n const cwd = process.cwd();\n const configPath = join(cwd, CONFIG_FILE);\n\n if (existsSync(configPath)) {\n console.error(`${CONFIG_FILE} already exists. Nothing to do.`);\n return 1;\n }\n\n const specs = await findSpecs(cwd);\n const specPath = specs.length > 0 ? `./${specs[0]}` : \"./openapi.yaml\";\n\n await writeFile(\n configPath,\n `import { defineConfig } from \"webmcp-codegen\";\nimport { openapi } from \"webmcp-codegen/sources\";\nimport { js } from \"webmcp-codegen/generators\";\n\nexport default defineConfig({\n sources: [openapi({ spec: \"${specPath}\" })],\n generate: [js({ outDir: \"./src/webmcp\" })],\n safety: {\n // Extra field names to treat as PII, on top of the built-in list:\n // piiFields: [\"internalId\"],\n // Tools to skip entirely (matched against name and route):\n // exclude: [\"internal\"],\n },\n});\n`,\n );\n\n // The config imports from the package, so keeping it means installing it.\n console.log(\"Installed the package? A config file needs it:\");\n console.log(\" npm install -D webmcp-codegen\\n\");\n if (specs.length > 0) {\n console.log(`Found ${specs[0]}. Wrote ${CONFIG_FILE}.`);\n console.log(\"\\nNext: npx webmcp-codegen generate --dry-run\");\n } else {\n console.log(`No OpenAPI spec found, so ${CONFIG_FILE} points at ./openapi.yaml.`);\n console.log(\"Edit the `spec` path to point at your spec, then run:\");\n console.log(\"\\n npx webmcp-codegen generate --dry-run\");\n }\n return 0;\n}\n\nasync function generate(flags: GenerateFlags): Promise<number> {\n const cwd = process.cwd();\n\n if (flags.watch) {\n // Watch mode never exits; it re-runs generate on every relevant change.\n // Wiring is idempotent, so it is part of every pass and quietly no-ops\n // after the first.\n await watchLoop(cwd, flags);\n return 0;\n }\n\n const result = await runOnce(cwd, flags);\n return result.blocked ? 1 : 0;\n}\n\n/** One generate pass: resolve setup, run the pipeline, wire, report. */\nasync function runOnce(cwd: string, flags: GenerateFlags): Promise<GenerateResult> {\n const setup = await resolveSetup(cwd, flags);\n const data = await loadDataFile(cwd);\n const result = await runGenerate(setup.config, {\n cwd,\n dryRun: flags.dryRun,\n skipAudit: flags.skipAudit,\n force: flags.force,\n overrides: data.overrides,\n });\n\n // Registration wiring: additive, idempotent, and only for real runs.\n let wiring: WirePlan | null = null;\n if (!result.blocked && setup.app) {\n const outDir = findOutDir(setup.config);\n if (outDir) {\n wiring = await planWiring(cwd, setup.app, outDir);\n if (wiring && wiring.edits.length > 0 && !flags.dryRun && result.wrote) {\n await applyWiring(wiring);\n }\n }\n }\n\n // Remember the choices detection made, so the next run never re-asks.\n if (!setup.fromConfigFile && !flags.dryRun && result.wrote) {\n await saveDataFile(cwd, setup.remember);\n }\n\n printReport(result, flags, setup, wiring, cwd);\n return result;\n}\n\n/** Pull the outDir back out of the resolved config (there is one generator). */\nfunction findOutDir(config: CodegenConfig): string | undefined {\n return config.generate[0]?.outDir;\n}\n\n/**\n * The report is the product's voice: plain language, no jargon, one line per\n * file, findings grouped by severity, and a summary that says what happens\n * next — including the one command's worth of \"try it\" at the end.\n */\nfunction printReport(\n result: GenerateResult,\n flags: GenerateFlags,\n setup: Setup,\n wiring: WirePlan | null,\n cwd: string,\n): void {\n const { tools, skipped, findings, files, notes, blocked } = result;\n\n console.log(`\\nwebmcp-codegen (${setup.label}): ${tools.length} tool(s)`);\n\n for (const note of notes) {\n console.log(`\\n note: ${note}`);\n }\n\n if (skipped.length > 0) {\n console.log(`\\n ${skipped.length} endpoint(s) skipped:`);\n for (const entry of skipped) {\n console.log(` ${entry.ref}: ${entry.reason}`);\n }\n }\n\n console.log(\"\");\n const listed = tools.slice(0, MAX_LISTED_TOOLS);\n for (const tool of listed) {\n const state = tool.enabledByDefault ? \"\" : \" starts disabled\";\n console.log(` ${tool.name} [${tool.sideEffect}]${state} ← ${tool.source.ref}`);\n }\n if (tools.length > listed.length) {\n console.log(` …and ${tools.length - listed.length} more`);\n }\n\n if (findings.length > 0) {\n console.log(\"\");\n for (const finding of findings) {\n const icon = finding.level === \"error\" ? \"✖\" : \"⚠\";\n const where = finding.tool ? ` (${finding.tool})` : \"\";\n console.log(` ${icon} ${finding.message}${where}`);\n }\n }\n\n if (files.length > 0) {\n console.log(\"\");\n for (const file of files) {\n if (file.action === \"unchanged\" && !file.conflict) continue;\n const shown = file.conflict\n ? `conflict → wrote ${relative(cwd, file.conflict)}`\n : file.action;\n console.log(` ${shown}: ${relative(cwd, file.path)}`);\n }\n }\n\n if (wiring) {\n if (wiring.alreadyWired) {\n console.log(\"\\n registration: already wired into your app\");\n } else if (wiring.edits.length > 0) {\n console.log(flags.dryRun ? \"\\n registration (would do):\" : \"\\n registration:\");\n for (const edit of wiring.edits) {\n console.log(` ${edit.summary}`);\n }\n if (!flags.dryRun) {\n console.log(\" undo: delete the added lines (nothing else was touched)\");\n }\n }\n } else if (!blocked && setup.app) {\n console.log(\"\\n registration: could not find your app's entry file, so add this by hand:\");\n console.log(' import { registerAllTools } from \"<path-to>/src/webmcp\";');\n console.log(\" void registerAllTools();\");\n }\n\n if (blocked) {\n console.log(\n \"\\nGeneration blocked by audit errors. Fix them, or re-run with --force to write anyway.\",\n );\n return;\n }\n if (flags.dryRun) {\n console.log(\"\\nDry run: nothing written. Re-run without --dry-run to write these files.\");\n return;\n }\n\n printNextSteps(result);\n}\n\n/**\n * The parting message. The read tools already work; the mutations are one\n * uncomment away; and we end with the single most convincing thing a new\n * user can do: watch an agent call their app.\n */\nfunction printNextSteps(result: GenerateResult): void {\n const enabled = result.tools.filter((tool) => tool.enabledByDefault);\n const disabled = result.tools.filter((tool) => !tool.enabledByDefault);\n\n const parts: string[] = [];\n if (enabled.length > 0) parts.push(`${enabled.length} read tool(s) work out of the box`);\n if (disabled.length > 0) {\n parts.push(`${disabled.length} tool(s) start disabled (open the file and uncomment to enable)`);\n }\n console.log(`\\nDone. ${parts.join(\"; \")}.`);\n\n const suggestion = pickSuggestedTool(result.tools);\n console.log(\"\\nTry it:\");\n console.log(\" 1. Start your app and open it in Chrome.\");\n console.log(\" 2. Turn on chrome://flags/#enable-webmcp-testing and reload the page.\");\n if (suggestion) {\n console.log(` 3. Ask the agent: \"${suggestion}\"`);\n } else {\n console.log(\" 3. Ask the agent to use one of your tools.\");\n }\n}\n\n/**\n * The example request in the \"try it\" line. Pick an enabled read tool —\n * preferably one whose name sounds like listing or looking something up —\n * and phrase it the way a user would say it, from the spec's own description.\n */\nfunction pickSuggestedTool(tools: ReviewedTool[]): string | undefined {\n const reads = tools.filter((tool) => tool.enabledByDefault && tool.endpointRole === \"endpoint\");\n if (reads.length === 0) return undefined;\n const tool =\n reads.find((candidate) => /^(list|get|search|find|fetch|recent)-/.test(candidate.name)) ??\n reads[0];\n if (!tool) return undefined;\n\n const description = tool.description.trim().replace(/\\.$/, \"\");\n // A template description (\"GET /v1/trips\") would read as jargon; fall back\n // to the tool name in words (\"list trips\").\n const looksTemplated = /^(GET|POST|PUT|PATCH|DELETE)\\s/.test(description);\n const phrase = looksTemplated\n ? tool.name.replace(/-/g, \" \")\n : description.charAt(0).toLowerCase() + description.slice(1);\n return phrase;\n}\n\n/**\n * The tools dashboard: a local control panel for what was generated.\n * Runs until Ctrl+C; nothing is written to the app, ever.\n */\nasync function dev(port: number): Promise<number> {\n const { startDevServer } = await import(\"./dev/server.js\");\n const server = await startDevServer({ cwd: process.cwd(), port });\n console.log(`\\nwebmcp-codegen dashboard: http://localhost:${port}`);\n console.log(\"List, describe, enable, and try your tools. Ctrl+C to stop.\\n\");\n\n await new Promise<void>((resolveExit) => {\n process.on(\"SIGINT\", () => {\n server.close();\n resolveExit();\n });\n });\n return 0;\n}\n\n/**\n * Re-run generate when anything relevant changes. Node's recursive watcher\n * covers Linux/macOS/Windows on Node 20+, which is our engine floor anyway.\n */\nasync function watchLoop(cwd: string, flags: GenerateFlags): Promise<void> {\n await runOnce(cwd, { ...flags, dryRun: false });\n console.log(\"\\nWatching for changes… (Ctrl+C to stop)\");\n\n let timer: NodeJS.Timeout | undefined;\n watch(cwd, { recursive: true }, (_event, filename) => {\n if (!filename) return;\n // Only source-ish changes are worth regenerating for. Never react to our\n // own outputs (generated files, the data file) or watch mode loops.\n if (/node_modules|\\.git|\\/dist|\\/src\\/webmcp|\\.webmcp-codegen\\.json/.test(filename)) return;\n if (!/\\.(ya?ml|json|ts|tsx|mts|mjs)$/.test(filename)) return;\n clearTimeout(timer);\n timer = setTimeout(() => {\n runOnce(cwd, { ...flags, dryRun: false }).catch((error: unknown) => {\n console.error(error instanceof Error ? error.message : error);\n });\n }, 300);\n });\n}\n\nmain()\n .then((code) => process.exit(code))\n .catch((error: unknown) => {\n console.error(error instanceof Error ? error.message : error);\n process.exit(1);\n });\n","/**\n * Registration wiring: making the app actually register its tools.\n *\n * Generated files do nothing until something calls registerAllTools() once at\n * startup. Rather than telling the developer to go do that, we do it for\n * them — under strict rules, because this is the one place we edit *their*\n * files instead of ours:\n *\n * 1. Edits are additive only. We insert lines; we never change or remove\n * existing ones.\n * 2. Idempotent. If the wiring is already there, we do nothing.\n * 3. Honest. Every edit is reported with exact paths and how to undo it.\n * 4. When we cannot find the entry point with confidence, we do not guess:\n * we print the two lines and where they go, and leave it to the human.\n */\n\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join, relative } from \"node:path\";\nimport type { WebApp } from \"./detect-app.js\";\n\nexport interface WireEdit {\n /** Absolute path of the file to create or modify. */\n path: string;\n action: \"create\" | \"modify\";\n /** The full new contents (for modify: the existing contents plus our lines). */\n contents: string;\n /** One human sentence per edit, for the report. */\n summary: string;\n}\n\nexport interface WirePlan {\n edits: WireEdit[];\n /** Set when we already found the wiring in place. */\n alreadyWired?: boolean;\n}\n\n/**\n * Compute the wiring edits for the detected app, or null when we cannot\n * locate the entry point with confidence (the CLI then prints manual\n * instructions instead).\n */\nexport async function planWiring(\n cwd: string,\n app: WebApp,\n outDir: string,\n): Promise<WirePlan | null> {\n switch (app.framework) {\n case \"next\":\n return planNextWiring(cwd, app, outDir);\n case \"vite-react\":\n return planViteWiring(cwd, app, outDir);\n default:\n // Nuxt/SvelteKit/unknown: we know where the tools go but not where the\n // app boots. Print instructions rather than guess-edit an entry file.\n return null;\n }\n}\n\n/** Apply a plan. Kept trivial on purpose: the plan already did the thinking. */\nexport async function applyWiring(plan: WirePlan): Promise<void> {\n for (const edit of plan.edits) {\n await writeFile(edit.path, edit.contents, \"utf8\");\n }\n}\n\n/* ── Next.js (app router) ──────────────────────────────────────────────── */\n\n/**\n * Next needs the registration to run on the client, so we generate a tiny\n * \"use client\" component next to the tools and mount it in the root layout:\n *\n * import { WebMCPRegister } from \"../webmcp/register\"; ← added\n * ...\n * <body>\n * <WebMCPRegister /> ← added\n * {children}\n */\nasync function planNextWiring(cwd: string, app: WebApp, outDir: string): Promise<WirePlan | null> {\n const layoutCandidates = [\n join(cwd, app.dir, \"src/app/layout.tsx\"),\n join(cwd, app.dir, \"src/app/layout.jsx\"),\n join(cwd, app.dir, \"app/layout.tsx\"),\n join(cwd, app.dir, \"app/layout.jsx\"),\n ];\n const layoutPath = await firstExisting(layoutCandidates);\n if (!layoutPath) return null;\n\n const registerPath = join(cwd, outDir, \"register.tsx\");\n const layout = await readFile(layoutPath, \"utf8\");\n if (layout.includes(\"WebMCPRegister\")) return { edits: [], alreadyWired: true };\n\n // Import path from the layout's directory to the register component.\n const importPath = withoutExtension(relative(dirname(layoutPath), registerPath));\n\n const edits: WireEdit[] = [\n {\n path: registerPath,\n action: \"create\",\n contents: nextRegisterComponent(),\n summary: `created ${relative(cwd, registerPath)} (a client component that registers your tools on page load)`,\n },\n ];\n\n const withImport = insertAfterLastImport(\n layout,\n `import { WebMCPRegister } from \"${importPath}\";`,\n );\n if (!withImport) return null;\n // Mount right after <body ...>, each element on its own line.\n const withComponent = withImport.replace(\n /<body([^>]*)>\\s*/,\n \"<body$1>\\n <WebMCPRegister />\\n \",\n );\n if (withComponent === withImport) return null; // No <body> tag found; do not guess.\n\n edits.push({\n path: layoutPath,\n action: \"modify\",\n contents: withComponent,\n summary: `added 2 lines to ${relative(cwd, layoutPath)} (an import and <WebMCPRegister /> inside <body>)`,\n });\n return { edits };\n}\n\nfunction nextRegisterComponent(): string {\n return `\"use client\";\n\nimport { useEffect } from \"react\";\nimport { registerAllTools } from \"./index\";\n\n/**\n * Registers the generated WebMCP tools once, on page load.\n * Generated by webmcp-codegen. Safe to move; keep it mounted near the root.\n */\nexport function WebMCPRegister() {\n useEffect(() => {\n void registerAllTools();\n }, []);\n return null;\n}\n`;\n}\n\n/* ── Vite + React (SPAs) ───────────────────────────────────────────────── */\n\n/**\n * A Vite app boots in main.tsx, so wiring is two added lines there:\n *\n * import { registerAllTools } from \"./webmcp\"; ← added\n * void registerAllTools(); ← added\n */\nasync function planViteWiring(cwd: string, app: WebApp, outDir: string): Promise<WirePlan | null> {\n const entryCandidates = [\n join(cwd, app.dir, \"src/main.tsx\"),\n join(cwd, app.dir, \"src/main.jsx\"),\n join(cwd, app.dir, \"src/index.tsx\"),\n join(cwd, app.dir, \"src/index.jsx\"),\n ];\n const entryPath = await firstExisting(entryCandidates);\n if (!entryPath) return null;\n\n const entry = await readFile(entryPath, \"utf8\");\n if (entry.includes(\"registerAllTools\")) return { edits: [], alreadyWired: true };\n\n const importPath = withoutExtension(relative(dirname(entryPath), join(cwd, outDir, \"index\")));\n const withWiring = insertAfterLastImport(\n entry,\n `import { registerAllTools } from \"${importPath}\";\\n\\nvoid registerAllTools();`,\n );\n if (!withWiring) return null;\n\n return {\n edits: [\n {\n path: entryPath,\n action: \"modify\",\n contents: withWiring,\n summary: `added 2 lines to ${relative(cwd, entryPath)} (an import and a registerAllTools() call)`,\n },\n ],\n };\n}\n\n/* ── Shared helpers ────────────────────────────────────────────────────── */\n\n/** Insert a line after the file's last top-level import statement. */\nfunction insertAfterLastImport(source: string, line: string): string | null {\n const lines = source.split(\"\\n\");\n let lastImport = -1;\n for (let index = 0; index < lines.length; index += 1) {\n if (/^import\\s/.test(lines[index] as string)) lastImport = index;\n }\n if (lastImport === -1) return null;\n lines.splice(lastImport + 1, 0, line);\n return lines.join(\"\\n\");\n}\n\n/**\n * Turn a filesystem path into a JS import specifier: no extension, and an\n * explicit \"./\" when the target is in the same directory or deeper —\n * `relative()` alone yields \"webmcp/index\", which JS would read as a\n * package name, not a file.\n */\nfunction withoutExtension(path: string): string {\n const bare = path.replace(/\\.(tsx?|jsx?)$/, \"\").replace(/\\/index$/, \"\");\n return bare.startsWith(\".\") ? bare : `./${bare}`;\n}\n\nasync function firstExisting(paths: string[]): Promise<string | undefined> {\n for (const path of paths) {\n try {\n await readFile(path, \"utf8\");\n return path;\n } catch {\n // Try the next candidate.\n }\n }\n return undefined;\n}\n"],"mappings":"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","names":["writeFile","join","relative","join","writeFile","relative","tool"]}
|
package/dist/generators/index.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as CodegenConfig, R as ReviewedTool, A as AuditFinding, G as GeneratedFile } from './types-
|
|
2
|
-
export {
|
|
1
|
+
import { C as CodegenConfig, a as ToolOverrides, R as ReviewedTool, b as SkippedEndpoint, A as AuditFinding, G as GeneratedFile } from './types-DWUum51l.js';
|
|
2
|
+
export { c as CandidateTool, J as JsonSchema, d as RiskTier, e as SafetyOptions, f as SideEffect, S as Source, g as SourceKind, T as ToolGenerator, h as ToolHints } from './types-DWUum51l.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Config: `defineConfig` for authoring, `loadConfig` for the CLI.
|
|
@@ -34,11 +34,21 @@ interface GenerateOptions {
|
|
|
34
34
|
skipAudit?: boolean;
|
|
35
35
|
/** Write even when the audit found errors. The report still shows them. */
|
|
36
36
|
force?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Hand-authored tweaks from .webmcp-codegen.json (usually written by the
|
|
39
|
+
* dev dashboard). Applied after the safety review so they survive
|
|
40
|
+
* regeneration.
|
|
41
|
+
*/
|
|
42
|
+
overrides?: ToolOverrides;
|
|
37
43
|
}
|
|
38
44
|
interface GenerateResult {
|
|
39
45
|
tools: ReviewedTool[];
|
|
46
|
+
/** Endpoints deliberately not generated (webhooks, config exclusions). */
|
|
47
|
+
skipped: SkippedEndpoint[];
|
|
40
48
|
findings: AuditFinding[];
|
|
41
49
|
files: GeneratedFile[];
|
|
50
|
+
/** Human-facing pipeline notes, e.g. "stripped the shared v1 prefix". */
|
|
51
|
+
notes: string[];
|
|
42
52
|
/** True when audit errors stopped any file from being written. */
|
|
43
53
|
blocked: boolean;
|
|
44
54
|
/** True when this run actually wrote files (false for dry runs and blocks). */
|