webmcp-codegen 0.1.0 → 0.2.0
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 +18 -4
- package/dist/chunk-GDJDVR4E.js +251 -0
- package/dist/chunk-GDJDVR4E.js.map +1 -0
- package/dist/{chunk-NFZ5FMDO.js → chunk-R3DEBBQ3.js} +2 -1
- package/dist/chunk-R3DEBBQ3.js.map +1 -0
- package/dist/chunk-WYGVTIGI.js +140 -0
- package/dist/chunk-WYGVTIGI.js.map +1 -0
- package/dist/cli.js +115 -22
- package/dist/cli.js.map +1 -1
- package/dist/generators/index.js +3 -245
- package/dist/generators/index.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/sources/index.js +4 -134
- package/dist/sources/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-NFZ5FMDO.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,31 +1,81 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
CONFIG_FILE_NAMES,
|
|
3
4
|
loadConfig,
|
|
4
5
|
runGenerate
|
|
5
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-R3DEBBQ3.js";
|
|
7
|
+
import {
|
|
8
|
+
js
|
|
9
|
+
} from "./chunk-GDJDVR4E.js";
|
|
10
|
+
import {
|
|
11
|
+
openapi
|
|
12
|
+
} from "./chunk-WYGVTIGI.js";
|
|
6
13
|
import "./chunk-BIKKPCRT.js";
|
|
7
14
|
import "./chunk-5L4KN6F4.js";
|
|
8
15
|
|
|
9
16
|
// src/cli.ts
|
|
10
17
|
import { existsSync, watch } from "fs";
|
|
11
|
-
import {
|
|
12
|
-
import { basename, join, relative } from "path";
|
|
18
|
+
import { writeFile } from "fs/promises";
|
|
19
|
+
import { basename, join as join2, relative as relative2 } from "path";
|
|
13
20
|
import { parseArgs } from "util";
|
|
21
|
+
|
|
22
|
+
// src/detect.ts
|
|
23
|
+
import { readdir } from "fs/promises";
|
|
24
|
+
import { join, relative } from "path";
|
|
25
|
+
var SPEC_FILE_PATTERN = /^(openapi|swagger|api)\.(ya?ml|json)$/i;
|
|
26
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
27
|
+
"node_modules",
|
|
28
|
+
".git",
|
|
29
|
+
".turbo",
|
|
30
|
+
".next",
|
|
31
|
+
"dist",
|
|
32
|
+
"build",
|
|
33
|
+
"coverage"
|
|
34
|
+
]);
|
|
35
|
+
var MAX_DEPTH = 5;
|
|
36
|
+
async function findSpecs(cwd) {
|
|
37
|
+
const found = [];
|
|
38
|
+
async function walk(dir, depth) {
|
|
39
|
+
if (depth > MAX_DEPTH) return;
|
|
40
|
+
let entries;
|
|
41
|
+
try {
|
|
42
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
43
|
+
} catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (entry.isDirectory()) {
|
|
48
|
+
if (!IGNORED_DIRS.has(entry.name)) await walk(join(dir, entry.name), depth + 1);
|
|
49
|
+
} else if (SPEC_FILE_PATTERN.test(entry.name)) {
|
|
50
|
+
found.push({ path: join(dir, entry.name), depth });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
await walk(cwd, 0);
|
|
55
|
+
return found.sort((a, b) => a.depth - b.depth).map((entry) => relative(cwd, entry.path));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/cli.ts
|
|
14
59
|
var HELP = `webmcp-codegen \u2014 generate WebMCP tools from the API contracts you already have
|
|
15
60
|
|
|
16
|
-
|
|
17
|
-
webmcp-codegen
|
|
18
|
-
webmcp-codegen generate
|
|
19
|
-
|
|
61
|
+
Fastest start (no install, no config):
|
|
62
|
+
npx webmcp-codegen generate --dry-run Detect your spec, preview the tools
|
|
63
|
+
npx webmcp-codegen generate Write the tool files
|
|
64
|
+
|
|
65
|
+
Commands:
|
|
66
|
+
init Write a codegen.config.mjs for full control
|
|
67
|
+
generate Generate (or update) your WebMCP tools
|
|
68
|
+
generate --watch Re-generate when files change
|
|
20
69
|
|
|
21
70
|
Flags for generate:
|
|
71
|
+
--spec PATH Which OpenAPI spec to use (auto-detected when omitted)
|
|
72
|
+
--out DIR Where the tool files go (default: ./src/webmcp)
|
|
22
73
|
--dry-run Preview what would be written, write nothing
|
|
23
74
|
--skip-audit Skip the safety report
|
|
24
75
|
--force Write files even when the audit reports errors
|
|
25
76
|
--config PATH Use a config file at PATH
|
|
26
77
|
`;
|
|
27
78
|
var CONFIG_FILE = "codegen.config.mjs";
|
|
28
|
-
var SPEC_FILE_PATTERN = /^(openapi|swagger|api)\.(ya?ml|json)$/i;
|
|
29
79
|
async function main() {
|
|
30
80
|
const { positionals, values } = parseArgs({
|
|
31
81
|
allowPositionals: true,
|
|
@@ -35,6 +85,8 @@ async function main() {
|
|
|
35
85
|
force: { type: "boolean", default: false },
|
|
36
86
|
watch: { type: "boolean", default: false },
|
|
37
87
|
config: { type: "string" },
|
|
88
|
+
spec: { type: "string" },
|
|
89
|
+
out: { type: "string" },
|
|
38
90
|
help: { type: "boolean", default: false }
|
|
39
91
|
}
|
|
40
92
|
});
|
|
@@ -52,7 +104,9 @@ async function main() {
|
|
|
52
104
|
skipAudit: values["skip-audit"],
|
|
53
105
|
force: values.force,
|
|
54
106
|
watch: values.watch,
|
|
55
|
-
configPath: values.config
|
|
107
|
+
configPath: values.config,
|
|
108
|
+
spec: values.spec,
|
|
109
|
+
out: values.out
|
|
56
110
|
});
|
|
57
111
|
default:
|
|
58
112
|
console.error(`Unknown command "${command}".
|
|
@@ -63,13 +117,13 @@ async function main() {
|
|
|
63
117
|
}
|
|
64
118
|
async function init() {
|
|
65
119
|
const cwd = process.cwd();
|
|
66
|
-
const configPath =
|
|
120
|
+
const configPath = join2(cwd, CONFIG_FILE);
|
|
67
121
|
if (existsSync(configPath)) {
|
|
68
122
|
console.error(`${CONFIG_FILE} already exists \u2014 nothing to do.`);
|
|
69
123
|
return 1;
|
|
70
124
|
}
|
|
71
|
-
const
|
|
72
|
-
const specPath =
|
|
125
|
+
const specs = await findSpecs(cwd);
|
|
126
|
+
const specPath = specs.length > 0 ? `./${specs[0]}` : "./openapi.yaml";
|
|
73
127
|
await writeFile(
|
|
74
128
|
configPath,
|
|
75
129
|
`import { defineConfig } from "webmcp-codegen";
|
|
@@ -88,15 +142,15 @@ export default defineConfig({
|
|
|
88
142
|
});
|
|
89
143
|
`
|
|
90
144
|
);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
145
|
+
console.log("Installed the package? A config file needs it:");
|
|
146
|
+
console.log(" npm install -D webmcp-codegen\n");
|
|
147
|
+
if (specs.length > 0) {
|
|
148
|
+
console.log(`Found ${specs[0]} \u2014 wrote ${CONFIG_FILE}.`);
|
|
149
|
+
console.log("\nNext: npx webmcp-codegen generate --dry-run");
|
|
95
150
|
} else {
|
|
96
151
|
console.log(`No OpenAPI spec found, so ${CONFIG_FILE} points at ./openapi.yaml.`);
|
|
97
152
|
console.log("Edit the `spec` path to point at your spec, then run:");
|
|
98
|
-
console.log(
|
|
99
|
-
npx webmcp-codegen generate --dry-run`);
|
|
153
|
+
console.log("\n npx webmcp-codegen generate --dry-run");
|
|
100
154
|
}
|
|
101
155
|
return 0;
|
|
102
156
|
}
|
|
@@ -109,15 +163,54 @@ async function generate(flags) {
|
|
|
109
163
|
const result = await runOnce(cwd, flags);
|
|
110
164
|
return result.blocked ? 1 : 0;
|
|
111
165
|
}
|
|
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 \u2014 ${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
|
+
}
|
|
112
205
|
async function runOnce(cwd, flags) {
|
|
113
|
-
const { config,
|
|
206
|
+
const { config, label } = await resolveConfig(cwd, flags);
|
|
114
207
|
const result = await runGenerate(config, {
|
|
115
208
|
cwd,
|
|
116
209
|
dryRun: flags.dryRun,
|
|
117
210
|
skipAudit: flags.skipAudit,
|
|
118
211
|
force: flags.force
|
|
119
212
|
});
|
|
120
|
-
printReport(result, flags,
|
|
213
|
+
printReport(result, flags, label, cwd);
|
|
121
214
|
return result;
|
|
122
215
|
}
|
|
123
216
|
function printReport(result, flags, configName, cwd) {
|
|
@@ -140,8 +233,8 @@ webmcp-codegen (${configName}) \u2014 ${tools.length} tool(s)
|
|
|
140
233
|
console.log("");
|
|
141
234
|
for (const file of files) {
|
|
142
235
|
if (file.action === "unchanged" && !file.conflict) continue;
|
|
143
|
-
const shown = file.conflict ? `conflict \u2192 wrote ${
|
|
144
|
-
console.log(` ${shown}: ${
|
|
236
|
+
const shown = file.conflict ? `conflict \u2192 wrote ${relative2(cwd, file.conflict)}` : file.action;
|
|
237
|
+
console.log(` ${shown}: ${relative2(cwd, file.path)}`);
|
|
145
238
|
}
|
|
146
239
|
}
|
|
147
240
|
if (blocked) {
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * The webmcp-codegen CLI.\n *\n * Deliberately three commands, learnable in one sitting:\n *\n * webmcp-codegen init detect your API spec, write codegen.config.mjs\n * webmcp-codegen generate run the pipeline, write the files\n * webmcp-codegen generate --watch re-run when source files change\n *\n * Plus the flags you'd expect on a codegen tool: --dry-run to preview,\n * --skip-audit to bypass the safety report, --force to write through\n * audit errors, --config to point at a config file somewhere else.\n */\n\nimport { existsSync, watch } from \"node:fs\";\nimport { readdir, writeFile } from \"node:fs/promises\";\nimport { basename, join, relative } from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport { loadConfig } from \"./config.js\";\nimport { type GenerateResult, runGenerate } from \"./pipeline.js\";\n\nconst HELP = `webmcp-codegen — generate WebMCP tools from the API contracts you already have\n\nUsage:\n webmcp-codegen init Detect your spec and write codegen.config.mjs\n webmcp-codegen generate Generate (or update) your WebMCP tools\n webmcp-codegen generate --watch Re-generate when files change\n\nFlags for generate:\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\n/** Spec filenames we recognize during `init`, most common first. */\nconst SPEC_FILE_PATTERN = /^(openapi|swagger|api)\\.(ya?ml|json)$/i;\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 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 });\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 specFile = (await readdir(cwd)).find((name) => SPEC_FILE_PATTERN.test(name));\n const specPath = specFile ? `./${specFile}` : \"./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 if (specFile) {\n console.log(`Found ${specFile} — 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}\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/** One generate pass: load config, run the pipeline, print the report. */\nasync function runOnce(cwd: string, flags: GenerateFlags): Promise<GenerateResult> {\n const { config, path } = await loadConfig(cwd, flags.configPath);\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, basename(path), 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.riskTier}] ← ${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"],"mappings":"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","names":[]}
|
|
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) — full control\n * 2. --spec/--out flags — quick overrides, no config needed\n * 3. auto-detection — 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.riskTier}] ← ${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"]}
|
package/dist/generators/index.js
CHANGED
|
@@ -1,249 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
// src/generators/js.ts
|
|
7
|
-
import { readFile } from "fs/promises";
|
|
8
|
-
import { join } from "path";
|
|
9
|
-
|
|
10
|
-
// src/generators/js-templates.ts
|
|
11
|
-
function generatedRegion(tool) {
|
|
12
|
-
const pascal = pascalCase(tool.name);
|
|
13
|
-
const camel = lowercaseFirst(pascal);
|
|
14
|
-
const schemaJson = JSON.stringify(tool.inputSchema, null, 2);
|
|
15
|
-
const inputType = jsonSchemaToTs(tool.inputSchema, void 0);
|
|
16
|
-
return [
|
|
17
|
-
`import { getModelContext } from "./runtime.webmcp";`,
|
|
18
|
-
``,
|
|
19
|
-
GENERATED_START,
|
|
20
|
-
`/**`,
|
|
21
|
-
` * ${tool.description}`,
|
|
22
|
-
` *`,
|
|
23
|
-
` * Source: ${tool.source.ref} (${tool.source.kind}) \xB7 risk: ${tool.riskTier}`,
|
|
24
|
-
` * Regenerate with: npx webmcp-codegen generate`,
|
|
25
|
-
` */`,
|
|
26
|
-
``,
|
|
27
|
-
`/** The exact contract advertised to the agent. Derived from the API spec \u2014 do not hand-edit. */`,
|
|
28
|
-
`export const ${camel}InputSchema = ${schemaJson};`,
|
|
29
|
-
``,
|
|
30
|
-
`/** What \`execute\` receives. The browser validates agent input against the schema above. */`,
|
|
31
|
-
`export type ${tool.inputTypeName} = ${inputType};`,
|
|
32
|
-
``,
|
|
33
|
-
`/** Safety hints computed by webmcp-codegen. Informational metadata for hosts and UIs. */`,
|
|
34
|
-
`export const ${camel}Hints = ${JSON.stringify(tool.hints)} as const;`,
|
|
35
|
-
``,
|
|
36
|
-
`/** The tool definition, minus \`execute\` (which is yours, below the marker). */`,
|
|
37
|
-
`export const ${camel}Tool = {`,
|
|
38
|
-
` name: ${JSON.stringify(tool.name)},`,
|
|
39
|
-
` description: ${JSON.stringify(tool.description)},`,
|
|
40
|
-
` inputSchema: ${camel}InputSchema,`,
|
|
41
|
-
`};`,
|
|
42
|
-
``,
|
|
43
|
-
`/**`,
|
|
44
|
-
` * Register this tool with WebMCP. Call it once on page load, or use`,
|
|
45
|
-
` * registerAllTools() from the generated index.ts.`,
|
|
46
|
-
` *`,
|
|
47
|
-
` * Pass an AbortSignal to unregister later: controller.abort().`,
|
|
48
|
-
` */`,
|
|
49
|
-
`export async function register${pascal}(signal?: AbortSignal): Promise<void> {`,
|
|
50
|
-
` const modelContext = getModelContext();`,
|
|
51
|
-
` await modelContext.registerTool(`,
|
|
52
|
-
` {`,
|
|
53
|
-
` ...${camel}Tool,`,
|
|
54
|
-
` // The browser has already validated the agent's input against the schema.`,
|
|
55
|
-
` execute: (input) => execute${pascal}(input as ${tool.inputTypeName}),`,
|
|
56
|
-
` },`,
|
|
57
|
-
` { signal },`,
|
|
58
|
-
` );`,
|
|
59
|
-
`}`,
|
|
60
|
-
``,
|
|
61
|
-
GENERATED_END
|
|
62
|
-
].join("\n");
|
|
63
|
-
}
|
|
64
|
-
function ownedRegionScaffold(tool) {
|
|
65
|
-
const pascal = pascalCase(tool.name);
|
|
66
|
-
const lines = [
|
|
67
|
-
``,
|
|
68
|
-
`/**`,
|
|
69
|
-
` * What actually happens when the agent calls "${tool.name}".`,
|
|
70
|
-
` *`,
|
|
71
|
-
` * Source: ${tool.source.ref} \u2014 call your existing client code here.`,
|
|
72
|
-
` * Return { content: [{ type: "text", text: ... }] } (the MCP result shape).`
|
|
73
|
-
];
|
|
74
|
-
if (tool.riskTier !== "safe-read") {
|
|
75
|
-
lines.push(
|
|
76
|
-
` *`,
|
|
77
|
-
` * \u26A0 This tool is ${tool.riskTier}: it ${tool.riskTier === "destructive-confirm" ? "cannot easily be undone" : "changes things"}.`,
|
|
78
|
-
` * Ask the user before acting \u2014 see requestUserConfirmation() in runtime.webmcp.ts.`
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
lines.push(` */`);
|
|
82
|
-
if (tool.piiInOutput.length > 0) {
|
|
83
|
-
lines.push(
|
|
84
|
-
`//`,
|
|
85
|
-
`// \u26A0 webmcp-codegen flagged these response fields as likely PII: ${tool.piiInOutput.join(", ")}.`,
|
|
86
|
-
`// Everything you return reaches the agent. Leave those fields out unless`,
|
|
87
|
-
`// the agent genuinely needs them, and say so in a comment if you keep them.`
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
lines.push(
|
|
91
|
-
`export async function execute${pascal}(input: ${tool.inputTypeName}) {`,
|
|
92
|
-
...usageExample(tool),
|
|
93
|
-
` throw new Error("Not implemented: execute${pascal}");`,
|
|
94
|
-
`}`
|
|
95
|
-
);
|
|
96
|
-
return lines.join("\n");
|
|
97
|
-
}
|
|
98
|
-
function usageExample(tool) {
|
|
99
|
-
if (!tool.httpMethod) {
|
|
100
|
-
return [` // TODO: implement using your app's existing code.`];
|
|
101
|
-
}
|
|
102
|
-
const path = tool.source.ref.replace(/^[A-Z]+ /, "");
|
|
103
|
-
const exampleUrl = path.replace(/\{(\w+)\}/g, (_match, param) => `" + input.${param} + "`).replace(/^"" \+ /, "").replace(/ \+ ""$/, "");
|
|
104
|
-
const fetchArgs = tool.httpMethod === "GET" ? `"${exampleUrl}"` : `"${exampleUrl}", { method: "${tool.httpMethod}" }`;
|
|
105
|
-
return [
|
|
106
|
-
` // TODO: implement using your app's existing code, e.g.:`,
|
|
107
|
-
` // const response = await fetch(${fetchArgs});`,
|
|
108
|
-
` // if (!response.ok) throw new Error("Request failed: " + response.status);`,
|
|
109
|
-
` // return { content: [{ type: "text", text: "Done" }] };`
|
|
110
|
-
];
|
|
111
|
-
}
|
|
112
|
-
function runtimeSource() {
|
|
113
|
-
return `/**
|
|
114
|
-
* Generated by webmcp-codegen \u2014 this file is fully regenerated on every run.
|
|
115
|
-
* Do not edit by hand; your changes will be lost.
|
|
116
|
-
*/
|
|
117
|
-
|
|
118
|
-
/** The result shape tools return (same as MCP tool results). */
|
|
119
|
-
export interface WebMcpToolResult {
|
|
120
|
-
content: { type: "text"; text: string }[];
|
|
121
|
-
[key: string]: unknown;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** A tool as the browser runtime understands it. */
|
|
125
|
-
export interface WebMcpToolDefinition {
|
|
126
|
-
name: string;
|
|
127
|
-
description: string;
|
|
128
|
-
inputSchema?: Record<string, unknown>;
|
|
129
|
-
execute: (input: Record<string, unknown>) => unknown | Promise<unknown>;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** The slice of the WebMCP draft spec the generated code uses. */
|
|
133
|
-
export interface ModelContext {
|
|
134
|
-
registerTool(
|
|
135
|
-
tool: WebMcpToolDefinition,
|
|
136
|
-
options?: { signal?: AbortSignal },
|
|
137
|
-
): Promise<void>;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Access the page's WebMCP model context, with a helpful error when the
|
|
142
|
-
* browser doesn't have one (rather than an undefined-callsite mystery).
|
|
143
|
-
*/
|
|
144
|
-
export function getModelContext(): ModelContext {
|
|
145
|
-
const modelContext = (document as unknown as { modelContext?: ModelContext }).modelContext;
|
|
146
|
-
if (!modelContext) {
|
|
147
|
-
throw new Error(
|
|
148
|
-
"WebMCP is not available in this browser. " +
|
|
149
|
-
"Enable chrome://flags/#enable-webmcp-testing (Chrome 146+), " +
|
|
150
|
-
"or add the WebMCP polyfill to your app.",
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
return modelContext;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Default "agent proposes, human confirms" gate for write/destructive tools.
|
|
158
|
-
* Deliberately minimal (window.confirm) \u2014 replace it with your app's own
|
|
159
|
-
* dialog when you outgrow it. The point is that the user always gets a say.
|
|
160
|
-
*/
|
|
161
|
-
export function requestUserConfirmation(message: string): Promise<boolean> {
|
|
162
|
-
return Promise.resolve(window.confirm(message));
|
|
163
|
-
}
|
|
164
|
-
`;
|
|
165
|
-
}
|
|
166
|
-
function barrelSource(tools) {
|
|
167
|
-
const imports = tools.map((tool) => `import { register${pascalCase(tool.name)} } from "./${tool.name}.webmcp";`).join("\n");
|
|
168
|
-
const names = tools.map((tool) => `register${pascalCase(tool.name)}`).join(",\n ");
|
|
169
|
-
return `/**
|
|
170
|
-
* Generated by webmcp-codegen \u2014 this file is fully regenerated on every run.
|
|
171
|
-
* Import registerAllTools() once at app startup:
|
|
172
|
-
*
|
|
173
|
-
* import { registerAllTools } from "./webmcp";
|
|
174
|
-
* await registerAllTools();
|
|
175
|
-
*/
|
|
176
|
-
|
|
177
|
-
${imports}
|
|
178
|
-
|
|
179
|
-
const registrations = [
|
|
180
|
-
${names}
|
|
181
|
-
];
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Register every generated tool with WebMCP. One tool failing (for example
|
|
185
|
-
* because the page's Permissions-Policy disables tools) never takes the
|
|
186
|
-
* others down with it \u2014 the failure is logged and registration continues.
|
|
187
|
-
*/
|
|
188
|
-
export async function registerAllTools(signal?: AbortSignal): Promise<void> {
|
|
189
|
-
for (const register of registrations) {
|
|
190
|
-
try {
|
|
191
|
-
await register(signal);
|
|
192
|
-
} catch (error) {
|
|
193
|
-
console.warn("[webmcp-codegen] a tool failed to register:", error);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
`;
|
|
198
|
-
}
|
|
199
|
-
function lowercaseFirst(pascal) {
|
|
200
|
-
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// src/generators/js.ts
|
|
204
|
-
var GENERATED_START = "// \u2500\u2500\u2500 webmcp-codegen: generated \u2014 do not edit this region \u2500\u2500\u2500";
|
|
205
|
-
var GENERATED_END = "// \u2500\u2500\u2500 webmcp-codegen: end generated \u2014 your code below survives regeneration \u2500\u2500\u2500";
|
|
206
|
-
function js(options) {
|
|
207
|
-
return {
|
|
208
|
-
kind: "js",
|
|
209
|
-
async generate(tools, cwd) {
|
|
210
|
-
const outDir = join(cwd, options.outDir);
|
|
211
|
-
const files = [];
|
|
212
|
-
files.push(await plainFile(join(outDir, "runtime.webmcp.ts"), runtimeSource()));
|
|
213
|
-
files.push(await plainFile(join(outDir, "index.ts"), barrelSource(tools)));
|
|
214
|
-
for (const tool of tools) {
|
|
215
|
-
files.push(await toolFile(tool, outDir));
|
|
216
|
-
}
|
|
217
|
-
return files;
|
|
218
|
-
}
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
async function plainFile(path, contents) {
|
|
222
|
-
try {
|
|
223
|
-
const existing = await readFile(path, "utf8");
|
|
224
|
-
return { path, contents, action: existing === contents ? "unchanged" : "update" };
|
|
225
|
-
} catch {
|
|
226
|
-
return { path, contents, action: "create" };
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
async function toolFile(tool, outDir) {
|
|
230
|
-
const path = join(outDir, `${tool.name}.webmcp.ts`);
|
|
231
|
-
const head = generatedRegion(tool);
|
|
232
|
-
let existing;
|
|
233
|
-
try {
|
|
234
|
-
existing = await readFile(path, "utf8");
|
|
235
|
-
} catch {
|
|
236
|
-
return { path, contents: `${head}
|
|
237
|
-
${ownedRegionScaffold(tool)}`, action: "create" };
|
|
238
|
-
}
|
|
239
|
-
const markerIndex = existing.indexOf(GENERATED_END);
|
|
240
|
-
if (markerIndex === -1) {
|
|
241
|
-
return { path, contents: existing, action: "unchanged", conflict: `${path}.new` };
|
|
242
|
-
}
|
|
243
|
-
const preservedTail = existing.slice(markerIndex + GENERATED_END.length);
|
|
244
|
-
const contents = head + preservedTail;
|
|
245
|
-
return { path, contents, action: contents === existing ? "unchanged" : "update" };
|
|
246
|
-
}
|
|
2
|
+
js
|
|
3
|
+
} from "../chunk-GDJDVR4E.js";
|
|
4
|
+
import "../chunk-5L4KN6F4.js";
|
|
247
5
|
export {
|
|
248
6
|
js
|
|
249
7
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/generators/js.ts","../../src/generators/js-templates.ts"],"sourcesContent":["/**\n * The `js` generator — named after what lands in your repo: plain JavaScript/\n * TypeScript files that call the spec's imperative API\n * (`document.modelContext.registerTool`).\n *\n * Output layout for `js({ outDir: \"./src/webmcp\" })`:\n *\n * src/webmcp/\n * ├── runtime.webmcp.ts ← fully generated, never edit\n * ├── index.ts ← fully generated, registers everything\n * ├── get-order-status.webmcp.ts ← generated contract + YOUR execute()\n * └── ...\n *\n * Each per-tool file has two regions, divided by marker comments:\n *\n * generated region schema, input type, tool definition, register()\n * ── end generated ── everything below survives regeneration\n * your region execute(), scaffolded once, then owned by you\n *\n * This file contains only the *file mechanics*: which files exist, and how to\n * update them without destroying hand-written code. The text of the generated\n * code itself lives in js-templates.ts — keeping \"what the output looks like\"\n * separate from \"how files get written\" is what keeps both readable.\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { GeneratedFile, ReviewedTool, ToolGenerator } from \"../types.js\";\nimport {\n barrelSource,\n generatedRegion,\n ownedRegionScaffold,\n runtimeSource,\n} from \"./js-templates.js\";\n\nexport interface JsGeneratorOptions {\n /** Where the tool files go, relative to the project root. */\n outDir: string;\n}\n\n/**\n * The marker lines that split a per-tool file in two. They are the merge\n * contract: we may rewrite everything up to and including GENERATED_END,\n * and we must never touch anything after it. js-templates.ts imports these\n * so the marker text is defined in exactly one place.\n */\nexport const GENERATED_START = \"// ─── webmcp-codegen: generated — do not edit this region ───\";\nexport const GENERATED_END =\n \"// ─── webmcp-codegen: end generated — your code below survives regeneration ───\";\n\n/** Create the `js` generator for the config's `generate` array. */\nexport function js(options: JsGeneratorOptions): ToolGenerator {\n return {\n kind: \"js\",\n async generate(tools, cwd) {\n const outDir = join(cwd, options.outDir);\n const files: GeneratedFile[] = [];\n\n // The runtime and the barrel are regenerated wholesale every run —\n // their headers say \"do not edit\", and we mean it.\n files.push(await plainFile(join(outDir, \"runtime.webmcp.ts\"), runtimeSource()));\n files.push(await plainFile(join(outDir, \"index.ts\"), barrelSource(tools)));\n\n for (const tool of tools) {\n files.push(await toolFile(tool, outDir));\n }\n return files;\n },\n };\n}\n\n/** A fully-generated file: create if missing, overwrite if changed, skip if same. */\nasync function plainFile(path: string, contents: string): Promise<GeneratedFile> {\n try {\n const existing = await readFile(path, \"utf8\");\n return { path, contents, action: existing === contents ? \"unchanged\" : \"update\" };\n } catch {\n return { path, contents, action: \"create\" };\n }\n}\n\n/**\n * Build (or merge) one per-tool file. The only I/O here is reading the\n * existing file to check for a hand-written region worth keeping.\n */\nasync function toolFile(tool: ReviewedTool, outDir: string): Promise<GeneratedFile> {\n const path = join(outDir, `${tool.name}.webmcp.ts`);\n const head = generatedRegion(tool);\n\n let existing: string | undefined;\n try {\n existing = await readFile(path, \"utf8\");\n } catch {\n // No file yet — brand new tool, so we also lay down the execute() scaffold.\n return { path, contents: `${head}\\n${ownedRegionScaffold(tool)}`, action: \"create\" };\n }\n\n const markerIndex = existing.indexOf(GENERATED_END);\n if (markerIndex === -1) {\n // Someone removed the markers or hand-wrote this path from scratch.\n // Never clobber their work: report a conflict and let the pipeline put\n // our version in a `.new` sibling for a human to merge.\n return { path, contents: existing, action: \"unchanged\", conflict: `${path}.new` };\n }\n\n // Keep everything the developer wrote below the marker, word for word.\n const preservedTail = existing.slice(markerIndex + GENERATED_END.length);\n const contents = head + preservedTail;\n return { path, contents, action: contents === existing ? \"unchanged\" : \"update\" };\n}\n","/**\n * The text of the code the `js` generator writes.\n *\n * Heads up before reading on: every function here returns *TypeScript source\n * code as a string*. When you see `export const ...` inside quotes, that's\n * the output a user's repo will contain — not this module's own logic.\n * Building output from arrays of lines (rather than nested template strings)\n * keeps the quoting readable; the only escaping left is for code samples\n * inside the generated comments.\n *\n * Three kinds of output are built here:\n * - generatedRegion() the per-tool contract (regenerated freely)\n * - ownedRegionScaffold() the execute() stub (written once, then owned)\n * - runtimeSource() / barrelSource() fully-generated support files\n */\n\nimport { jsonSchemaToTs, pascalCase } from \"../schema.js\";\nimport type { ReviewedTool } from \"../types.js\";\nimport { GENERATED_END, GENERATED_START } from \"./js.js\";\n\n/**\n * Everything above the end-marker of a per-tool file: the parts that must\n * track the API contract exactly — name, description, schema, input type,\n * hints, and the register() wrapper.\n */\nexport function generatedRegion(tool: ReviewedTool): string {\n const pascal = pascalCase(tool.name);\n const camel = lowercaseFirst(pascal);\n const schemaJson = JSON.stringify(tool.inputSchema, null, 2);\n const inputType = jsonSchemaToTs(tool.inputSchema, undefined);\n\n return [\n `import { getModelContext } from \"./runtime.webmcp\";`,\n ``,\n GENERATED_START,\n `/**`,\n ` * ${tool.description}`,\n ` *`,\n ` * Source: ${tool.source.ref} (${tool.source.kind}) · risk: ${tool.riskTier}`,\n ` * Regenerate with: npx webmcp-codegen generate`,\n ` */`,\n ``,\n `/** The exact contract advertised to the agent. Derived from the API spec — do not hand-edit. */`,\n `export const ${camel}InputSchema = ${schemaJson};`,\n ``,\n `/** What \\`execute\\` receives. The browser validates agent input against the schema above. */`,\n `export type ${tool.inputTypeName} = ${inputType};`,\n ``,\n `/** Safety hints computed by webmcp-codegen. Informational metadata for hosts and UIs. */`,\n `export const ${camel}Hints = ${JSON.stringify(tool.hints)} as const;`,\n ``,\n `/** The tool definition, minus \\`execute\\` (which is yours, below the marker). */`,\n `export const ${camel}Tool = {`,\n ` name: ${JSON.stringify(tool.name)},`,\n ` description: ${JSON.stringify(tool.description)},`,\n ` inputSchema: ${camel}InputSchema,`,\n `};`,\n ``,\n `/**`,\n ` * Register this tool with WebMCP. Call it once on page load, or use`,\n ` * registerAllTools() from the generated index.ts.`,\n ` *`,\n ` * Pass an AbortSignal to unregister later: controller.abort().`,\n ` */`,\n `export async function register${pascal}(signal?: AbortSignal): Promise<void> {`,\n ` const modelContext = getModelContext();`,\n ` await modelContext.registerTool(`,\n ` {`,\n ` ...${camel}Tool,`,\n ` // The browser has already validated the agent's input against the schema.`,\n ` execute: (input) => execute${pascal}(input as ${tool.inputTypeName}),`,\n ` },`,\n ` { signal },`,\n ` );`,\n `}`,\n ``,\n GENERATED_END,\n ].join(\"\\n\");\n}\n\n/**\n * The scaffold below the marker, written exactly once (when the file is\n * first created). After that the developer owns it and regeneration never\n * touches it — that promise is the whole reason the marker split exists.\n */\nexport function ownedRegionScaffold(tool: ReviewedTool): string {\n const pascal = pascalCase(tool.name);\n const lines: string[] = [\n ``,\n `/**`,\n ` * What actually happens when the agent calls \"${tool.name}\".`,\n ` *`,\n ` * Source: ${tool.source.ref} — call your existing client code here.`,\n ` * Return { content: [{ type: \"text\", text: ... }] } (the MCP result shape).`,\n ];\n\n if (tool.riskTier !== \"safe-read\") {\n lines.push(\n ` *`,\n ` * ⚠ This tool is ${tool.riskTier}: it ${\n tool.riskTier === \"destructive-confirm\" ? \"cannot easily be undone\" : \"changes things\"\n }.`,\n ` * Ask the user before acting — see requestUserConfirmation() in runtime.webmcp.ts.`,\n );\n }\n lines.push(` */`);\n\n if (tool.piiInOutput.length > 0) {\n lines.push(\n `//`,\n `// ⚠ webmcp-codegen flagged these response fields as likely PII: ${tool.piiInOutput.join(\", \")}.`,\n `// Everything you return reaches the agent. Leave those fields out unless`,\n `// the agent genuinely needs them, and say so in a comment if you keep them.`,\n );\n }\n\n lines.push(\n `export async function execute${pascal}(input: ${tool.inputTypeName}) {`,\n ...usageExample(tool),\n ` throw new Error(\"Not implemented: execute${pascal}\");`,\n `}`,\n );\n\n return lines.join(\"\\n\");\n}\n\n/**\n * The TODO example inside a fresh scaffold. When the source knows the route\n * (OpenAPI always does), the example shows the actual call — seeing\n * `fetch(\"/pets/\" + input.id, …)` beats an abstract placeholder every time.\n */\nfunction usageExample(tool: ReviewedTool): string[] {\n if (!tool.httpMethod) {\n return [` // TODO: implement using your app's existing code.`];\n }\n const path = tool.source.ref.replace(/^[A-Z]+ /, \"\");\n // Turn \"/pets/{id}\" into '\"/pets/\" + input.id' — a copy-pasteable example.\n const exampleUrl = path\n .replace(/\\{(\\w+)\\}/g, (_match, param: string) => `\" + input.${param} + \"`)\n // Trim the empty-string concat a leading/trailing placeholder leaves behind.\n .replace(/^\"\" \\+ /, \"\")\n .replace(/ \\+ \"\"$/, \"\");\n const fetchArgs =\n tool.httpMethod === \"GET\"\n ? `\"${exampleUrl}\"`\n : `\"${exampleUrl}\", { method: \"${tool.httpMethod}\" }`;\n return [\n ` // TODO: implement using your app's existing code, e.g.:`,\n ` // const response = await fetch(${fetchArgs});`,\n ` // if (!response.ok) throw new Error(\"Request failed: \" + response.status);`,\n ` // return { content: [{ type: \"text\", text: \"Done\" }] };`,\n ];\n}\n\n/**\n * The shared runtime: the minimal WebMCP browser types plus getModelContext().\n * Kept tiny on purpose — this is the only browser coupling in the output.\n */\nexport function runtimeSource(): string {\n return `/**\n * Generated by webmcp-codegen — this file is fully regenerated on every run.\n * Do not edit by hand; your changes will be lost.\n */\n\n/** The result shape tools return (same as MCP tool results). */\nexport interface WebMcpToolResult {\n content: { type: \"text\"; text: string }[];\n [key: string]: unknown;\n}\n\n/** A tool as the browser runtime understands it. */\nexport interface WebMcpToolDefinition {\n name: string;\n description: string;\n inputSchema?: Record<string, unknown>;\n execute: (input: Record<string, unknown>) => unknown | Promise<unknown>;\n}\n\n/** The slice of the WebMCP draft spec the generated code uses. */\nexport interface ModelContext {\n registerTool(\n tool: WebMcpToolDefinition,\n options?: { signal?: AbortSignal },\n ): Promise<void>;\n}\n\n/**\n * Access the page's WebMCP model context, with a helpful error when the\n * browser doesn't have one (rather than an undefined-callsite mystery).\n */\nexport function getModelContext(): ModelContext {\n const modelContext = (document as unknown as { modelContext?: ModelContext }).modelContext;\n if (!modelContext) {\n throw new Error(\n \"WebMCP is not available in this browser. \" +\n \"Enable chrome://flags/#enable-webmcp-testing (Chrome 146+), \" +\n \"or add the WebMCP polyfill to your app.\",\n );\n }\n return modelContext;\n}\n\n/**\n * Default \"agent proposes, human confirms\" gate for write/destructive tools.\n * Deliberately minimal (window.confirm) — replace it with your app's own\n * dialog when you outgrow it. The point is that the user always gets a say.\n */\nexport function requestUserConfirmation(message: string): Promise<boolean> {\n return Promise.resolve(window.confirm(message));\n}\n`;\n}\n\n/** The barrel: one import that registers every generated tool. */\nexport function barrelSource(tools: ReviewedTool[]): string {\n const imports = tools\n .map((tool) => `import { register${pascalCase(tool.name)} } from \"./${tool.name}.webmcp\";`)\n .join(\"\\n\");\n const names = tools.map((tool) => `register${pascalCase(tool.name)}`).join(\",\\n \");\n\n return `/**\n * Generated by webmcp-codegen — this file is fully regenerated on every run.\n * Import registerAllTools() once at app startup:\n *\n * import { registerAllTools } from \"./webmcp\";\n * await registerAllTools();\n */\n\n${imports}\n\nconst registrations = [\n ${names}\n];\n\n/**\n * Register every generated tool with WebMCP. One tool failing (for example\n * because the page's Permissions-Policy disables tools) never takes the\n * others down with it — the failure is logged and registration continues.\n */\nexport async function registerAllTools(signal?: AbortSignal): Promise<void> {\n for (const register of registrations) {\n try {\n await register(signal);\n } catch (error) {\n console.warn(\"[webmcp-codegen] a tool failed to register:\", error);\n }\n }\n}\n`;\n}\n\n/** \"GetOrderStatus\" → \"getOrderStatus\" (for the generated const names). */\nfunction lowercaseFirst(pascal: string): string {\n return pascal.charAt(0).toLowerCase() + pascal.slice(1);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|