webmcp-codegen 0.0.1 → 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
@@ -1,22 +1,96 @@
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 — OpenAPI specs, tRPC routers, Zod schemas — 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
- **Status: under active development.** This package is published as a placeholder to claim the name; the CLI and adapters are being built in the open. Design document: [`notes/webmcp-codegen-design.md`](https://github.com/SouravInsights/groundstate/blob/main/notes/webmcp-codegen-design.md).
7
+ ## Quick start
8
8
 
9
- ## What it will do
9
+ Zero install, zero config — the CLI detects your OpenAPI spec:
10
10
 
11
11
  ```bash
12
- npx webmcp-codegen init # detect your API layer, scaffold config
13
- npx webmcp-codegen generate # generate reviewed, safety-classified tool files
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
- - Real TypeScript files in your reporeadable, editable, no runtime magic
17
- - Schemas derived from your existing types, never hand-typed twice
18
- - Automatic risk classification (`readOnlyHint`, `destructiveHint`), PII field detection, and an audit pass built for CI
19
- - Regeneration that never clobbers your hand-written logic
16
+ Then implement each `execute()` (below the markerit's yours, regeneration never touches it) and register everything at app startup:
17
+
18
+ ```ts
19
+ import { registerAllTools } from "./webmcp";
20
+
21
+ await registerAllTools();
22
+ ```
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
+
38
+ ## What you get
39
+
40
+ For every operation in your spec, one file like `get-order-status.webmcp.ts`:
41
+
42
+ ```ts
43
+ // ─── webmcp-codegen: generated — do not edit this region ───
44
+ export const getOrderStatusInputSchema = { /* derived from your spec */ };
45
+ export type GetOrderStatusInput = { orderId: string };
46
+ export const getOrderStatusTool = { name: "get-order-status", /* ... */ };
47
+ export async function registerGetOrderStatus(signal?: AbortSignal) { /* ... */ }
48
+ // ─── webmcp-codegen: end generated — your code below survives regeneration ───
49
+
50
+ export async function executeGetOrderStatus(input: GetOrderStatusInput) {
51
+ // You own this. Regeneration never touches it.
52
+ }
53
+ ```
54
+
55
+ - **Real files in your repo** — readable, editable, no runtime magic
56
+ - **Schemas derived from your spec**, never hand-typed twice; `$ref`s fully resolved
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
60
+
61
+ ## CLI
62
+
63
+ | Command | What it does |
64
+ |---|---|
65
+ | `webmcp-codegen init` | Detect your spec, write `codegen.config.mjs` |
66
+ | `webmcp-codegen generate` | Generate/update tool files (audit runs by default) |
67
+ | `generate --dry-run` | Preview everything, write nothing |
68
+ | `generate --watch` | Re-generate when source files change |
69
+ | `generate --force` | Write files even when the audit reports errors |
70
+ | `generate --skip-audit` | Skip the audit pass |
71
+
72
+ ## Config
73
+
74
+ ```js
75
+ // codegen.config.mjs
76
+ import { defineConfig } from "webmcp-codegen";
77
+ import { openapi } from "webmcp-codegen/sources";
78
+ import { js } from "webmcp-codegen/generators";
79
+
80
+ export default defineConfig({
81
+ sources: [openapi({ spec: "./openapi.yaml" })],
82
+ generate: [js({ outDir: "./src/webmcp" })],
83
+ safety: {
84
+ piiFields: ["internalId"], // extend the built-in PII heuristics
85
+ exclude: ["internal"], // skip tools by name or route substring
86
+ },
87
+ });
88
+ ```
89
+
90
+ ## Requirements
91
+
92
+ - Node.js ≥ 20
93
+ - To *use* the generated tools in a browser: Chrome 146+ with `#enable-webmcp-testing` (or the WebMCP polyfill)
20
94
 
21
95
  ## License
22
96
 
@@ -0,0 +1,109 @@
1
+ // src/schema.ts
2
+ function resolveLocalRef(spec, ref) {
3
+ if (!ref.startsWith("#/")) {
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.`
6
+ );
7
+ }
8
+ let node = spec;
9
+ for (const segment of ref.slice(2).split("/")) {
10
+ const key = segment.replace(/~1/g, "/").replace(/~0/g, "~");
11
+ if (node === null || typeof node !== "object" || !(key in node)) {
12
+ throw new Error(`$ref "${ref}" does not point at anything in this spec (missing "${key}").`);
13
+ }
14
+ node = node[key];
15
+ }
16
+ return node;
17
+ }
18
+ function deref(schema, spec) {
19
+ const ref = schema.$ref;
20
+ if (typeof ref !== "string") return schema;
21
+ const target = resolveLocalRef(spec, ref);
22
+ const { $ref: _ignored, ...siblings } = schema;
23
+ return { ...target, ...siblings };
24
+ }
25
+ function deepDeref(schema, spec, ancestorRefs = /* @__PURE__ */ new Set()) {
26
+ const ref = schema.$ref;
27
+ if (typeof ref === "string") {
28
+ if (ancestorRefs.has(ref)) {
29
+ return {
30
+ type: "object",
31
+ description: `Recursive reference to ${ref} (resolved once to keep the schema finite).`
32
+ };
33
+ }
34
+ const target = resolveLocalRef(spec, ref);
35
+ return deepDeref(target, spec, new Set(ancestorRefs).add(ref));
36
+ }
37
+ const out = { ...schema };
38
+ if (out.properties) {
39
+ out.properties = Object.fromEntries(
40
+ Object.entries(out.properties).map(([key, value]) => [
41
+ key,
42
+ deepDeref(value, spec, ancestorRefs)
43
+ ])
44
+ );
45
+ }
46
+ if (out.items) out.items = deepDeref(out.items, spec, ancestorRefs);
47
+ for (const unionKeyword of ["anyOf", "oneOf", "allOf"]) {
48
+ const variants = out[unionKeyword];
49
+ if (variants) {
50
+ out[unionKeyword] = variants.map((variant) => deepDeref(variant, spec, ancestorRefs));
51
+ }
52
+ }
53
+ return out;
54
+ }
55
+ function jsonSchemaToTs(schema, spec) {
56
+ const node = deref(schema, spec);
57
+ if (node.enum && Array.isArray(node.enum)) {
58
+ return node.enum.map((value) => JSON.stringify(value)).join(" | ");
59
+ }
60
+ if (node.anyOf || node.oneOf) {
61
+ const variants = node.anyOf ?? node.oneOf;
62
+ return variants.map((variant) => jsonSchemaToTs(variant, spec)).join(" | ");
63
+ }
64
+ if (node.allOf) {
65
+ return node.allOf.map((part) => jsonSchemaToTs(part, spec)).join(" & ");
66
+ }
67
+ switch (node.type) {
68
+ case "string":
69
+ return "string";
70
+ case "number":
71
+ case "integer":
72
+ return "number";
73
+ case "boolean":
74
+ return "boolean";
75
+ case "null":
76
+ return "null";
77
+ case "array": {
78
+ const items = node.items ? jsonSchemaToTs(node.items, spec) : "unknown";
79
+ return items.includes("|") ? `Array<${items}>` : `${items}[]`;
80
+ }
81
+ case "object":
82
+ case void 0: {
83
+ const properties = node.properties;
84
+ if (!properties || Object.keys(properties).length === 0) {
85
+ return "Record<string, unknown>";
86
+ }
87
+ const required = new Set(node.required ?? []);
88
+ const fields = Object.entries(properties).map(([key, fieldSchema]) => {
89
+ const optional = required.has(key) ? "" : "?";
90
+ const nullable = fieldSchema.nullable ? " | null" : "";
91
+ return `${JSON.stringify(key)}${optional}: ${jsonSchemaToTs(fieldSchema, spec)}${nullable}`;
92
+ });
93
+ return `{ ${fields.join("; ")} }`;
94
+ }
95
+ default:
96
+ return "unknown /* TODO: webmcp-codegen could not express this schema \u2014 tighten it by hand */";
97
+ }
98
+ }
99
+ function pascalCase(name) {
100
+ return name.split(/[-_]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
101
+ }
102
+
103
+ export {
104
+ deref,
105
+ deepDeref,
106
+ jsonSchemaToTs,
107
+ pascalCase
108
+ };
109
+ //# sourceMappingURL=chunk-5L4KN6F4.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":[]}
@@ -0,0 +1,39 @@
1
+ // src/naming.ts
2
+ function toToolName(raw) {
3
+ const name = raw.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[{}/_\s.]+/g, "-").replace(/[^a-zA-Z0-9-]/g, "").toLowerCase().replace(/-+/g, "-").replace(/^-+|-+$/g, "");
4
+ const prefixed = /^[a-zA-Z]/.test(name) ? name : `tool-${name}`;
5
+ return prefixed || "unnamed-tool";
6
+ }
7
+ function nameFromRoute(method, path) {
8
+ return toToolName(`${method.toLowerCase()} ${path}`);
9
+ }
10
+ function dedupeNames(candidates) {
11
+ const seen = /* @__PURE__ */ new Set();
12
+ const names = [];
13
+ const renames = [];
14
+ for (const candidate of candidates) {
15
+ let name = candidate.name;
16
+ if (seen.has(name) && candidate.httpMethod) {
17
+ name = `${name}-${candidate.httpMethod.toLowerCase()}`;
18
+ }
19
+ let counter = 2;
20
+ const base = name;
21
+ while (seen.has(name)) {
22
+ name = `${base}-${counter}`;
23
+ counter += 1;
24
+ }
25
+ if (name !== candidate.name) {
26
+ renames.push({ from: candidate.name, to: name });
27
+ }
28
+ seen.add(name);
29
+ names.push(name);
30
+ }
31
+ return { names, renames };
32
+ }
33
+
34
+ export {
35
+ toToolName,
36
+ nameFromRoute,
37
+ dedupeNames
38
+ };
39
+ //# sourceMappingURL=chunk-BIKKPCRT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/naming.ts"],"sourcesContent":["/**\n * Tool naming.\n *\n * Tool names are the vocabulary an agent reasons over, so we make them\n * boring and predictable: kebab-case, derived from the operationId when the\n * source has one, and always matching the character set the WebMCP runtime\n * accepts (the same rule the groundstate core registry enforces).\n */\n\n/** The character set the WebMCP runtime accepts for tool names. */\nexport const TOOL_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;\n\n/**\n * Turn an operationId or route into a valid, readable tool name.\n *\n * Examples:\n * \"getOrderStatus\" → \"get-order-status\"\n * \"GET /orders/{id}\" → \"get-orders-id\"\n * \"list_pets\" → \"list-pets\"\n */\nexport function toToolName(raw: string): string {\n const name = raw\n // Split acronym boundaries first: \"getHTTPStatus\" → \"get-HTTPStatus\"\n .replace(/([A-Z]+)([A-Z][a-z])/g, \"$1-$2\")\n // Then camelCase and PascalCase boundaries: \"getOrder\" → \"get-Order\"\n .replace(/([a-z0-9])([A-Z])/g, \"$1-$2\")\n // Path placeholders and separators become dashes: \"/orders/{id}\" → \"-orders-id\"\n .replace(/[{}/_\\s.]+/g, \"-\")\n // Anything left that isn't a letter, digit or dash is dropped\n .replace(/[^a-zA-Z0-9-]/g, \"\")\n .toLowerCase()\n // Collapse and trim dashes\n .replace(/-+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n\n // A name must start with a letter. If it doesn't (e.g. it came from a bare\n // numeric path), give it a neutral prefix rather than failing.\n const prefixed = /^[a-zA-Z]/.test(name) ? name : `tool-${name}`;\n return prefixed || \"unnamed-tool\";\n}\n\n/**\n * Build the fallback name for an operation that has no operationId:\n * the HTTP method plus the path, e.g. GET /orders/{id} → \"get-orders-id\".\n */\nexport function nameFromRoute(method: string, path: string): string {\n return toToolName(`${method.toLowerCase()} ${path}`);\n}\n\n/**\n * Make every name unique. When two operations slugify to the same name we\n * append the HTTP method (\"get-order-status-post\" would be worse); when that\n * still collides we append a counter. Returns the final names plus a list of\n * renames so the audit report can show them.\n */\nexport function dedupeNames(candidates: { name: string; httpMethod?: string }[]): {\n names: string[];\n renames: { from: string; to: string }[];\n} {\n const seen = new Set<string>();\n const names: string[] = [];\n const renames: { from: string; to: string }[] = [];\n\n for (const candidate of candidates) {\n let name = candidate.name;\n if (seen.has(name) && candidate.httpMethod) {\n name = `${name}-${candidate.httpMethod.toLowerCase()}`;\n }\n let counter = 2;\n const base = name;\n while (seen.has(name)) {\n name = `${base}-${counter}`;\n counter += 1;\n }\n if (name !== candidate.name) {\n renames.push({ from: candidate.name, to: name });\n }\n seen.add(name);\n names.push(name);\n }\n\n return { names, renames };\n}\n"],"mappings":";AAoBO,SAAS,WAAW,KAAqB;AAC9C,QAAM,OAAO,IAEV,QAAQ,yBAAyB,OAAO,EAExC,QAAQ,sBAAsB,OAAO,EAErC,QAAQ,eAAe,GAAG,EAE1B,QAAQ,kBAAkB,EAAE,EAC5B,YAAY,EAEZ,QAAQ,OAAO,GAAG,EAClB,QAAQ,YAAY,EAAE;AAIzB,QAAM,WAAW,YAAY,KAAK,IAAI,IAAI,OAAO,QAAQ,IAAI;AAC7D,SAAO,YAAY;AACrB;AAMO,SAAS,cAAc,QAAgB,MAAsB;AAClE,SAAO,WAAW,GAAG,OAAO,YAAY,CAAC,IAAI,IAAI,EAAE;AACrD;AAQO,SAAS,YAAY,YAG1B;AACA,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,QAAkB,CAAC;AACzB,QAAM,UAA0C,CAAC;AAEjD,aAAW,aAAa,YAAY;AAClC,QAAI,OAAO,UAAU;AACrB,QAAI,KAAK,IAAI,IAAI,KAAK,UAAU,YAAY;AAC1C,aAAO,GAAG,IAAI,IAAI,UAAU,WAAW,YAAY,CAAC;AAAA,IACtD;AACA,QAAI,UAAU;AACd,UAAM,OAAO;AACb,WAAO,KAAK,IAAI,IAAI,GAAG;AACrB,aAAO,GAAG,IAAI,IAAI,OAAO;AACzB,iBAAW;AAAA,IACb;AACA,QAAI,SAAS,UAAU,MAAM;AAC3B,cAAQ,KAAK,EAAE,MAAM,UAAU,MAAM,IAAI,KAAK,CAAC;AAAA,IACjD;AACA,SAAK,IAAI,IAAI;AACb,UAAM,KAAK,IAAI;AAAA,EACjB;AAEA,SAAO,EAAE,OAAO,QAAQ;AAC1B;","names":[]}
@@ -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":[]}