webmcp-codegen 0.0.1 → 0.1.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 +70 -10
- package/dist/chunk-5L4KN6F4.js +109 -0
- package/dist/chunk-5L4KN6F4.js.map +1 -0
- package/dist/chunk-BIKKPCRT.js +39 -0
- package/dist/chunk-BIKKPCRT.js.map +1 -0
- package/dist/chunk-NFZ5FMDO.js +237 -0
- package/dist/chunk-NFZ5FMDO.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +177 -0
- package/dist/cli.js.map +1 -0
- package/dist/generators/index.d.ts +35 -0
- package/dist/generators/index.js +250 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/sources/index.d.ts +28 -0
- package/dist/sources/index.js +139 -0
- package/dist/sources/index.js.map +1 -0
- package/dist/types-Bf5MxWeH.d.ts +136 -0
- package/package.json +36 -8
- package/src/index.js +0 -4
package/dist/cli.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
loadConfig,
|
|
4
|
+
runGenerate
|
|
5
|
+
} from "./chunk-NFZ5FMDO.js";
|
|
6
|
+
import "./chunk-BIKKPCRT.js";
|
|
7
|
+
import "./chunk-5L4KN6F4.js";
|
|
8
|
+
|
|
9
|
+
// src/cli.ts
|
|
10
|
+
import { existsSync, watch } from "fs";
|
|
11
|
+
import { readdir, writeFile } from "fs/promises";
|
|
12
|
+
import { basename, join, relative } from "path";
|
|
13
|
+
import { parseArgs } from "util";
|
|
14
|
+
var HELP = `webmcp-codegen \u2014 generate WebMCP tools from the API contracts you already have
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
webmcp-codegen init Detect your spec and write codegen.config.mjs
|
|
18
|
+
webmcp-codegen generate Generate (or update) your WebMCP tools
|
|
19
|
+
webmcp-codegen generate --watch Re-generate when files change
|
|
20
|
+
|
|
21
|
+
Flags for generate:
|
|
22
|
+
--dry-run Preview what would be written, write nothing
|
|
23
|
+
--skip-audit Skip the safety report
|
|
24
|
+
--force Write files even when the audit reports errors
|
|
25
|
+
--config PATH Use a config file at PATH
|
|
26
|
+
`;
|
|
27
|
+
var CONFIG_FILE = "codegen.config.mjs";
|
|
28
|
+
var SPEC_FILE_PATTERN = /^(openapi|swagger|api)\.(ya?ml|json)$/i;
|
|
29
|
+
async function main() {
|
|
30
|
+
const { positionals, values } = parseArgs({
|
|
31
|
+
allowPositionals: true,
|
|
32
|
+
options: {
|
|
33
|
+
"dry-run": { type: "boolean", default: false },
|
|
34
|
+
"skip-audit": { type: "boolean", default: false },
|
|
35
|
+
force: { type: "boolean", default: false },
|
|
36
|
+
watch: { type: "boolean", default: false },
|
|
37
|
+
config: { type: "string" },
|
|
38
|
+
help: { type: "boolean", default: false }
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
const command = positionals[0];
|
|
42
|
+
if (values.help || !command) {
|
|
43
|
+
console.log(HELP);
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
switch (command) {
|
|
47
|
+
case "init":
|
|
48
|
+
return init();
|
|
49
|
+
case "generate":
|
|
50
|
+
return generate({
|
|
51
|
+
dryRun: values["dry-run"],
|
|
52
|
+
skipAudit: values["skip-audit"],
|
|
53
|
+
force: values.force,
|
|
54
|
+
watch: values.watch,
|
|
55
|
+
configPath: values.config
|
|
56
|
+
});
|
|
57
|
+
default:
|
|
58
|
+
console.error(`Unknown command "${command}".
|
|
59
|
+
`);
|
|
60
|
+
console.log(HELP);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function init() {
|
|
65
|
+
const cwd = process.cwd();
|
|
66
|
+
const configPath = join(cwd, CONFIG_FILE);
|
|
67
|
+
if (existsSync(configPath)) {
|
|
68
|
+
console.error(`${CONFIG_FILE} already exists \u2014 nothing to do.`);
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
const specFile = (await readdir(cwd)).find((name) => SPEC_FILE_PATTERN.test(name));
|
|
72
|
+
const specPath = specFile ? `./${specFile}` : "./openapi.yaml";
|
|
73
|
+
await writeFile(
|
|
74
|
+
configPath,
|
|
75
|
+
`import { defineConfig } from "webmcp-codegen";
|
|
76
|
+
import { openapi } from "webmcp-codegen/sources";
|
|
77
|
+
import { js } from "webmcp-codegen/generators";
|
|
78
|
+
|
|
79
|
+
export default defineConfig({
|
|
80
|
+
sources: [openapi({ spec: "${specPath}" })],
|
|
81
|
+
generate: [js({ outDir: "./src/webmcp" })],
|
|
82
|
+
safety: {
|
|
83
|
+
// Extra field names to treat as PII, on top of the built-in list:
|
|
84
|
+
// piiFields: ["internalId"],
|
|
85
|
+
// Tools to skip entirely (matched against name and route):
|
|
86
|
+
// exclude: ["internal"],
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
`
|
|
90
|
+
);
|
|
91
|
+
if (specFile) {
|
|
92
|
+
console.log(`Found ${specFile} \u2014 wrote ${CONFIG_FILE}.`);
|
|
93
|
+
console.log(`
|
|
94
|
+
Next: npx webmcp-codegen generate --dry-run`);
|
|
95
|
+
} else {
|
|
96
|
+
console.log(`No OpenAPI spec found, so ${CONFIG_FILE} points at ./openapi.yaml.`);
|
|
97
|
+
console.log("Edit the `spec` path to point at your spec, then run:");
|
|
98
|
+
console.log(`
|
|
99
|
+
npx webmcp-codegen generate --dry-run`);
|
|
100
|
+
}
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
async function generate(flags) {
|
|
104
|
+
const cwd = process.cwd();
|
|
105
|
+
if (flags.watch) {
|
|
106
|
+
await watchLoop(cwd, flags);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
const result = await runOnce(cwd, flags);
|
|
110
|
+
return result.blocked ? 1 : 0;
|
|
111
|
+
}
|
|
112
|
+
async function runOnce(cwd, flags) {
|
|
113
|
+
const { config, path } = await loadConfig(cwd, flags.configPath);
|
|
114
|
+
const result = await runGenerate(config, {
|
|
115
|
+
cwd,
|
|
116
|
+
dryRun: flags.dryRun,
|
|
117
|
+
skipAudit: flags.skipAudit,
|
|
118
|
+
force: flags.force
|
|
119
|
+
});
|
|
120
|
+
printReport(result, flags, basename(path), cwd);
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function printReport(result, flags, configName, cwd) {
|
|
124
|
+
const { tools, findings, files, blocked } = result;
|
|
125
|
+
console.log(`
|
|
126
|
+
webmcp-codegen (${configName}) \u2014 ${tools.length} tool(s)
|
|
127
|
+
`);
|
|
128
|
+
for (const tool of tools) {
|
|
129
|
+
console.log(` ${tool.name} [${tool.riskTier}] \u2190 ${tool.source.ref}`);
|
|
130
|
+
}
|
|
131
|
+
if (findings.length > 0) {
|
|
132
|
+
console.log("");
|
|
133
|
+
for (const finding of findings) {
|
|
134
|
+
const icon = finding.level === "error" ? "\u2716" : "\u26A0";
|
|
135
|
+
const where = finding.tool ? ` (${finding.tool})` : "";
|
|
136
|
+
console.log(` ${icon} ${finding.message}${where}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (files.length > 0) {
|
|
140
|
+
console.log("");
|
|
141
|
+
for (const file of files) {
|
|
142
|
+
if (file.action === "unchanged" && !file.conflict) continue;
|
|
143
|
+
const shown = file.conflict ? `conflict \u2192 wrote ${relative(cwd, file.conflict)}` : file.action;
|
|
144
|
+
console.log(` ${shown}: ${relative(cwd, file.path)}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (blocked) {
|
|
148
|
+
console.log(
|
|
149
|
+
"\nGeneration blocked by audit errors. Fix them, or re-run with --force to write anyway."
|
|
150
|
+
);
|
|
151
|
+
} else if (flags.dryRun) {
|
|
152
|
+
console.log("\nDry run \u2014 nothing written. Re-run without --dry-run to write these files.");
|
|
153
|
+
} else {
|
|
154
|
+
console.log("\nDone. Fill in each execute() below the marker, then registerAllTools().");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function watchLoop(cwd, flags) {
|
|
158
|
+
await runOnce(cwd, { ...flags, dryRun: false });
|
|
159
|
+
console.log("\nWatching for changes\u2026 (Ctrl+C to stop)");
|
|
160
|
+
let timer;
|
|
161
|
+
watch(cwd, { recursive: true }, (_event, filename) => {
|
|
162
|
+
if (!filename) return;
|
|
163
|
+
if (/node_modules|\.git|\/dist|\/src\/webmcp/.test(filename)) return;
|
|
164
|
+
if (!/\.(ya?ml|json|ts|tsx|mts|mjs)$/.test(filename)) return;
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
timer = setTimeout(() => {
|
|
167
|
+
runOnce(cwd, { ...flags, dryRun: false }).catch((error) => {
|
|
168
|
+
console.error(error instanceof Error ? error.message : error);
|
|
169
|
+
});
|
|
170
|
+
}, 300);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
main().then((code) => process.exit(code)).catch((error) => {
|
|
174
|
+
console.error(error instanceof Error ? error.message : error);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
});
|
|
177
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +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":[]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { T as ToolGenerator } from '../types-Bf5MxWeH.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The `js` generator — named after what lands in your repo: plain JavaScript/
|
|
5
|
+
* TypeScript files that call the spec's imperative API
|
|
6
|
+
* (`document.modelContext.registerTool`).
|
|
7
|
+
*
|
|
8
|
+
* Output layout for `js({ outDir: "./src/webmcp" })`:
|
|
9
|
+
*
|
|
10
|
+
* src/webmcp/
|
|
11
|
+
* ├── runtime.webmcp.ts ← fully generated, never edit
|
|
12
|
+
* ├── index.ts ← fully generated, registers everything
|
|
13
|
+
* ├── get-order-status.webmcp.ts ← generated contract + YOUR execute()
|
|
14
|
+
* └── ...
|
|
15
|
+
*
|
|
16
|
+
* Each per-tool file has two regions, divided by marker comments:
|
|
17
|
+
*
|
|
18
|
+
* generated region schema, input type, tool definition, register()
|
|
19
|
+
* ── end generated ── everything below survives regeneration
|
|
20
|
+
* your region execute(), scaffolded once, then owned by you
|
|
21
|
+
*
|
|
22
|
+
* This file contains only the *file mechanics*: which files exist, and how to
|
|
23
|
+
* update them without destroying hand-written code. The text of the generated
|
|
24
|
+
* code itself lives in js-templates.ts — keeping "what the output looks like"
|
|
25
|
+
* separate from "how files get written" is what keeps both readable.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
interface JsGeneratorOptions {
|
|
29
|
+
/** Where the tool files go, relative to the project root. */
|
|
30
|
+
outDir: string;
|
|
31
|
+
}
|
|
32
|
+
/** Create the `js` generator for the config's `generate` array. */
|
|
33
|
+
declare function js(options: JsGeneratorOptions): ToolGenerator;
|
|
34
|
+
|
|
35
|
+
export { js };
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import {
|
|
2
|
+
jsonSchemaToTs,
|
|
3
|
+
pascalCase
|
|
4
|
+
} from "../chunk-5L4KN6F4.js";
|
|
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
|
+
}
|
|
247
|
+
export {
|
|
248
|
+
js
|
|
249
|
+
};
|
|
250
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { C as CodegenConfig, R as ReviewedTool, A as AuditFinding, G as GeneratedFile } from './types-Bf5MxWeH.js';
|
|
2
|
+
export { a as CandidateTool, J as JsonSchema, b as RiskTier, c as SafetyOptions, d as SideEffect, S as Source, e as SourceKind, T as ToolGenerator, f as ToolHints } from './types-Bf5MxWeH.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Config: `defineConfig` for authoring, `loadConfig` for the CLI.
|
|
6
|
+
*
|
|
7
|
+
* Config files are plain JavaScript (`codegen.config.mjs`) so the CLI can
|
|
8
|
+
* load them with a plain dynamic import — no TypeScript loader, no build
|
|
9
|
+
* step, no extra dependencies. If you want types while authoring, that is
|
|
10
|
+
* what `defineConfig` is for:
|
|
11
|
+
*
|
|
12
|
+
* import { defineConfig } from "webmcp-codegen";
|
|
13
|
+
* export default defineConfig({ ... });
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Identity function whose only job is type-checking the config object. */
|
|
17
|
+
declare function defineConfig(config: CodegenConfig): CodegenConfig;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The pipeline: sources → normalize → safety review → audit → write.
|
|
21
|
+
*
|
|
22
|
+
* This module is the only place the stages meet. It owns no opinions of its
|
|
23
|
+
* own — naming, safety, and file formats all live in their own modules — it
|
|
24
|
+
* just runs them in order and produces one honest report of what happened
|
|
25
|
+
* (or what *would* happen, when called with `write: false`).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
interface GenerateOptions {
|
|
29
|
+
/** Project root. Everything (config, spec paths, outDir) resolves from here. */
|
|
30
|
+
cwd: string;
|
|
31
|
+
/** Preview mode: compute everything, write nothing. */
|
|
32
|
+
dryRun?: boolean;
|
|
33
|
+
/** Skip the audit pass entirely (classification still runs — output needs it). */
|
|
34
|
+
skipAudit?: boolean;
|
|
35
|
+
/** Write even when the audit found errors. The report still shows them. */
|
|
36
|
+
force?: boolean;
|
|
37
|
+
}
|
|
38
|
+
interface GenerateResult {
|
|
39
|
+
tools: ReviewedTool[];
|
|
40
|
+
findings: AuditFinding[];
|
|
41
|
+
files: GeneratedFile[];
|
|
42
|
+
/** True when audit errors stopped any file from being written. */
|
|
43
|
+
blocked: boolean;
|
|
44
|
+
/** True when this run actually wrote files (false for dry runs and blocks). */
|
|
45
|
+
wrote: boolean;
|
|
46
|
+
}
|
|
47
|
+
declare function runGenerate(config: CodegenConfig, options: GenerateOptions): Promise<GenerateResult>;
|
|
48
|
+
|
|
49
|
+
export { AuditFinding, CodegenConfig, type GenerateOptions, type GenerateResult, GeneratedFile, ReviewedTool, defineConfig, runGenerate };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { S as Source } from '../types-Bf5MxWeH.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The OpenAPI source.
|
|
5
|
+
*
|
|
6
|
+
* Reads an OpenAPI 3.x document (YAML or JSON) and turns every operation
|
|
7
|
+
* into a CandidateTool. This is the highest-reach source: most backend
|
|
8
|
+
* frameworks can already emit an OpenAPI spec, so teams get value without
|
|
9
|
+
* changing any application code.
|
|
10
|
+
*
|
|
11
|
+
* What we read from each operation:
|
|
12
|
+
* - name ← operationId, slugified (falls back to method + path)
|
|
13
|
+
* - description ← summary, else the first line of description, else a template
|
|
14
|
+
* - inputSchema ← path + query parameters merged with the JSON request body
|
|
15
|
+
* - outputSchema ← the first 2xx response's JSON schema, when present
|
|
16
|
+
*
|
|
17
|
+
* Header and cookie parameters are skipped on purpose: agents should not be
|
|
18
|
+
* setting those by hand, and auth headers are the app's job, not the tool's.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
interface OpenApiSourceOptions {
|
|
22
|
+
/** Path to the OpenAPI document, relative to the project root. */
|
|
23
|
+
spec: string;
|
|
24
|
+
}
|
|
25
|
+
/** Create an OpenAPI source for the config's `sources` array. */
|
|
26
|
+
declare function openapi(options: OpenApiSourceOptions): Source;
|
|
27
|
+
|
|
28
|
+
export { openapi };
|