webmcp-codegen 0.2.0 → 0.3.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,76 +1,85 @@
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
- > Generate the tool. Review the tool. You own the tool.
5
+ > Your spec already knows your tools. One command writes them, wires them into your app, and gets out of the way.
6
6
 
7
7
  ## Quick start
8
8
 
9
- Zero install, zero config — the CLI detects your OpenAPI spec:
9
+ Zero install, zero config:
10
10
 
11
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
12
+ npx webmcp-codegen generate
14
13
  ```
15
14
 
16
- Then implement each `execute()` (below the marker — it's yours, regeneration never touches it) and register everything at app startup:
15
+ One run finds your OpenAPI spec (monorepos included), finds the package that is your web app, and:
17
16
 
18
- ```ts
19
- import { registerAllTools } from "./webmcp";
17
+ - **generates working tools** in `src/webmcp/`: reads call your API out of the box, mutations are generated disabled (working code, one uncomment away)
18
+ - **filters what shouldn't be a tool**: webhooks skipped, auth and admin endpoints flagged and disabled
19
+ - **wires registration into your app** (two additive lines for Next.js and Vite, reported with undo instructions)
20
+
21
+ Preview first, write nothing:
20
22
 
21
- await registerAllTools();
23
+ ```bash
24
+ npx webmcp-codegen generate --dry-run
22
25
  ```
23
26
 
24
- ### Full control
27
+ Then start your app, open it in Chrome with `#enable-webmcp-testing`, and ask the agent to use one of your tools.
25
28
 
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:
29
+ ## The dashboard
28
30
 
29
31
  ```bash
30
- npm install -D webmcp-codegen
31
- npx webmcp-codegen init
32
+ npx webmcp-codegen dev
32
33
  ```
33
34
 
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.)
35
+ A local control panel for your tools: browse them, edit descriptions, toggle tools on and off, and run any tool directly to check it works. Edits save to `.webmcp-codegen.json` and survive regeneration. Nothing is added to your app.
37
36
 
38
- ## What you get
37
+ ## What a generated tool looks like
39
38
 
40
- For every operation in your spec, one file like `get-order-status.webmcp.ts`:
39
+ One file per endpoint, like `delete-pet.webmcp.ts`:
41
40
 
42
41
  ```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.
42
+ // ─── webmcp-codegen: generated. Do not edit this region. ───
43
+ export const deletePetInputSchema = { /* derived from your spec */ };
44
+ export type DeletePetInput = { id: string };
45
+ export async function registerDeletePet(signal?: AbortSignal) {
46
+ // Registers the tool; mutations ask the user to confirm, always.
47
+ }
48
+ // ─── webmcp-codegen: end generated. Your code below survives regeneration. ───
49
+
50
+ export async function executeDeletePet(input: DeletePetInput) {
51
+ // This tool starts disabled: it changes things. To enable it, delete the
52
+ // line below and uncomment the code.
53
+ return toolDisabled("delete-pet.webmcp.ts");
54
+
55
+ // const data = await callApi(`/pets/${input.id}`, { method: "DELETE" });
56
+ // return toolResult(data);
52
57
  }
53
58
  ```
54
59
 
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
+ - **Real files in your repo**: readable, editable, no runtime dependency on this package
61
+ - **Working implementations**: path params, query strings, and JSON bodies built from the spec; session cookies included
62
+ - **Safety classification on every tool**: read/write/destructive, with WebMCP hints computed
63
+ - **An audit pass built into `generate`**: PII-in-response warnings, agent-instructing description linting, auth-boundary checks; errors block generation (exit codes for CI)
64
+ - **Regeneration never clobbers your code**: the contract regenerates above the marker, your code below it survives; hand-edited generated regions produce a `.new` file, never a silent overwrite
60
65
 
61
66
  ## CLI
62
67
 
63
68
  | Command | What it does |
64
69
  |---|---|
65
- | `webmcp-codegen init` | Detect your spec, write `codegen.config.mjs` |
66
- | `webmcp-codegen generate` | Generate/update tool files (audit runs by default) |
70
+ | `webmcp-codegen generate` | Generate/update tools, wire registration (audit runs by default) |
67
71
  | `generate --dry-run` | Preview everything, write nothing |
68
72
  | `generate --watch` | Re-generate when source files change |
69
73
  | `generate --force` | Write files even when the audit reports errors |
70
- | `generate --skip-audit` | Skip the audit pass |
74
+ | `generate --spec PATH` / `--out DIR` | Overrides without a config file |
75
+ | `webmcp-codegen dev` | Open the tools dashboard (`--port N` to change the port) |
76
+ | `webmcp-codegen init` | Write `codegen.config.mjs` for full control (needs the package installed) |
71
77
 
72
78
  ## Config
73
79
 
80
+ Structure lives in `codegen.config.mjs` (code); remembered choices and per-tool
81
+ overrides live in `.webmcp-codegen.json` (data, safe with npx, commit it).
82
+
74
83
  ```js
75
84
  // codegen.config.mjs
76
85
  import { defineConfig } from "webmcp-codegen";
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  nameFromRoute,
3
3
  toToolName
4
- } from "./chunk-BIKKPCRT.js";
4
+ } from "./chunk-FWSATV7C.js";
5
5
  import {
6
6
  deepDeref,
7
7
  deref,
8
8
  pascalCase
9
- } from "./chunk-5L4KN6F4.js";
9
+ } from "./chunk-KSQMJERY.js";
10
10
 
11
11
  // src/sources/openapi.ts
12
12
  import { readFile } from "fs/promises";
@@ -38,6 +38,7 @@ function operationsFromSpec(spec) {
38
38
  const root = spec;
39
39
  const paths = root.paths ?? {};
40
40
  const rootSecurity = Array.isArray(root.security) && root.security.length > 0;
41
+ const serverUrl = pickServerUrl(root);
41
42
  const candidates = [];
42
43
  for (const [path, pathItem] of Object.entries(paths)) {
43
44
  const sharedParams = Array.isArray(pathItem.parameters) ? pathItem.parameters : [];
@@ -57,7 +58,10 @@ function operationsFromSpec(spec) {
57
58
  outputSchema: findOutputSchema(operation, spec),
58
59
  inputTypeName: `${pascalCase(name)}Input`,
59
60
  httpMethod: upperMethod,
60
- // The safety layer refines this — the source only reports the verb.
61
+ pathTemplate: path,
62
+ paramLocations: locateParams(operation, sharedParams, spec),
63
+ serverUrl,
64
+ // The safety layer refines this; the source only reports the verb.
61
65
  sideEffect: "unknown",
62
66
  requiresAuth,
63
67
  description: pickDescription(operation, upperMethod ?? method.toUpperCase(), path),
@@ -70,6 +74,35 @@ function operationsFromSpec(spec) {
70
74
  }
71
75
  return candidates;
72
76
  }
77
+ function pickServerUrl(root) {
78
+ const servers = root.servers;
79
+ if (!Array.isArray(servers) || servers.length === 0) return void 0;
80
+ const first = servers[0];
81
+ const url = first?.url;
82
+ return typeof url === "string" && url.length > 0 ? url : void 0;
83
+ }
84
+ function locateParams(operation, sharedParams, spec) {
85
+ const locations = { path: [], query: [], body: [] };
86
+ const parameters = [
87
+ ...sharedParams,
88
+ ...Array.isArray(operation.parameters) ? operation.parameters : []
89
+ ];
90
+ for (const rawParam of parameters) {
91
+ const param = deref(rawParam, spec);
92
+ if (typeof param.name !== "string") continue;
93
+ if (param.in === "path") locations.path.push(param.name);
94
+ if (param.in === "query") locations.query.push(param.name);
95
+ }
96
+ const body = extractJsonBody(operation.requestBody, spec);
97
+ if (body) {
98
+ if (body.schema.type === "object" || body.schema.properties) {
99
+ locations.body.push(...Object.keys(body.schema.properties ?? {}));
100
+ } else {
101
+ locations.body.push("body");
102
+ }
103
+ }
104
+ return locations;
105
+ }
73
106
  function buildInputSchema(operation, sharedParams, spec) {
74
107
  const properties = {};
75
108
  const required = /* @__PURE__ */ new Set();
@@ -137,4 +170,4 @@ function pickDescription(operation, method, path) {
137
170
  export {
138
171
  openapi
139
172
  };
140
- //# sourceMappingURL=chunk-WYGVTIGI.js.map
173
+ //# sourceMappingURL=chunk-3LTHWIAP.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 serverUrl = pickServerUrl(root);\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 pathTemplate: path,\n paramLocations: locateParams(operation, sharedParams, spec),\n serverUrl,\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 * The spec's preferred base URL (the first entry in `servers`), when absolute.\n * Generated code calls the API relative to the page by default; this goes in\n * a comment so developers with a separate API host know the intended base.\n */\nfunction pickServerUrl(root: Record<string, unknown>): string | undefined {\n const servers = root.servers;\n if (!Array.isArray(servers) || servers.length === 0) return undefined;\n const first = servers[0] as Record<string, unknown> | undefined;\n const url = first?.url;\n return typeof url === \"string\" && url.length > 0 ? url : undefined;\n}\n\n/**\n * Record which input fields belong to the path, the query string, or the\n * JSON body. The generated execute() needs this split to build a real request\n * from the same flat input object the agent fills in.\n */\nfunction locateParams(\n operation: Record<string, unknown>,\n sharedParams: unknown[],\n spec: unknown,\n): CandidateTool[\"paramLocations\"] {\n const locations = { path: [] as string[], query: [] as string[], body: [] as string[] };\n\n const parameters = [\n ...sharedParams,\n ...(Array.isArray(operation.parameters) ? operation.parameters : []),\n ];\n for (const rawParam of parameters) {\n const param = deref(rawParam as JsonSchema, spec) as Record<string, unknown>;\n if (typeof param.name !== \"string\") continue;\n if (param.in === \"path\") locations.path.push(param.name);\n if (param.in === \"query\") locations.query.push(param.name);\n }\n\n const body = extractJsonBody(operation.requestBody, spec);\n if (body) {\n if (body.schema.type === \"object\" || body.schema.properties) {\n locations.body.push(...Object.keys(body.schema.properties ?? {}));\n } else {\n locations.body.push(\"body\");\n }\n }\n return locations;\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":[]}
@@ -30,10 +30,34 @@ function dedupeNames(candidates) {
30
30
  }
31
31
  return { names, renames };
32
32
  }
33
+ function stripVersionPrefix(candidates) {
34
+ const VERSION_AT = /^(get|post|put|patch|delete|head|options)-(v\d+)-/;
35
+ const counts = /* @__PURE__ */ new Map();
36
+ for (const { name } of candidates) {
37
+ const match = VERSION_AT.exec(name);
38
+ if (match) counts.set(match[2], (counts.get(match[2]) ?? 0) + 1);
39
+ }
40
+ let shared;
41
+ for (const [version, count] of counts) {
42
+ if (count / candidates.length >= 0.8 && (!shared || count > (counts.get(shared) ?? 0))) {
43
+ shared = version;
44
+ }
45
+ }
46
+ if (!shared) return { names: candidates.map((candidate) => candidate.name) };
47
+ const prefix = new RegExp(`^((?:get|post|put|patch|delete|head|options))-${shared}-`);
48
+ const names = candidates.map(
49
+ ({ name }) => prefix.test(name) ? name.replace(prefix, "$1-") : name
50
+ );
51
+ return {
52
+ names,
53
+ note: `Stripped the shared "${shared}" version prefix from tool names.`
54
+ };
55
+ }
33
56
 
34
57
  export {
35
58
  toToolName,
36
59
  nameFromRoute,
37
- dedupeNames
60
+ dedupeNames,
61
+ stripVersionPrefix
38
62
  };
39
- //# sourceMappingURL=chunk-BIKKPCRT.js.map
63
+ //# sourceMappingURL=chunk-FWSATV7C.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\n/**\n * Strip a shared API version prefix from route-derived names.\n *\n * APIs that version every path (`/v1/trips`, `/v1/users`) would otherwise\n * put \"v1-\" in every single tool name: \"get-v1-trips\", \"post-v1-users\".\n * The prefix carries no information when it is on *every* name, so when at\n * least 80% of names share the same version segment we drop it and tell the\n * report. Runs before dedupe: stripping can create collisions (both\n * `/v1/trips` and `/trips` become \"get-trips\"), and dedupe resolves them.\n *\n * Only the segment right after the method word is considered\n * (\"get-v1-trips\"), so an operationId like \"preview-v2-changes\" is untouched.\n */\nexport function stripVersionPrefix(candidates: { name: string }[]): {\n names: string[];\n note?: string;\n} {\n // Route-derived names always start with an HTTP method (\"get-v1-trips\");\n // an operationId like \"preview-v2-changes\" does not, so it never counts.\n const VERSION_AT = /^(get|post|put|patch|delete|head|options)-(v\\d+)-/;\n\n const counts = new Map<string, number>();\n for (const { name } of candidates) {\n const match = VERSION_AT.exec(name);\n if (match) counts.set(match[2] as string, (counts.get(match[2] as string) ?? 0) + 1);\n }\n\n let shared: string | undefined;\n for (const [version, count] of counts) {\n if (count / candidates.length >= 0.8 && (!shared || count > (counts.get(shared) ?? 0))) {\n shared = version;\n }\n }\n if (!shared) return { names: candidates.map((candidate) => candidate.name) };\n\n const prefix = new RegExp(`^((?:get|post|put|patch|delete|head|options))-${shared}-`);\n const names = candidates.map(({ name }) =>\n prefix.test(name) ? name.replace(prefix, \"$1-\") : name,\n );\n return {\n names,\n note: `Stripped the shared \"${shared}\" version prefix from tool names.`,\n };\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;AAeO,SAAS,mBAAmB,YAGjC;AAGA,QAAM,aAAa;AAEnB,QAAM,SAAS,oBAAI,IAAoB;AACvC,aAAW,EAAE,KAAK,KAAK,YAAY;AACjC,UAAM,QAAQ,WAAW,KAAK,IAAI;AAClC,QAAI,MAAO,QAAO,IAAI,MAAM,CAAC,IAAc,OAAO,IAAI,MAAM,CAAC,CAAW,KAAK,KAAK,CAAC;AAAA,EACrF;AAEA,MAAI;AACJ,aAAW,CAAC,SAAS,KAAK,KAAK,QAAQ;AACrC,QAAI,QAAQ,WAAW,UAAU,QAAQ,CAAC,UAAU,SAAS,OAAO,IAAI,MAAM,KAAK,KAAK;AACtF,eAAS;AAAA,IACX;AAAA,EACF;AACA,MAAI,CAAC,OAAQ,QAAO,EAAE,OAAO,WAAW,IAAI,CAAC,cAAc,UAAU,IAAI,EAAE;AAE3E,QAAM,SAAS,IAAI,OAAO,iDAAiD,MAAM,GAAG;AACpF,QAAM,QAAQ,WAAW;AAAA,IAAI,CAAC,EAAE,KAAK,MACnC,OAAO,KAAK,IAAI,IAAI,KAAK,QAAQ,QAAQ,KAAK,IAAI;AAAA,EACpD;AACA,SAAO;AAAA,IACL;AAAA,IACA,MAAM,wBAAwB,MAAM;AAAA,EACtC;AACF;","names":[]}
@@ -0,0 +1,235 @@
1
+ import {
2
+ CONFIG_FILE_NAMES,
3
+ loadConfig
4
+ } from "./chunk-MJQ5B6HB.js";
5
+ import {
6
+ js
7
+ } from "./chunk-TGOJ3HUE.js";
8
+ import {
9
+ openapi
10
+ } from "./chunk-3LTHWIAP.js";
11
+
12
+ // src/data-file.ts
13
+ import { readFile, writeFile } from "fs/promises";
14
+ import { join } from "path";
15
+ var DATA_FILE_NAME = ".webmcp-codegen.json";
16
+ async function loadDataFile(cwd) {
17
+ try {
18
+ const parsed = JSON.parse(await readFile(join(cwd, DATA_FILE_NAME), "utf8"));
19
+ return parsed && typeof parsed === "object" ? parsed : {};
20
+ } catch {
21
+ return {};
22
+ }
23
+ }
24
+ async function saveDataFile(cwd, patch) {
25
+ const current = await loadDataFile(cwd);
26
+ const next = { ...current, ...patch };
27
+ if (JSON.stringify(next) === JSON.stringify(current)) return;
28
+ await writeFile(join(cwd, DATA_FILE_NAME), `${JSON.stringify(next, null, 2)}
29
+ `, "utf8");
30
+ }
31
+
32
+ // src/detect.ts
33
+ import { readdir } from "fs/promises";
34
+ import { join as join2, relative } from "path";
35
+ var SPEC_FILE_PATTERN = /^(openapi|swagger|api)\.(ya?ml|json)$/i;
36
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([
37
+ "node_modules",
38
+ ".git",
39
+ ".turbo",
40
+ ".next",
41
+ "dist",
42
+ "build",
43
+ "coverage"
44
+ ]);
45
+ var MAX_DEPTH = 5;
46
+ async function findSpecs(cwd) {
47
+ const found = [];
48
+ async function walk(dir, depth) {
49
+ if (depth > MAX_DEPTH) return;
50
+ let entries;
51
+ try {
52
+ entries = await readdir(dir, { withFileTypes: true });
53
+ } catch {
54
+ return;
55
+ }
56
+ for (const entry of entries) {
57
+ if (entry.isDirectory()) {
58
+ if (!IGNORED_DIRS.has(entry.name)) await walk(join2(dir, entry.name), depth + 1);
59
+ } else if (SPEC_FILE_PATTERN.test(entry.name)) {
60
+ found.push({ path: join2(dir, entry.name), depth });
61
+ }
62
+ }
63
+ }
64
+ await walk(cwd, 0);
65
+ return found.sort((a, b) => a.depth - b.depth).map((entry) => relative(cwd, entry.path));
66
+ }
67
+
68
+ // src/setup.ts
69
+ import { existsSync } from "fs";
70
+ import { basename, join as join4 } from "path";
71
+ import { createInterface } from "readline/promises";
72
+
73
+ // src/detect-app.ts
74
+ import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
75
+ import { join as join3 } from "path";
76
+ var FRAMEWORKS = [
77
+ { dep: "next", framework: "next" },
78
+ { dep: "nuxt", framework: "nuxt" },
79
+ { dep: "@sveltejs/kit", framework: "sveltekit" }
80
+ ];
81
+ async function findWebApps(cwd) {
82
+ const packageDirs = await findPackageDirs(cwd);
83
+ const apps = [];
84
+ for (const dir of packageDirs) {
85
+ const pkg = await readPackageJson(join3(cwd, dir));
86
+ if (!pkg) continue;
87
+ const deps = {
88
+ ...pkg.dependencies,
89
+ ...pkg.devDependencies
90
+ };
91
+ const known = FRAMEWORKS.find(({ dep }) => deps[dep]);
92
+ const framework = known?.framework ?? (deps.react && deps.vite ? "vite-react" : void 0);
93
+ if (framework) apps.push({ dir, framework });
94
+ }
95
+ return apps.sort((a, b) => score(b) - score(a));
96
+ function score(app) {
97
+ return (app.framework === "unknown" ? 0 : 10) + (/(^|\/)(web|app|frontend|client)$/.test(app.dir) ? 2 : 0);
98
+ }
99
+ }
100
+ async function findPackageDirs(cwd) {
101
+ const dirs = [];
102
+ const root = await readPackageJson(join3(cwd, ""));
103
+ if (root) {
104
+ dirs.push(".");
105
+ for (const pattern of await workspaceGlobs(cwd, root)) {
106
+ dirs.push(...await expandShallowGlob(cwd, pattern));
107
+ }
108
+ }
109
+ return [...new Set(dirs)];
110
+ }
111
+ async function workspaceGlobs(cwd, rootPkg) {
112
+ const workspaces = rootPkg.workspaces;
113
+ if (Array.isArray(workspaces)) return workspaces;
114
+ if (workspaces && typeof workspaces === "object" && Array.isArray(workspaces.packages)) {
115
+ return workspaces.packages;
116
+ }
117
+ return readPnpmWorkspaceGlobs(cwd);
118
+ }
119
+ async function readPnpmWorkspaceGlobs(cwd) {
120
+ try {
121
+ const text = await readFile2(join3(cwd, "pnpm-workspace.yaml"), "utf8");
122
+ const packagesBlock = /^packages:\s*\n((?:\s+-\s+.+\n?)+)/m.exec(text);
123
+ if (!packagesBlock) return [];
124
+ return [...packagesBlock[1].matchAll(/^\s+-\s+['"]?([^'"\n]+?)['"]?\s*$/gm)].map(
125
+ (match) => match[1]
126
+ );
127
+ } catch {
128
+ return [];
129
+ }
130
+ }
131
+ async function expandShallowGlob(cwd, pattern) {
132
+ const starAt = pattern.indexOf("*");
133
+ const base = starAt === -1 ? pattern : pattern.slice(0, starAt).replace(/\/$/, "");
134
+ if (starAt === -1) return [base];
135
+ try {
136
+ const entries = await readdir2(join3(cwd, base), { withFileTypes: true });
137
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => `${base}/${entry.name}`);
138
+ } catch {
139
+ return [];
140
+ }
141
+ }
142
+ async function readPackageJson(dir) {
143
+ try {
144
+ return JSON.parse(await readFile2(join3(dir, "package.json"), "utf8"));
145
+ } catch {
146
+ return void 0;
147
+ }
148
+ }
149
+
150
+ // src/setup.ts
151
+ async function resolveSetup(cwd, flags) {
152
+ const hasConfigFile = flags.configPath ? existsSync(join4(cwd, flags.configPath)) : CONFIG_FILE_NAMES.some((name) => existsSync(join4(cwd, name)));
153
+ if (hasConfigFile) {
154
+ const { config, path } = await loadConfig(cwd, flags.configPath);
155
+ if (flags.spec || flags.out) {
156
+ console.warn(`Note: --spec/--out are ignored; ${basename(path)} is in charge here.`);
157
+ }
158
+ const data2 = await loadDataFile(cwd);
159
+ const apps = await findWebApps(cwd);
160
+ const app2 = apps.find((candidate) => candidate.dir === data2.app) ?? apps[0];
161
+ return { config, label: basename(path), app: app2, fromConfigFile: true, remember: {} };
162
+ }
163
+ if (flags.configPath) {
164
+ throw new Error(`No config file at "${flags.configPath}".`);
165
+ }
166
+ const data = await loadDataFile(cwd);
167
+ const spec = flags.spec ?? data.spec ?? await detectSpec(cwd);
168
+ let app;
169
+ if (!flags.out) {
170
+ const apps = await findWebApps(cwd);
171
+ const remembered = apps.find((candidate) => candidate.dir === data.app);
172
+ if (remembered) {
173
+ app = remembered;
174
+ } else if (apps.length === 1) {
175
+ app = apps[0];
176
+ console.log(`Found your web app: ${app?.dir} (${app?.framework})`);
177
+ } else if (apps.length > 1) {
178
+ app = await askWhichApp(apps);
179
+ }
180
+ }
181
+ const outDir = flags.out ?? (app && app.dir !== "." ? `${app.dir}/src/webmcp` : "./src/webmcp");
182
+ return {
183
+ config: { sources: [openapi({ spec })], generate: [js({ outDir })] },
184
+ label: flags.spec ? `--spec ${spec}` : `detected ${spec}`,
185
+ app,
186
+ fromConfigFile: false,
187
+ remember: { spec, app: app?.dir }
188
+ };
189
+ }
190
+ async function askWhichApp(apps) {
191
+ if (!process.stdin.isTTY) {
192
+ const first = apps[0];
193
+ console.log(`Several packages look like web apps; using ${first.dir}. Override with --out.`);
194
+ return first;
195
+ }
196
+ console.log("Several packages look like the web app. Which one should the tools live in?");
197
+ apps.forEach((app, index) => {
198
+ console.log(` ${index + 1}. ${app.dir} (${app.framework})${index === 0 ? " [default]" : ""}`);
199
+ });
200
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
201
+ try {
202
+ const answer = await rl.question("Choice [1]: ");
203
+ const picked = Number.parseInt(answer.trim() || "1", 10);
204
+ return apps[picked - 1] ?? apps[0];
205
+ } finally {
206
+ rl.close();
207
+ }
208
+ }
209
+ async function detectSpec(cwd) {
210
+ const specs = await findSpecs(cwd);
211
+ if (specs.length === 0) {
212
+ throw new Error(
213
+ "No OpenAPI spec found in this project.\nPoint at one: npx webmcp-codegen generate --spec path/to/openapi.json"
214
+ );
215
+ }
216
+ if (specs.length > 1) {
217
+ const list = specs.map((spec) => ` - ${spec}`).join("\n");
218
+ throw new Error(
219
+ `Found ${specs.length} API specs:
220
+ ${list}
221
+
222
+ Pick one: npx webmcp-codegen generate --spec ${specs[0]}`
223
+ );
224
+ }
225
+ console.log(`Detected ${specs[0]} (override with --spec)`);
226
+ return specs[0];
227
+ }
228
+
229
+ export {
230
+ loadDataFile,
231
+ saveDataFile,
232
+ findSpecs,
233
+ resolveSetup
234
+ };
235
+ //# sourceMappingURL=chunk-JVBVTHZ7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/data-file.ts","../src/detect.ts","../src/setup.ts","../src/detect-app.ts"],"sourcesContent":["/**\n * The remembered-choices file: `.webmcp-codegen.json` at the project root.\n *\n * It is plain data — never code — so it works in the pure-npx flow (no\n * install needed) and can be read and written safely by the CLI and the dev\n * dashboard alike. It holds two kinds of things:\n *\n * - choices we asked for once and should never ask again\n * (\"which of these packages is your web app?\")\n * - overrides per-tool edits made in the dashboard (description,\n * enabled). They are applied after the safety review, so\n * they survive regeneration.\n *\n * The config file (codegen.config.mjs) stays the source of truth for\n * *structure* (sources, generators, safety). This file is for *choices and\n * tweaks*. Editing it by hand is fine; it is meant to be committed.\n */\n\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { ToolOverrides } from \"./types.js\";\n\nexport const DATA_FILE_NAME = \".webmcp-codegen.json\";\n\nexport interface DataFile {\n /** The spec we used (or were told to use), relative to the project root. */\n spec?: string;\n /** The web app package directory, relative to the project root. */\n app?: string;\n /** Per-tool tweaks, keyed by tool name. */\n overrides?: ToolOverrides;\n}\n\nexport async function loadDataFile(cwd: string): Promise<DataFile> {\n try {\n const parsed = JSON.parse(await readFile(join(cwd, DATA_FILE_NAME), \"utf8\")) as DataFile;\n return parsed && typeof parsed === \"object\" ? parsed : {};\n } catch {\n return {};\n }\n}\n\n/**\n * Merge and write. Only the keys given are touched; everything already in\n * the file (especially overrides) survives. Writes nothing when the merged\n * result equals what's already there, so watch mode never loops on us.\n */\nexport async function saveDataFile(cwd: string, patch: Partial<DataFile>): Promise<void> {\n const current = await loadDataFile(cwd);\n const next: DataFile = { ...current, ...patch };\n if (JSON.stringify(next) === JSON.stringify(current)) return;\n await writeFile(join(cwd, DATA_FILE_NAME), `${JSON.stringify(next, null, 2)}\\n`, \"utf8\");\n}\n","/**\n * Spec auto-detection: the reason `npx webmcp-codegen generate` works with\n * zero arguments, zero config, and zero install.\n *\n * The rule is deliberately boring: walk the project (skipping the obvious\n * noise), recognize the usual spec filenames, and return what we find\n * shallowest-first. When exactly one spec exists we just use it; the CLI\n * layer decides what to do about zero or several.\n */\n\nimport type { Dirent } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join, relative } from \"node:path\";\n\n/** Filenames we recognize as API specs. */\nexport const SPEC_FILE_PATTERN = /^(openapi|swagger|api)\\.(ya?ml|json)$/i;\n\n/** Directories never worth descending into. */\nconst IGNORED_DIRS = new Set([\n \"node_modules\",\n \".git\",\n \".turbo\",\n \".next\",\n \"dist\",\n \"build\",\n \"coverage\",\n]);\n\n/**\n * How deep we look. Enough for monorepo layouts like\n * apps/server/openapi/openapi.json (depth 3) without wandering forever.\n */\nconst MAX_DEPTH = 5;\n\n/**\n * Find API spec files under `cwd`, returned as paths relative to `cwd`,\n * shallowest first. A root-level spec is a likelier intent than one\n * buried six folders deep.\n */\nexport async function findSpecs(cwd: string): Promise<string[]> {\n const found: { path: string; depth: number }[] = [];\n\n async function walk(dir: string, depth: number): Promise<void> {\n if (depth > MAX_DEPTH) return;\n let entries: Dirent[];\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return; // Unreadable directory. Skip it, never die on detection.\n }\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (!IGNORED_DIRS.has(entry.name)) await walk(join(dir, entry.name), depth + 1);\n } else if (SPEC_FILE_PATTERN.test(entry.name)) {\n found.push({ path: join(dir, entry.name), depth });\n }\n }\n }\n\n await walk(cwd, 0);\n return found.sort((a, b) => a.depth - b.depth).map((entry) => relative(cwd, entry.path));\n}\n","/**\n * Setup resolution: where the tools come from and where they go.\n *\n * Shared by the CLI (`generate`) and the dev dashboard (`dev`), so both see\n * the same project the same way:\n *\n * 1. a config file (codegen.config.mjs or --config) for full control\n * 2. --spec/--out flags as quick overrides, no config needed\n * 3. remembered choices from .webmcp-codegen.json\n * 4. auto-detection: the spec by filename, the web app by its package.json\n *\n * Branches 2-4 build the config right here, which is what makes\n * `npx webmcp-codegen generate` work without installing the package: the\n * user's project never has to resolve a webmcp-codegen import.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport { createInterface } from \"node:readline/promises\";\nimport { CONFIG_FILE_NAMES, loadConfig } from \"./config.js\";\nimport { loadDataFile } from \"./data-file.js\";\nimport { findSpecs } from \"./detect.js\";\nimport { findWebApps, type WebApp } from \"./detect-app.js\";\nimport { js } from \"./generators/js.js\";\nimport { openapi } from \"./sources/openapi.js\";\nimport type { CodegenConfig } from \"./types.js\";\n\nexport interface GenerateFlags {\n dryRun: boolean;\n skipAudit: boolean;\n force: boolean;\n watch: boolean;\n configPath?: string;\n spec?: string;\n out?: string;\n}\n\nexport interface Setup {\n config: CodegenConfig;\n label: string;\n /** The web app we detected (when detection ran). Drives placement + wiring. */\n app?: WebApp;\n /** True when the config came from a config file rather than detection. */\n fromConfigFile: boolean;\n /** Choices to remember in .webmcp-codegen.json after a successful run. */\n remember: { spec?: string; app?: string };\n}\n\n/**\n * Where the tools come from and where they go, in priority order:\n *\n * 1. a config file (codegen.config.mjs or --config) for full control\n * 2. --spec/--out flags as quick overrides, no config needed\n * 3. remembered choices from .webmcp-codegen.json\n * 4. auto-detection: the spec by filename, the web app by its package.json\n *\n * Branches 2-4 build the config right here inside the CLI, which is what\n * makes `npx webmcp-codegen generate` work without installing the package:\n * the user's project never has to resolve a webmcp-codegen import.\n */\nexport async function resolveSetup(cwd: string, flags: GenerateFlags): Promise<Setup> {\n const hasConfigFile = flags.configPath\n ? existsSync(join(cwd, flags.configPath))\n : CONFIG_FILE_NAMES.some((name) => existsSync(join(cwd, name)));\n\n if (hasConfigFile) {\n const { config, path } = await loadConfig(cwd, flags.configPath);\n if (flags.spec || flags.out) {\n console.warn(`Note: --spec/--out are ignored; ${basename(path)} is in charge here.`);\n }\n // Wiring still works with a config file if we can find the app.\n const data = await loadDataFile(cwd);\n const apps = await findWebApps(cwd);\n const app = apps.find((candidate) => candidate.dir === data.app) ?? apps[0];\n return { config, label: basename(path), app, fromConfigFile: true, remember: {} };\n }\n if (flags.configPath) {\n throw new Error(`No config file at \"${flags.configPath}\".`);\n }\n\n const data = await loadDataFile(cwd);\n\n // The spec: flag wins, then the remembered choice, then detection.\n const spec = flags.spec ?? data.spec ?? (await detectSpec(cwd));\n\n // The web app: detection decides placement. Only asked once; the answer\n // is remembered in .webmcp-codegen.json.\n let app: WebApp | undefined;\n if (!flags.out) {\n const apps = await findWebApps(cwd);\n const remembered = apps.find((candidate) => candidate.dir === data.app);\n if (remembered) {\n app = remembered;\n } else if (apps.length === 1) {\n app = apps[0];\n console.log(`Found your web app: ${app?.dir} (${app?.framework})`);\n } else if (apps.length > 1) {\n app = await askWhichApp(apps);\n }\n }\n\n const outDir = flags.out ?? (app && app.dir !== \".\" ? `${app.dir}/src/webmcp` : \"./src/webmcp\");\n return {\n config: { sources: [openapi({ spec })], generate: [js({ outDir })] },\n label: flags.spec ? `--spec ${spec}` : `detected ${spec}`,\n app,\n fromConfigFile: false,\n remember: { spec, app: app?.dir },\n };\n}\n\n/**\n * The one question this CLI asks. Several packages look like the web app;\n * a human picks, and .webmcp-codegen.json remembers it. Non-interactive\n * shells (CI) get the best guess with a note, never a hang.\n */\nasync function askWhichApp(apps: WebApp[]): Promise<WebApp> {\n if (!process.stdin.isTTY) {\n const first = apps[0] as WebApp;\n console.log(`Several packages look like web apps; using ${first.dir}. Override with --out.`);\n return first;\n }\n console.log(\"Several packages look like the web app. Which one should the tools live in?\");\n apps.forEach((app, index) => {\n console.log(` ${index + 1}. ${app.dir} (${app.framework})${index === 0 ? \" [default]\" : \"\"}`);\n });\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n try {\n const answer = await rl.question(\"Choice [1]: \");\n const picked = Number.parseInt(answer.trim() || \"1\", 10);\n return apps[picked - 1] ?? (apps[0] as WebApp);\n } finally {\n rl.close();\n }\n}\n\n/**\n * Find the project's API spec. One candidate: use it and say so. Several:\n * list them and make the human pick. None: say exactly what to do next.\n */\nasync function detectSpec(cwd: string): Promise<string> {\n const specs = await findSpecs(cwd);\n\n if (specs.length === 0) {\n throw new Error(\n \"No OpenAPI spec found in this project.\\n\" +\n \"Point at one: npx webmcp-codegen generate --spec path/to/openapi.json\",\n );\n }\n if (specs.length > 1) {\n const list = specs.map((spec) => ` - ${spec}`).join(\"\\n\");\n throw new Error(\n `Found ${specs.length} API specs:\\n${list}\\n\\n` +\n `Pick one: npx webmcp-codegen generate --spec ${specs[0]}`,\n );\n }\n\n console.log(`Detected ${specs[0]} (override with --spec)`);\n return specs[0] as string;\n}\n\n","/**\n * Web-app detection: where the generated tools should live.\n *\n * The tools are browser code, so they belong in whichever package *is* the\n * web app — not next to the spec, and not wherever the command happened to\n * run. In a monorepo like:\n *\n * apps/\n * ├── server/ (has the openapi.json)\n * └── web/ (has next in its package.json) ← tools go here\n *\n * detection means reading package.json files and looking for a browser\n * framework. One candidate: we use it and say so. Several: the CLI asks\n * once and remembers the answer in .webmcp-codegen.json.\n */\n\nimport { readdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nexport interface WebApp {\n /** Package directory relative to the project root, e.g. \"apps/web\". */\n dir: string;\n framework: \"next\" | \"vite-react\" | \"nuxt\" | \"sveltekit\" | \"unknown\";\n}\n\n/** The frameworks we recognize, best-supported first. */\nconst FRAMEWORKS: { dep: string; framework: WebApp[\"framework\"] }[] = [\n { dep: \"next\", framework: \"next\" },\n { dep: \"nuxt\", framework: \"nuxt\" },\n { dep: \"@sveltejs/kit\", framework: \"sveltekit\" },\n];\n\n/**\n * Find web apps in the project. Returns candidates with the likeliest first\n * (a known framework beats \"has react\", an app named \"web\" beats \"admin\").\n */\nexport async function findWebApps(cwd: string): Promise<WebApp[]> {\n const packageDirs = await findPackageDirs(cwd);\n const apps: WebApp[] = [];\n\n for (const dir of packageDirs) {\n const pkg = await readPackageJson(join(cwd, dir));\n if (!pkg) continue;\n const deps = {\n ...(pkg.dependencies as Record<string, string> | undefined),\n ...(pkg.devDependencies as Record<string, string> | undefined),\n };\n const known = FRAMEWORKS.find(({ dep }) => deps[dep]);\n // A bare react+vite pair is a Vite SPA; react alone is too weak a signal.\n const framework =\n known?.framework ?? (deps.react && deps.vite ? (\"vite-react\" as const) : undefined);\n if (framework) apps.push({ dir, framework });\n }\n\n // Prefer known frameworks, then the package literally named like the app.\n return apps.sort((a, b) => score(b) - score(a));\n\n function score(app: WebApp): number {\n return (\n (app.framework === \"unknown\" ? 0 : 10) +\n (/(^|\\/)(web|app|frontend|client)$/.test(app.dir) ? 2 : 0)\n );\n }\n}\n\n/** Every directory holding a package.json, root first. */\nasync function findPackageDirs(cwd: string): Promise<string[]> {\n const dirs: string[] = [];\n const root = await readPackageJson(join(cwd, \"\"));\n if (root) {\n dirs.push(\".\");\n for (const pattern of await workspaceGlobs(cwd, root)) {\n dirs.push(...(await expandShallowGlob(cwd, pattern)));\n }\n }\n return [...new Set(dirs)];\n}\n\n/** Workspace globs from package.json workspaces or pnpm-workspace.yaml. */\nasync function workspaceGlobs(cwd: string, rootPkg: Record<string, unknown>): Promise<string[]> {\n const workspaces = rootPkg.workspaces;\n if (Array.isArray(workspaces)) return workspaces as string[];\n if (\n workspaces &&\n typeof workspaces === \"object\" &&\n Array.isArray((workspaces as { packages?: unknown }).packages)\n ) {\n return (workspaces as { packages: string[] }).packages;\n }\n // pnpm monorepos: parse the \"packages:\" list out of pnpm-workspace.yaml.\n // Kept deliberately shallow: we only support single-star globs anyway.\n return readPnpmWorkspaceGlobs(cwd);\n}\n\nasync function readPnpmWorkspaceGlobs(cwd: string): Promise<string[]> {\n try {\n const text = await readFile(join(cwd, \"pnpm-workspace.yaml\"), \"utf8\");\n const packagesBlock = /^packages:\\s*\\n((?:\\s+-\\s+.+\\n?)+)/m.exec(text);\n if (!packagesBlock) return [];\n return [...(packagesBlock[1] as string).matchAll(/^\\s+-\\s+['\"]?([^'\"\\n]+?)['\"]?\\s*$/gm)].map(\n (match) => match[1] as string,\n );\n } catch {\n return [];\n }\n}\n\n/**\n * Expand a workspace glob, but only one star deep (\"apps/*\"). Deep globs\n * (\"packages/**\") are truncated at the first star; a monorepo app is never\n * buried deeper than that in practice.\n */\nasync function expandShallowGlob(cwd: string, pattern: string): Promise<string[]> {\n const starAt = pattern.indexOf(\"*\");\n const base = starAt === -1 ? pattern : pattern.slice(0, starAt).replace(/\\/$/, \"\");\n if (starAt === -1) return [base];\n try {\n const entries = await readdir(join(cwd, base), { withFileTypes: true });\n return entries.filter((entry) => entry.isDirectory()).map((entry) => `${base}/${entry.name}`);\n } catch {\n return [];\n }\n}\n\nasync function readPackageJson(dir: string): Promise<Record<string, unknown> | undefined> {\n try {\n return JSON.parse(await readFile(join(dir, \"package.json\"), \"utf8\")) as Record<string, unknown>;\n } catch {\n return undefined;\n }\n}\n"],"mappings":"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","names":["join","join","readdir","readFile","join","join","data","app"]}
@@ -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":[]}