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 CHANGED
@@ -6,11 +6,11 @@ Generate safe, typed, human-reviewed [WebMCP](https://github.com/webmachinelearn
6
6
 
7
7
  ## Quick start
8
8
 
9
- ```bash
10
- npm install -D webmcp-codegen
9
+ Zero install, zero config — the CLI detects your OpenAPI spec:
11
10
 
12
- npx webmcp-codegen init # detects your OpenAPI spec, writes codegen.config.mjs
13
- npx webmcp-codegen generate # generates reviewed tool files into ./src/webmcp
11
+ ```bash
12
+ npx webmcp-codegen generate --dry-run # preview the tools it would generate
13
+ npx webmcp-codegen generate # write them into ./src/webmcp
14
14
  ```
15
15
 
16
16
  Then implement each `execute()` (below the marker — it's yours, regeneration never touches it) and register everything at app startup:
@@ -21,6 +21,20 @@ import { registerAllTools } from "./webmcp";
21
21
  await registerAllTools();
22
22
  ```
23
23
 
24
+ ### Full control
25
+
26
+ When you want to choose the spec, the output directory, or safety options,
27
+ install the package and let `init` write a config file:
28
+
29
+ ```bash
30
+ npm install -D webmcp-codegen
31
+ npx webmcp-codegen init
32
+ ```
33
+
34
+ (The config file imports from `webmcp-codegen`, which is why the install is
35
+ needed in this mode. The generated code never depends on the package — you
36
+ own it.)
37
+
24
38
  ## What you get
25
39
 
26
40
  For every operation in your spec, one file like `get-order-status.webmcp.ts`:
@@ -0,0 +1,251 @@
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
+
248
+ export {
249
+ js
250
+ };
251
+ //# sourceMappingURL=chunk-GDJDVR4E.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":[]}
@@ -231,7 +231,8 @@ function conflictContents(file) {
231
231
 
232
232
  export {
233
233
  defineConfig,
234
+ CONFIG_FILE_NAMES,
234
235
  loadConfig,
235
236
  runGenerate
236
237
  };
237
- //# sourceMappingURL=chunk-NFZ5FMDO.js.map
238
+ //# sourceMappingURL=chunk-R3DEBBQ3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts","../src/pipeline.ts","../src/safety.ts"],"sourcesContent":["/**\n * Config: `defineConfig` for authoring, `loadConfig` for the CLI.\n *\n * Config files are plain JavaScript (`codegen.config.mjs`) so the CLI can\n * load them with a plain dynamic import — no TypeScript loader, no build\n * step, no extra dependencies. If you want types while authoring, that is\n * what `defineConfig` is for:\n *\n * import { defineConfig } from \"webmcp-codegen\";\n * export default defineConfig({ ... });\n */\n\nimport { access } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { CodegenConfig } from \"./types.js\";\n\n/** Identity function whose only job is type-checking the config object. */\nexport function defineConfig(config: CodegenConfig): CodegenConfig {\n return config;\n}\n\nexport const CONFIG_FILE_NAMES = [\"codegen.config.mjs\", \"codegen.config.js\"];\n\n/**\n * Find and load the config file. Resolving relative to `cwd` keeps the CLI\n * usable from any directory, the same way `eslint -c` behaves.\n */\nexport async function loadConfig(\n cwd: string,\n explicitPath?: string,\n): Promise<{ config: CodegenConfig; path: string }> {\n const candidates = explicitPath\n ? [resolve(cwd, explicitPath)]\n : CONFIG_FILE_NAMES.map((name) => join(cwd, name));\n\n for (const candidate of candidates) {\n if (!(await exists(candidate))) continue;\n const module = (await import(pathToFileURL(candidate).href)) as { default?: unknown };\n const config = module.default;\n if (!isCodegenConfig(config)) {\n throw new Error(\n `${candidate} must default-export defineConfig({ sources: [...], generate: [...] }).`,\n );\n }\n return { config, path: candidate };\n }\n\n throw new Error(\n explicitPath\n ? `No config file at \"${explicitPath}\".`\n : `No codegen.config.mjs found in ${cwd}. Run \\`npx webmcp-codegen init\\` to create one.`,\n );\n}\n\n/** The lightest possible shape check — clear error beats deep validation. */\nfunction isCodegenConfig(value: unknown): value is CodegenConfig {\n if (value === null || typeof value !== \"object\") return false;\n const config = value as Record<string, unknown>;\n return Array.isArray(config.sources) && Array.isArray(config.generate);\n}\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n","/**\n * The pipeline: sources → normalize → safety review → audit → write.\n *\n * This module is the only place the stages meet. It owns no opinions of its\n * own — naming, safety, and file formats all live in their own modules — it\n * just runs them in order and produces one honest report of what happened\n * (or what *would* happen, when called with `write: false`).\n */\n\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport { dedupeNames } from \"./naming.js\";\nimport { auditTools, reviewTools } from \"./safety.js\";\nimport { pascalCase } from \"./schema.js\";\nimport type { AuditFinding, CodegenConfig, GeneratedFile, ReviewedTool } from \"./types.js\";\n\nexport interface GenerateOptions {\n /** Project root. Everything (config, spec paths, outDir) resolves from here. */\n cwd: string;\n /** Preview mode: compute everything, write nothing. */\n dryRun?: boolean;\n /** Skip the audit pass entirely (classification still runs — output needs it). */\n skipAudit?: boolean;\n /** Write even when the audit found errors. The report still shows them. */\n force?: boolean;\n}\n\nexport interface GenerateResult {\n tools: ReviewedTool[];\n findings: AuditFinding[];\n files: GeneratedFile[];\n /** True when audit errors stopped any file from being written. */\n blocked: boolean;\n /** True when this run actually wrote files (false for dry runs and blocks). */\n wrote: boolean;\n}\n\nexport async function runGenerate(\n config: CodegenConfig,\n options: GenerateOptions,\n): Promise<GenerateResult> {\n // 1. Collect candidate tools from every configured source.\n const candidates = (await Promise.all(config.sources.map((source) => source.collect()))).flat();\n\n // 2. Normalize: make names unique before anything downstream sees them.\n // The input type name is derived from the *final* name so they never drift.\n const { names, renames } = dedupeNames(candidates);\n const named = candidates.map((candidate, index) => {\n const name = names[index] ?? candidate.name;\n return { ...candidate, name, inputTypeName: `${pascalCase(name)}Input` };\n });\n\n // 3. Safety review: classify side effects, compute hints, scan for PII,\n // apply config exclusions.\n const tools = reviewTools(named, config.safety);\n\n // 4. Audit. Errors block the write unless --force (or --skip-audit) was passed.\n const findings = options.skipAudit ? [] : auditTools(tools, renames);\n const errors = findings.filter((finding) => finding.level === \"error\");\n const blocked = errors.length > 0 && !options.force && !options.skipAudit;\n\n if (blocked) {\n return { tools, findings, files: [], blocked, wrote: false };\n }\n\n // 5. Generate the files, then write them (unless this is a dry run).\n const files: GeneratedFile[] = [];\n for (const generator of config.generate) {\n files.push(...(await generator.generate(tools, options.cwd)));\n }\n\n let wrote = false;\n if (!options.dryRun) {\n for (const file of files) {\n if (file.action === \"unchanged\" && !file.conflict) continue;\n // A conflict means a human edited the generated region by hand:\n // leave their file alone and put our version in a `.new` sibling.\n const target = file.conflict ?? file.path;\n await mkdir(dirname(target), { recursive: true });\n await writeFile(target, file.conflict ? conflictContents(file) : file.contents);\n }\n wrote = true;\n }\n\n return { tools, findings, files, blocked, wrote };\n}\n\n/**\n * When a hand-edited file blocks regeneration, the `.new` file explains\n * itself at the top so nobody mistakes it for something to import.\n */\nfunction conflictContents(file: GeneratedFile): string {\n return (\n `// webmcp-codegen could not regenerate ${file.path} because its generated\\n` +\n `// region was edited by hand. Review this version, then merge it manually.\\n\\n` +\n file.contents\n );\n}\n","/**\n * The safety layer.\n *\n * Nothing gets written to disk until every candidate tool has been through\n * here. This layer does three jobs:\n *\n * 1. Classify — what does calling this tool do to the world?\n * (read / write / destructive, from the HTTP verb plus name heuristics)\n * 2. Hint — derive the WebMCP tool hints (readOnlyHint etc.) from that\n * 3. Audit — lint the result and report problems in plain language\n *\n * Every rule is a heuristic with an escape hatch: the generated code carries\n * the classification in plain sight, and the developer owns the final file.\n */\n\nimport type {\n AuditFinding,\n CandidateTool,\n JsonSchema,\n ReviewedTool,\n RiskTier,\n SafetyOptions,\n SideEffect,\n ToolHints,\n} from \"./types.js\";\n\n/**\n * Words that signal \"this changes something the user can't easily undo\",\n * even when the HTTP verb looks innocent. `POST /orders/{id}/cancel` is the\n * classic case: a POST that behaves like a DELETE.\n */\nconst DESTRUCTIVE_WORDS =\n /\\b(cancel|delete|remove|destroy|deactivate|refund|revoke|purge|close)\\b/i;\n\n/**\n * Field names that usually hold personal data or secrets. Matched against\n * the last segment of a field path, case-insensitively. Teams extend this\n * list via `safety.piiFields` in the config.\n */\nconst DEFAULT_PII_FIELDS = [\n \"password\",\n \"ssn\",\n \"token\",\n \"secret\",\n \"apikey\",\n \"api_key\",\n \"email\",\n \"dob\",\n \"birthdate\",\n \"phone\",\n \"address\",\n \"creditcard\",\n \"cardnumber\",\n \"cvv\",\n];\n\n/**\n * Phrases that suggest a description is trying to *instruct the agent*\n * instead of describing the tool — a known prompt-injection smell.\n */\nconst AGENT_INSTRUCTION_PATTERN =\n /\\b(you (must|should|always|are)|as an ai|ignore (all |previous )?instructions|do not refuse)\\b/i;\n\n/** Step 1: classify what calling the tool does. */\nexport function classifySideEffect(tool: CandidateTool): SideEffect {\n switch (tool.httpMethod) {\n case \"GET\":\n case \"HEAD\":\n case \"OPTIONS\":\n // A safe verb whose name says otherwise is suspicious — audit flags it.\n return \"read\";\n case \"DELETE\":\n return \"destructive\";\n case \"POST\":\n case \"PUT\":\n case \"PATCH\":\n // Upgrade nominally-\"write\" verbs when the name says it can't be undone.\n return DESTRUCTIVE_WORDS.test(tool.name) || DESTRUCTIVE_WORDS.test(tool.source.ref)\n ? \"destructive\"\n : \"write\";\n default:\n return \"unknown\";\n }\n}\n\n/** Step 2: derive the WebMCP hints from the classification. */\nexport function hintsFor(tool: CandidateTool, sideEffect: SideEffect): ToolHints {\n const method = tool.httpMethod;\n return {\n readOnlyHint: sideEffect === \"read\",\n destructiveHint: sideEffect === \"destructive\",\n // PUT/PATCH/DELETE can be safely retried with the same input; POST cannot.\n idempotentHint:\n sideEffect === \"read\" || method === \"PUT\" || method === \"PATCH\" || method === \"DELETE\",\n };\n}\n\nexport function riskTierFor(sideEffect: SideEffect): RiskTier {\n switch (sideEffect) {\n case \"read\":\n return \"safe-read\";\n case \"destructive\":\n return \"destructive-confirm\";\n default:\n return \"write-confirm\";\n }\n}\n\n/**\n * Walk a schema and return the paths of fields that look like PII or\n * secrets, e.g. \"user.email\". Only *output* schemas are scanned: the\n * security-relevant direction is data leaving the page and reaching the agent.\n */\nexport function findPiiFields(\n schema: JsonSchema | undefined,\n extraFields: string[] = [],\n prefix = \"\",\n): string[] {\n if (!schema?.properties) return [];\n const piiNames = new Set(\n [...DEFAULT_PII_FIELDS, ...extraFields].map((name) => name.toLowerCase()),\n );\n const found: string[] = [];\n\n for (const [key, fieldSchema] of Object.entries(schema.properties)) {\n const path = prefix ? `${prefix}.${key}` : key;\n const normalizedKey = key.toLowerCase().replace(/[-_]/g, \"\");\n const looksSensitive =\n piiNames.has(key.toLowerCase()) ||\n piiNames.has(normalizedKey) ||\n [...piiNames].some((name) => normalizedKey === name.replace(/[-_]/g, \"\"));\n if (looksSensitive) found.push(path);\n // Recurse into nested objects (\"user\": { \"email\": ... }).\n found.push(...findPiiFields(fieldSchema, extraFields, path));\n }\n return found;\n}\n\n/** Run the full review: classify, hint, PII-scan. Pure — no I/O. */\nexport function reviewTools(\n candidates: CandidateTool[],\n safety: SafetyOptions = {},\n): ReviewedTool[] {\n const excluded = (safety.exclude ?? []).map((pattern) => pattern.toLowerCase());\n\n return candidates\n .filter(\n (tool) =>\n !excluded.some(\n (pattern) =>\n tool.name.toLowerCase().includes(pattern) ||\n tool.source.ref.toLowerCase().includes(pattern),\n ),\n )\n .map((tool) => {\n const sideEffect = classifySideEffect(tool);\n return {\n ...tool,\n sideEffect,\n riskTier: riskTierFor(sideEffect),\n hints: hintsFor(tool, sideEffect),\n piiInOutput: findPiiFields(tool.outputSchema, safety.piiFields),\n };\n });\n}\n\n/**\n * Step 3: audit the reviewed tools and report in plain language.\n * Errors block file writing (unless --force); warnings never do. This is\n * meant to run in CI like `npm audit` — exit codes, not vibes.\n */\nexport function auditTools(\n tools: ReviewedTool[],\n renames: { from: string; to: string }[] = [],\n): AuditFinding[] {\n const findings: AuditFinding[] = [];\n\n for (const rename of renames) {\n findings.push({\n level: \"warning\",\n tool: rename.to,\n message: `Renamed \"${rename.from}\" → \"${rename.to}\" to keep tool names unique.`,\n });\n }\n\n for (const tool of tools) {\n if (!tool.description || tool.description.trim().length === 0) {\n findings.push({\n level: \"error\",\n tool: tool.name,\n message: \"No description. Agents pick tools by description — this tool is invisible.\",\n });\n continue;\n }\n\n if (tool.descriptionSource === \"generated-template\") {\n findings.push({\n level: \"warning\",\n tool: tool.name,\n message:\n `Description is just \"${tool.description}\" (no summary in the source). ` +\n \"Write one sentence about what it does and why — it goes straight into the agent's prompt.\",\n });\n }\n\n if (AGENT_INSTRUCTION_PATTERN.test(tool.description)) {\n findings.push({\n level: \"warning\",\n tool: tool.name,\n message:\n \"The description reads like instructions to the agent, not a description of the tool. \" +\n \"Describe what the tool does; never try to steer the agent from here.\",\n });\n }\n\n if (tool.riskTier === \"safe-read\" && DESTRUCTIVE_WORDS.test(tool.name)) {\n findings.push({\n level: \"error\",\n tool: tool.name,\n message:\n `The name suggests something destructive but ${tool.httpMethod} is a safe verb. ` +\n \"Check the spec — a GET named like a delete is either mislabeled or a design smell.\",\n });\n }\n\n if (tool.piiInOutput.length > 0) {\n findings.push({\n level: \"warning\",\n tool: tool.name,\n message:\n `Response may expose ${tool.piiInOutput.join(\", \")}. ` +\n \"These fields reach the agent — exclude them in execute() unless they are truly needed.\",\n });\n }\n\n if (tool.requiresAuth && tool.riskTier !== \"safe-read\") {\n findings.push({\n level: \"warning\",\n tool: tool.name,\n message:\n \"This mutating tool wraps an authenticated endpoint. It runs with the page's session — \" +\n \"make sure your server-side authorization checks apply to tool calls too.\",\n });\n }\n }\n\n return findings;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,140 @@
1
+ import {
2
+ nameFromRoute,
3
+ toToolName
4
+ } from "./chunk-BIKKPCRT.js";
5
+ import {
6
+ deepDeref,
7
+ deref,
8
+ pascalCase
9
+ } from "./chunk-5L4KN6F4.js";
10
+
11
+ // src/sources/openapi.ts
12
+ import { readFile } from "fs/promises";
13
+ import { resolve } from "path";
14
+ import { parse as parseYaml } from "yaml";
15
+ var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
16
+ function openapi(options) {
17
+ return {
18
+ kind: "openapi",
19
+ async collect() {
20
+ const specPath = resolve(process.cwd(), options.spec);
21
+ const spec = await readSpec(specPath);
22
+ return operationsFromSpec(spec);
23
+ }
24
+ };
25
+ }
26
+ async function readSpec(specPath) {
27
+ let text;
28
+ try {
29
+ text = await readFile(specPath, "utf8");
30
+ } catch {
31
+ throw new Error(
32
+ `Could not read the OpenAPI spec at "${specPath}". Check the "spec" path in codegen.config.`
33
+ );
34
+ }
35
+ return parseYaml(text);
36
+ }
37
+ function operationsFromSpec(spec) {
38
+ const root = spec;
39
+ const paths = root.paths ?? {};
40
+ const rootSecurity = Array.isArray(root.security) && root.security.length > 0;
41
+ const candidates = [];
42
+ for (const [path, pathItem] of Object.entries(paths)) {
43
+ const sharedParams = Array.isArray(pathItem.parameters) ? pathItem.parameters : [];
44
+ for (const method of HTTP_METHODS) {
45
+ const operation = pathItem[method];
46
+ if (!operation) continue;
47
+ const upperMethod = method.toUpperCase();
48
+ const ref = `${upperMethod} ${path}`;
49
+ const name = typeof operation.operationId === "string" && operation.operationId.length > 0 ? toToolName(operation.operationId) : nameFromRoute(method, path);
50
+ const opSecurity = operation.security;
51
+ const requiresAuth = Array.isArray(opSecurity) ? opSecurity.length > 0 : rootSecurity;
52
+ candidates.push({
53
+ id: ref,
54
+ name,
55
+ source: { kind: "openapi", ref },
56
+ inputSchema: buildInputSchema(operation, sharedParams, spec),
57
+ outputSchema: findOutputSchema(operation, spec),
58
+ inputTypeName: `${pascalCase(name)}Input`,
59
+ httpMethod: upperMethod,
60
+ // The safety layer refines this — the source only reports the verb.
61
+ sideEffect: "unknown",
62
+ requiresAuth,
63
+ description: pickDescription(operation, upperMethod ?? method.toUpperCase(), path),
64
+ descriptionSource: typeof operation.summary === "string" || typeof operation.description === "string" ? "openapi-summary" : "generated-template"
65
+ });
66
+ }
67
+ }
68
+ if (candidates.length === 0) {
69
+ throw new Error('The OpenAPI spec has no operations under "paths". Nothing to generate.');
70
+ }
71
+ return candidates;
72
+ }
73
+ function buildInputSchema(operation, sharedParams, spec) {
74
+ const properties = {};
75
+ const required = /* @__PURE__ */ new Set();
76
+ const parameters = [
77
+ ...sharedParams,
78
+ ...Array.isArray(operation.parameters) ? operation.parameters : []
79
+ ];
80
+ for (const rawParam of parameters) {
81
+ const param = deref(rawParam, spec);
82
+ if (param.in !== "path" && param.in !== "query") continue;
83
+ if (typeof param.name !== "string") continue;
84
+ const fieldSchema = param.schema ? deepDeref(param.schema, spec) : { type: "string" };
85
+ properties[param.name] = typeof param.description === "string" ? { ...fieldSchema, description: param.description } : fieldSchema;
86
+ if (param.in === "path" || param.required === true) required.add(param.name);
87
+ }
88
+ const body = extractJsonBody(operation.requestBody, spec);
89
+ if (body) {
90
+ if (body.schema.type === "object" || body.schema.properties) {
91
+ for (const [key, value] of Object.entries(body.schema.properties ?? {})) {
92
+ properties[key] = value;
93
+ }
94
+ if (body.required) {
95
+ for (const key of body.schema.required ?? []) required.add(key);
96
+ }
97
+ } else {
98
+ properties.body = body.schema;
99
+ if (body.required) required.add("body");
100
+ }
101
+ }
102
+ return { type: "object", properties, required: [...required] };
103
+ }
104
+ function extractJsonBody(requestBody, spec) {
105
+ if (!requestBody || typeof requestBody !== "object") return void 0;
106
+ const body = deref(requestBody, spec);
107
+ const content = body.content;
108
+ const jsonEntry = content?.["application/json"] ?? Object.entries(content ?? {}).find(([mediaType]) => mediaType.endsWith("+json"))?.[1];
109
+ if (!jsonEntry?.schema) return void 0;
110
+ return {
111
+ schema: deepDeref(jsonEntry.schema, spec),
112
+ required: body.required === true
113
+ };
114
+ }
115
+ function findOutputSchema(operation, spec) {
116
+ const responses = operation.responses;
117
+ if (!responses) return void 0;
118
+ for (const [status, rawResponse] of Object.entries(responses)) {
119
+ if (!status.startsWith("2")) continue;
120
+ const response = deref(rawResponse, spec);
121
+ const content = response.content;
122
+ const jsonEntry = content?.["application/json"] ?? Object.entries(content ?? {}).find(([mediaType]) => mediaType.endsWith("+json"))?.[1];
123
+ if (jsonEntry?.schema) return deepDeref(jsonEntry.schema, spec);
124
+ }
125
+ return void 0;
126
+ }
127
+ function pickDescription(operation, method, path) {
128
+ if (typeof operation.summary === "string" && operation.summary.trim().length > 0) {
129
+ return operation.summary.trim();
130
+ }
131
+ if (typeof operation.description === "string" && operation.description.trim().length > 0) {
132
+ return operation.description.trim().split("\n")[0];
133
+ }
134
+ return `${method} ${path}`;
135
+ }
136
+
137
+ export {
138
+ openapi
139
+ };
140
+ //# sourceMappingURL=chunk-WYGVTIGI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sources/openapi.ts"],"sourcesContent":["/**\n * The OpenAPI source.\n *\n * Reads an OpenAPI 3.x document (YAML or JSON) and turns every operation\n * into a CandidateTool. This is the highest-reach source: most backend\n * frameworks can already emit an OpenAPI spec, so teams get value without\n * changing any application code.\n *\n * What we read from each operation:\n * - name ← operationId, slugified (falls back to method + path)\n * - description ← summary, else the first line of description, else a template\n * - inputSchema ← path + query parameters merged with the JSON request body\n * - outputSchema ← the first 2xx response's JSON schema, when present\n *\n * Header and cookie parameters are skipped on purpose: agents should not be\n * setting those by hand, and auth headers are the app's job, not the tool's.\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\nimport { nameFromRoute, toToolName } from \"../naming.js\";\nimport { deepDeref, deref, pascalCase } from \"../schema.js\";\nimport type { CandidateTool, JsonSchema, Source } from \"../types.js\";\n\nexport interface OpenApiSourceOptions {\n /** Path to the OpenAPI document, relative to the project root. */\n spec: string;\n}\n\nconst HTTP_METHODS = [\"get\", \"post\", \"put\", \"patch\", \"delete\", \"head\", \"options\"] as const;\n\n/** Create an OpenAPI source for the config's `sources` array. */\nexport function openapi(options: OpenApiSourceOptions): Source {\n return {\n kind: \"openapi\",\n async collect() {\n const specPath = resolve(process.cwd(), options.spec);\n const spec = await readSpec(specPath);\n return operationsFromSpec(spec);\n },\n };\n}\n\nasync function readSpec(specPath: string): Promise<unknown> {\n let text: string;\n try {\n text = await readFile(specPath, \"utf8\");\n } catch {\n throw new Error(\n `Could not read the OpenAPI spec at \"${specPath}\". Check the \"spec\" path in codegen.config.`,\n );\n }\n // YAML is a superset of JSON, so one parser handles both file types.\n return parseYaml(text);\n}\n\nfunction operationsFromSpec(spec: unknown): CandidateTool[] {\n const root = spec as Record<string, unknown>;\n const paths = (root.paths ?? {}) as Record<string, Record<string, unknown>>;\n const rootSecurity = Array.isArray(root.security) && root.security.length > 0;\n const candidates: CandidateTool[] = [];\n\n for (const [path, pathItem] of Object.entries(paths)) {\n // Parameters declared on the path item apply to every operation under it.\n const sharedParams = Array.isArray(pathItem.parameters) ? pathItem.parameters : [];\n\n for (const method of HTTP_METHODS) {\n const operation = pathItem[method] as Record<string, unknown> | undefined;\n if (!operation) continue;\n\n const upperMethod = method.toUpperCase() as CandidateTool[\"httpMethod\"];\n const ref = `${upperMethod} ${path}`;\n const name =\n typeof operation.operationId === \"string\" && operation.operationId.length > 0\n ? toToolName(operation.operationId)\n : nameFromRoute(method, path);\n\n const opSecurity = operation.security;\n const requiresAuth = Array.isArray(opSecurity) ? opSecurity.length > 0 : rootSecurity;\n\n candidates.push({\n id: ref,\n name,\n source: { kind: \"openapi\", ref },\n inputSchema: buildInputSchema(operation, sharedParams, spec),\n outputSchema: findOutputSchema(operation, spec),\n inputTypeName: `${pascalCase(name)}Input`,\n httpMethod: upperMethod,\n // The safety layer refines this — the source only reports the verb.\n sideEffect: \"unknown\",\n requiresAuth,\n description: pickDescription(operation, upperMethod ?? method.toUpperCase(), path),\n descriptionSource:\n typeof operation.summary === \"string\" || typeof operation.description === \"string\"\n ? \"openapi-summary\"\n : \"generated-template\",\n });\n }\n }\n\n if (candidates.length === 0) {\n throw new Error('The OpenAPI spec has no operations under \"paths\". Nothing to generate.');\n }\n return candidates;\n}\n\n/**\n * Merge path + query parameters and the JSON request body into one object\n * schema — that single object is what the agent fills in when calling the tool.\n */\nfunction buildInputSchema(\n operation: Record<string, unknown>,\n sharedParams: unknown[],\n spec: unknown,\n): JsonSchema {\n const properties: Record<string, JsonSchema> = {};\n const required = new Set<string>();\n\n const parameters = [\n ...sharedParams,\n ...(Array.isArray(operation.parameters) ? operation.parameters : []),\n ];\n\n for (const rawParam of parameters) {\n const param = deref(rawParam as JsonSchema, spec) as Record<string, unknown>;\n // Headers and cookies are transport concerns, not tool inputs.\n if (param.in !== \"path\" && param.in !== \"query\") continue;\n if (typeof param.name !== \"string\") continue;\n\n const fieldSchema = param.schema\n ? deepDeref(param.schema as JsonSchema, spec)\n : { type: \"string\" };\n properties[param.name] =\n typeof param.description === \"string\"\n ? { ...fieldSchema, description: param.description }\n : fieldSchema;\n // Path params are always required by definition; query params say so.\n if (param.in === \"path\" || param.required === true) required.add(param.name);\n }\n\n const body = extractJsonBody(operation.requestBody, spec);\n if (body) {\n if (body.schema.type === \"object\" || body.schema.properties) {\n // The common case: an object body flattens into the tool input.\n for (const [key, value] of Object.entries(body.schema.properties ?? {})) {\n properties[key] = value;\n }\n if (body.required) {\n for (const key of body.schema.required ?? []) required.add(key);\n }\n } else {\n // A non-object body (array, raw string, …) goes under a \"body\" field.\n properties.body = body.schema;\n if (body.required) required.add(\"body\");\n }\n }\n\n return { type: \"object\", properties, required: [...required] };\n}\n\n/** Find the operation's JSON request body schema, if it declares one. */\nfunction extractJsonBody(\n requestBody: unknown,\n spec: unknown,\n): { schema: JsonSchema; required: boolean } | undefined {\n if (!requestBody || typeof requestBody !== \"object\") return undefined;\n const body = deref(requestBody as JsonSchema, spec) as Record<string, unknown>;\n const content = body.content as Record<string, { schema?: JsonSchema }> | undefined;\n // Prefer application/json; accept any \"+json\" media type as a fallback.\n const jsonEntry =\n content?.[\"application/json\"] ??\n Object.entries(content ?? {}).find(([mediaType]) => mediaType.endsWith(\"+json\"))?.[1];\n if (!jsonEntry?.schema) return undefined;\n return {\n schema: deepDeref(jsonEntry.schema, spec),\n required: body.required === true,\n };\n}\n\n/** The response contract, used by the safety layer's PII scan (and later, docs). */\nfunction findOutputSchema(\n operation: Record<string, unknown>,\n spec: unknown,\n): JsonSchema | undefined {\n const responses = operation.responses as Record<string, unknown> | undefined;\n if (!responses) return undefined;\n // The first 2xx response with a JSON schema wins.\n for (const [status, rawResponse] of Object.entries(responses)) {\n if (!status.startsWith(\"2\")) continue;\n const response = deref(rawResponse as JsonSchema, spec) as Record<string, unknown>;\n const content = response.content as Record<string, { schema?: JsonSchema }> | undefined;\n const jsonEntry =\n content?.[\"application/json\"] ??\n Object.entries(content ?? {}).find(([mediaType]) => mediaType.endsWith(\"+json\"))?.[1];\n if (jsonEntry?.schema) return deepDeref(jsonEntry.schema, spec);\n }\n return undefined;\n}\n\n/**\n * The description is part of the agent's prompt, so we take the spec's own\n * words when they exist and only fall back to a plain template. Both paths\n * are marked so the audit report shows which descriptions need human love.\n */\nfunction pickDescription(operation: Record<string, unknown>, method: string, path: string): string {\n if (typeof operation.summary === \"string\" && operation.summary.trim().length > 0) {\n return operation.summary.trim();\n }\n if (typeof operation.description === \"string\" && operation.description.trim().length > 0) {\n // Use the first line only — long prose belongs in docs, not in a prompt.\n return operation.description.trim().split(\"\\n\")[0] as string;\n }\n return `${method} ${path}`;\n}\n"],"mappings":"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","names":[]}