webmcp-codegen 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,19 +1,19 @@
1
1
  # webmcp-codegen
2
2
 
3
- Generate safe, typed, human-reviewed [WebMCP](https://github.com/webmachinelearning/webmcp) tools from the API contracts you already have instead of hand-writing `registerTool()` calls for every action.
3
+ Generate safe, typed, human-reviewed [WebMCP](https://github.com/webmachinelearning/webmcp) tools from the API contracts you already have, instead of hand-writing `registerTool()` calls for every action.
4
4
 
5
5
  > Generate the tool. Review the tool. You own the tool.
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
- Then implement each `execute()` (below the marker it's yours, regeneration never touches it) and register everything at app startup:
16
+ Then implement each `execute()` (below the marker; it's yours, regeneration never touches it) and register everything at app startup:
17
17
 
18
18
  ```ts
19
19
  import { registerAllTools } from "./webmcp";
@@ -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`:
@@ -38,11 +52,11 @@ export async function executeGetOrderStatus(input: GetOrderStatusInput) {
38
52
  }
39
53
  ```
40
54
 
41
- - **Real files in your repo** readable, editable, no runtime magic
55
+ - **Real files in your repo**: readable, editable, no runtime magic
42
56
  - **Schemas derived from your spec**, never hand-typed twice; `$ref`s fully resolved
43
- - **Safety classification on every tool** read/write/destructive from the HTTP verb and naming heuristics, with `readOnlyHint`/`destructiveHint`/`idempotentHint` computed for you
44
- - **An audit pass built into `generate`** PII-in-response warnings, agent-instructing description linting, auth-boundary checks; errors block generation (like `npm audit`, with exit codes for CI)
45
- - **Regeneration never clobbers your code** contracts regenerate, your `execute()` survives; hand-edited generated regions produce a `.new` file instead of a conflict
57
+ - **Safety classification on every tool**: read/write/destructive from the HTTP verb and naming heuristics, with `readOnlyHint`/`destructiveHint`/`idempotentHint` computed for you
58
+ - **An audit pass built into `generate`**: PII-in-response warnings, agent-instructing description linting, auth-boundary checks; errors block generation (like `npm audit`, with exit codes for CI)
59
+ - **Regeneration never clobbers your code**: contracts regenerate, your `execute()` survives; hand-edited generated regions produce a `.new` file instead of a conflict
46
60
 
47
61
  ## CLI
48
62
 
@@ -2,7 +2,7 @@
2
2
  function resolveLocalRef(spec, ref) {
3
3
  if (!ref.startsWith("#/")) {
4
4
  throw new Error(
5
- `Cannot resolve external $ref "${ref}". Only local refs (starting with "#/") are supported \u2014 bundle the spec first if it is split across files.`
5
+ `Cannot resolve external $ref "${ref}". Only local refs (starting with "#/") are supported. Bundle the spec first if it is split across files.`
6
6
  );
7
7
  }
8
8
  let node = spec;
@@ -93,7 +93,7 @@ function jsonSchemaToTs(schema, spec) {
93
93
  return `{ ${fields.join("; ")} }`;
94
94
  }
95
95
  default:
96
- return "unknown /* TODO: webmcp-codegen could not express this schema \u2014 tighten it by hand */";
96
+ return "unknown /* TODO: webmcp-codegen could not express this schema; tighten it by hand */";
97
97
  }
98
98
  }
99
99
  function pascalCase(name) {
@@ -106,4 +106,4 @@ export {
106
106
  jsonSchemaToTs,
107
107
  pascalCase
108
108
  };
109
- //# sourceMappingURL=chunk-5L4KN6F4.js.map
109
+ //# sourceMappingURL=chunk-KSQMJERY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schema.ts"],"sourcesContent":["/**\n * JSON Schema helpers: resolving `$ref` pointers and turning a schema into\n * TypeScript source text for the generated input types.\n *\n * Scope, deliberately small:\n * - Only *local* refs (`#/components/schemas/...`) are resolved. External\n * refs (other files or URLs) produce a clear error instead of a silent\n * wrong answer. This covers the overwhelming majority of hand-written and\n * framework-emitted OpenAPI specs.\n * - The TypeScript printer covers the shapes REST APIs actually use:\n * objects with required/optional fields, arrays, enums, primitives, and\n * `anyOf`/`oneOf` unions. Anything more exotic becomes `unknown` with a\n * TODO comment rather than a plausible-looking lie.\n */\n\nimport type { JsonSchema } from \"./types.js\";\n\n/**\n * Resolve a local `$ref` like \"#/components/schemas/Order\" against the\n * parsed spec document. Throws a clear error for external refs.\n */\nexport function resolveLocalRef(spec: unknown, ref: string): unknown {\n if (!ref.startsWith(\"#/\")) {\n throw new Error(\n `Cannot resolve external $ref \"${ref}\". ` +\n `Only local refs (starting with \"#/\") are supported. Bundle the spec first if it is split across files.`,\n );\n }\n let node: unknown = spec;\n for (const segment of ref.slice(2).split(\"/\")) {\n // JSON Pointer escapes: \"~1\" is \"/\", \"~0\" is \"~\"\n const key = segment.replace(/~1/g, \"/\").replace(/~0/g, \"~\");\n if (node === null || typeof node !== \"object\" || !(key in node)) {\n throw new Error(`$ref \"${ref}\" does not point at anything in this spec (missing \"${key}\").`);\n }\n node = (node as Record<string, unknown>)[key];\n }\n return node;\n}\n\n/**\n * If `schema` is a `$ref`, resolve it (one level; recursion happens as the\n * caller walks the tree). Sibling keywords next to `$ref` are merged over\n * the resolved schema, which is what OpenAPI 3.1 semantics require.\n */\nexport function deref(schema: JsonSchema, spec: unknown): JsonSchema {\n const ref = schema.$ref;\n if (typeof ref !== \"string\") return schema;\n const target = resolveLocalRef(spec, ref) as JsonSchema;\n const { $ref: _ignored, ...siblings } = schema;\n return { ...target, ...siblings };\n}\n\n/**\n * Resolve `$ref`s at every depth, so the generated `inputSchema` is a\n * self-contained JSON Schema. The browser has no idea what\n * \"#/components/schemas/Order\" means, so refs must not survive codegen.\n *\n * Recursive models (Order → LineItem → Order) would loop forever, so a ref\n * that points back to one of its own ancestors resolves to a plain object\n * with a note instead. The tool schema stays finite and honest.\n */\nexport function deepDeref(\n schema: JsonSchema,\n spec: unknown,\n ancestorRefs: Set<string> = new Set(),\n): JsonSchema {\n const ref = schema.$ref;\n if (typeof ref === \"string\") {\n if (ancestorRefs.has(ref)) {\n return {\n type: \"object\",\n description: `Recursive reference to ${ref} (resolved once to keep the schema finite).`,\n };\n }\n const target = resolveLocalRef(spec, ref) as JsonSchema;\n return deepDeref(target, spec, new Set(ancestorRefs).add(ref));\n }\n\n const out: JsonSchema = { ...schema };\n if (out.properties) {\n out.properties = Object.fromEntries(\n Object.entries(out.properties).map(([key, value]) => [\n key,\n deepDeref(value, spec, ancestorRefs),\n ]),\n );\n }\n if (out.items) out.items = deepDeref(out.items, spec, ancestorRefs);\n for (const unionKeyword of [\"anyOf\", \"oneOf\", \"allOf\"] as const) {\n const variants = out[unionKeyword];\n if (variants) {\n out[unionKeyword] = variants.map((variant) => deepDeref(variant, spec, ancestorRefs));\n }\n }\n return out;\n}\n\n/**\n * Print a JSON Schema as TypeScript type source, e.g. for the generated\n * `GetOrderStatusInput` interface. `spec` is the root document, needed to\n * resolve any `$ref`s encountered along the way.\n */\nexport function jsonSchemaToTs(schema: JsonSchema, spec: unknown): string {\n const node = deref(schema, spec);\n\n if (node.enum && Array.isArray(node.enum)) {\n return node.enum.map((value) => JSON.stringify(value)).join(\" | \");\n }\n\n if (node.anyOf || node.oneOf) {\n const variants = (node.anyOf ?? node.oneOf) as JsonSchema[];\n return variants.map((variant) => jsonSchemaToTs(variant, spec)).join(\" | \");\n }\n\n if (node.allOf) {\n return node.allOf.map((part) => jsonSchemaToTs(part, spec)).join(\" & \");\n }\n\n switch (node.type) {\n case \"string\":\n return \"string\";\n case \"number\":\n case \"integer\":\n return \"number\";\n case \"boolean\":\n return \"boolean\";\n case \"null\":\n return \"null\";\n case \"array\": {\n const items = node.items ? jsonSchemaToTs(node.items, spec) : \"unknown\";\n // Wrap unions so `string | number[]` doesn't silently change meaning.\n return items.includes(\"|\") ? `Array<${items}>` : `${items}[]`;\n }\n case \"object\":\n case undefined: {\n // Schemas without an explicit \"type\" but with \"properties\" are objects.\n const properties = node.properties;\n if (!properties || Object.keys(properties).length === 0) {\n return \"Record<string, unknown>\";\n }\n const required = new Set(node.required ?? []);\n const fields = Object.entries(properties).map(([key, fieldSchema]) => {\n const optional = required.has(key) ? \"\" : \"?\";\n const nullable = fieldSchema.nullable ? \" | null\" : \"\";\n return `${JSON.stringify(key)}${optional}: ${jsonSchemaToTs(fieldSchema, spec)}${nullable}`;\n });\n return `{ ${fields.join(\"; \")} }`;\n }\n default:\n return \"unknown /* TODO: webmcp-codegen could not express this schema; tighten it by hand */\";\n }\n}\n\n/** \"get-order-status\" → \"GetOrderStatus\" (for generated type names). */\nexport function pascalCase(name: string): string {\n return name\n .split(/[-_]/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(\"\");\n}\n"],"mappings":"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","names":[]}
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-BIKKPCRT.js";
4
4
  import {
5
5
  pascalCase
6
- } from "./chunk-5L4KN6F4.js";
6
+ } from "./chunk-KSQMJERY.js";
7
7
 
8
8
  // src/config.ts
9
9
  import { access } from "fs/promises";
@@ -148,7 +148,7 @@ function auditTools(tools, renames = []) {
148
148
  findings.push({
149
149
  level: "error",
150
150
  tool: tool.name,
151
- message: "No description. Agents pick tools by description \u2014 this tool is invisible."
151
+ message: "No description. Agents pick tools by description. This tool is invisible."
152
152
  });
153
153
  continue;
154
154
  }
@@ -156,7 +156,7 @@ function auditTools(tools, renames = []) {
156
156
  findings.push({
157
157
  level: "warning",
158
158
  tool: tool.name,
159
- message: `Description is just "${tool.description}" (no summary in the source). Write one sentence about what it does and why \u2014 it goes straight into the agent's prompt.`
159
+ message: `Description is just "${tool.description}" (no summary in the source). Write one sentence about what it does and why. It goes straight into the agent's prompt.`
160
160
  });
161
161
  }
162
162
  if (AGENT_INSTRUCTION_PATTERN.test(tool.description)) {
@@ -170,21 +170,21 @@ function auditTools(tools, renames = []) {
170
170
  findings.push({
171
171
  level: "error",
172
172
  tool: tool.name,
173
- message: `The name suggests something destructive but ${tool.httpMethod} is a safe verb. Check the spec \u2014 a GET named like a delete is either mislabeled or a design smell.`
173
+ message: `The name suggests something destructive but ${tool.httpMethod} is a safe verb. Check the spec: a GET named like a delete is either mislabeled or a design smell.`
174
174
  });
175
175
  }
176
176
  if (tool.piiInOutput.length > 0) {
177
177
  findings.push({
178
178
  level: "warning",
179
179
  tool: tool.name,
180
- message: `Response may expose ${tool.piiInOutput.join(", ")}. These fields reach the agent \u2014 exclude them in execute() unless they are truly needed.`
180
+ message: `Response may expose ${tool.piiInOutput.join(", ")}. These fields reach the agent. Exclude them in execute() unless they are truly needed.`
181
181
  });
182
182
  }
183
183
  if (tool.requiresAuth && tool.riskTier !== "safe-read") {
184
184
  findings.push({
185
185
  level: "warning",
186
186
  tool: tool.name,
187
- message: "This mutating tool wraps an authenticated endpoint. It runs with the page's session \u2014 make sure your server-side authorization checks apply to tool calls too."
187
+ message: "This mutating tool wraps an authenticated endpoint. It runs with the page's session, so make sure your server-side authorization checks apply to tool calls too."
188
188
  });
189
189
  }
190
190
  }
@@ -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-LYHLGSAI.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: a 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. That is 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, so \" +\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-KSQMJERY.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-N245GNCB.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":[]}
@@ -0,0 +1,251 @@
1
+ import {
2
+ jsonSchemaToTs,
3
+ pascalCase
4
+ } from "./chunk-KSQMJERY.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}). 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. 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}. 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. 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. 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). 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. 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. 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. Do not edit this region. \u2500\u2500\u2500";
205
+ var GENERATED_END = "// \u2500\u2500\u2500 webmcp-codegen: end generated. 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-OILNQ2HE.js.map