webmcp-codegen 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,139 @@
1
+ import {
2
+ nameFromRoute,
3
+ toToolName
4
+ } from "../chunk-BIKKPCRT.js";
5
+ import {
6
+ deepDeref,
7
+ deref,
8
+ pascalCase
9
+ } from "../chunk-5L4KN6F4.js";
10
+
11
+ // src/sources/openapi.ts
12
+ import { readFile } from "fs/promises";
13
+ import { resolve } from "path";
14
+ import { parse as parseYaml } from "yaml";
15
+ var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
16
+ function openapi(options) {
17
+ return {
18
+ kind: "openapi",
19
+ async collect() {
20
+ const specPath = resolve(process.cwd(), options.spec);
21
+ const spec = await readSpec(specPath);
22
+ return operationsFromSpec(spec);
23
+ }
24
+ };
25
+ }
26
+ async function readSpec(specPath) {
27
+ let text;
28
+ try {
29
+ text = await readFile(specPath, "utf8");
30
+ } catch {
31
+ throw new Error(
32
+ `Could not read the OpenAPI spec at "${specPath}". Check the "spec" path in codegen.config.`
33
+ );
34
+ }
35
+ return parseYaml(text);
36
+ }
37
+ function operationsFromSpec(spec) {
38
+ const root = spec;
39
+ const paths = root.paths ?? {};
40
+ const rootSecurity = Array.isArray(root.security) && root.security.length > 0;
41
+ const candidates = [];
42
+ for (const [path, pathItem] of Object.entries(paths)) {
43
+ const sharedParams = Array.isArray(pathItem.parameters) ? pathItem.parameters : [];
44
+ for (const method of HTTP_METHODS) {
45
+ const operation = pathItem[method];
46
+ if (!operation) continue;
47
+ const upperMethod = method.toUpperCase();
48
+ const ref = `${upperMethod} ${path}`;
49
+ const name = typeof operation.operationId === "string" && operation.operationId.length > 0 ? toToolName(operation.operationId) : nameFromRoute(method, path);
50
+ const opSecurity = operation.security;
51
+ const requiresAuth = Array.isArray(opSecurity) ? opSecurity.length > 0 : rootSecurity;
52
+ candidates.push({
53
+ id: ref,
54
+ name,
55
+ source: { kind: "openapi", ref },
56
+ inputSchema: buildInputSchema(operation, sharedParams, spec),
57
+ outputSchema: findOutputSchema(operation, spec),
58
+ inputTypeName: `${pascalCase(name)}Input`,
59
+ httpMethod: upperMethod,
60
+ // The safety layer refines this — the source only reports the verb.
61
+ sideEffect: "unknown",
62
+ requiresAuth,
63
+ description: pickDescription(operation, upperMethod ?? method.toUpperCase(), path),
64
+ descriptionSource: typeof operation.summary === "string" || typeof operation.description === "string" ? "openapi-summary" : "generated-template"
65
+ });
66
+ }
67
+ }
68
+ if (candidates.length === 0) {
69
+ throw new Error('The OpenAPI spec has no operations under "paths". Nothing to generate.');
70
+ }
71
+ return candidates;
72
+ }
73
+ function buildInputSchema(operation, sharedParams, spec) {
74
+ const properties = {};
75
+ const required = /* @__PURE__ */ new Set();
76
+ const parameters = [
77
+ ...sharedParams,
78
+ ...Array.isArray(operation.parameters) ? operation.parameters : []
79
+ ];
80
+ for (const rawParam of parameters) {
81
+ const param = deref(rawParam, spec);
82
+ if (param.in !== "path" && param.in !== "query") continue;
83
+ if (typeof param.name !== "string") continue;
84
+ const fieldSchema = param.schema ? deepDeref(param.schema, spec) : { type: "string" };
85
+ properties[param.name] = typeof param.description === "string" ? { ...fieldSchema, description: param.description } : fieldSchema;
86
+ if (param.in === "path" || param.required === true) required.add(param.name);
87
+ }
88
+ const body = extractJsonBody(operation.requestBody, spec);
89
+ if (body) {
90
+ if (body.schema.type === "object" || body.schema.properties) {
91
+ for (const [key, value] of Object.entries(body.schema.properties ?? {})) {
92
+ properties[key] = value;
93
+ }
94
+ if (body.required) {
95
+ for (const key of body.schema.required ?? []) required.add(key);
96
+ }
97
+ } else {
98
+ properties.body = body.schema;
99
+ if (body.required) required.add("body");
100
+ }
101
+ }
102
+ return { type: "object", properties, required: [...required] };
103
+ }
104
+ function extractJsonBody(requestBody, spec) {
105
+ if (!requestBody || typeof requestBody !== "object") return void 0;
106
+ const body = deref(requestBody, spec);
107
+ const content = body.content;
108
+ const jsonEntry = content?.["application/json"] ?? Object.entries(content ?? {}).find(([mediaType]) => mediaType.endsWith("+json"))?.[1];
109
+ if (!jsonEntry?.schema) return void 0;
110
+ return {
111
+ schema: deepDeref(jsonEntry.schema, spec),
112
+ required: body.required === true
113
+ };
114
+ }
115
+ function findOutputSchema(operation, spec) {
116
+ const responses = operation.responses;
117
+ if (!responses) return void 0;
118
+ for (const [status, rawResponse] of Object.entries(responses)) {
119
+ if (!status.startsWith("2")) continue;
120
+ const response = deref(rawResponse, spec);
121
+ const content = response.content;
122
+ const jsonEntry = content?.["application/json"] ?? Object.entries(content ?? {}).find(([mediaType]) => mediaType.endsWith("+json"))?.[1];
123
+ if (jsonEntry?.schema) return deepDeref(jsonEntry.schema, spec);
124
+ }
125
+ return void 0;
126
+ }
127
+ function pickDescription(operation, method, path) {
128
+ if (typeof operation.summary === "string" && operation.summary.trim().length > 0) {
129
+ return operation.summary.trim();
130
+ }
131
+ if (typeof operation.description === "string" && operation.description.trim().length > 0) {
132
+ return operation.description.trim().split("\n")[0];
133
+ }
134
+ return `${method} ${path}`;
135
+ }
136
+ export {
137
+ openapi
138
+ };
139
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/sources/openapi.ts"],"sourcesContent":["/**\n * The OpenAPI source.\n *\n * Reads an OpenAPI 3.x document (YAML or JSON) and turns every operation\n * into a CandidateTool. This is the highest-reach source: most backend\n * frameworks can already emit an OpenAPI spec, so teams get value without\n * changing any application code.\n *\n * What we read from each operation:\n * - name ← operationId, slugified (falls back to method + path)\n * - description ← summary, else the first line of description, else a template\n * - inputSchema ← path + query parameters merged with the JSON request body\n * - outputSchema ← the first 2xx response's JSON schema, when present\n *\n * Header and cookie parameters are skipped on purpose: agents should not be\n * setting those by hand, and auth headers are the app's job, not the tool's.\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\nimport { nameFromRoute, toToolName } from \"../naming.js\";\nimport { deepDeref, deref, pascalCase } from \"../schema.js\";\nimport type { CandidateTool, JsonSchema, Source } from \"../types.js\";\n\nexport interface OpenApiSourceOptions {\n /** Path to the OpenAPI document, relative to the project root. */\n spec: string;\n}\n\nconst HTTP_METHODS = [\"get\", \"post\", \"put\", \"patch\", \"delete\", \"head\", \"options\"] as const;\n\n/** Create an OpenAPI source for the config's `sources` array. */\nexport function openapi(options: OpenApiSourceOptions): Source {\n return {\n kind: \"openapi\",\n async collect() {\n const specPath = resolve(process.cwd(), options.spec);\n const spec = await readSpec(specPath);\n return operationsFromSpec(spec);\n },\n };\n}\n\nasync function readSpec(specPath: string): Promise<unknown> {\n let text: string;\n try {\n text = await readFile(specPath, \"utf8\");\n } catch {\n throw new Error(\n `Could not read the OpenAPI spec at \"${specPath}\". Check the \"spec\" path in codegen.config.`,\n );\n }\n // YAML is a superset of JSON, so one parser handles both file types.\n return parseYaml(text);\n}\n\nfunction operationsFromSpec(spec: unknown): CandidateTool[] {\n const root = spec as Record<string, unknown>;\n const paths = (root.paths ?? {}) as Record<string, Record<string, unknown>>;\n const rootSecurity = Array.isArray(root.security) && root.security.length > 0;\n const candidates: CandidateTool[] = [];\n\n for (const [path, pathItem] of Object.entries(paths)) {\n // Parameters declared on the path item apply to every operation under it.\n const sharedParams = Array.isArray(pathItem.parameters) ? pathItem.parameters : [];\n\n for (const method of HTTP_METHODS) {\n const operation = pathItem[method] as Record<string, unknown> | undefined;\n if (!operation) continue;\n\n const upperMethod = method.toUpperCase() as CandidateTool[\"httpMethod\"];\n const ref = `${upperMethod} ${path}`;\n const name =\n typeof operation.operationId === \"string\" && operation.operationId.length > 0\n ? toToolName(operation.operationId)\n : nameFromRoute(method, path);\n\n const opSecurity = operation.security;\n const requiresAuth = Array.isArray(opSecurity) ? opSecurity.length > 0 : rootSecurity;\n\n candidates.push({\n id: ref,\n name,\n source: { kind: \"openapi\", ref },\n inputSchema: buildInputSchema(operation, sharedParams, spec),\n outputSchema: findOutputSchema(operation, spec),\n inputTypeName: `${pascalCase(name)}Input`,\n httpMethod: upperMethod,\n // The safety layer refines this — the source only reports the verb.\n sideEffect: \"unknown\",\n requiresAuth,\n description: pickDescription(operation, upperMethod ?? method.toUpperCase(), path),\n descriptionSource:\n typeof operation.summary === \"string\" || typeof operation.description === \"string\"\n ? \"openapi-summary\"\n : \"generated-template\",\n });\n }\n }\n\n if (candidates.length === 0) {\n throw new Error('The OpenAPI spec has no operations under \"paths\". Nothing to generate.');\n }\n return candidates;\n}\n\n/**\n * Merge path + query parameters and the JSON request body into one object\n * schema — that single object is what the agent fills in when calling the tool.\n */\nfunction buildInputSchema(\n operation: Record<string, unknown>,\n sharedParams: unknown[],\n spec: unknown,\n): JsonSchema {\n const properties: Record<string, JsonSchema> = {};\n const required = new Set<string>();\n\n const parameters = [\n ...sharedParams,\n ...(Array.isArray(operation.parameters) ? operation.parameters : []),\n ];\n\n for (const rawParam of parameters) {\n const param = deref(rawParam as JsonSchema, spec) as Record<string, unknown>;\n // Headers and cookies are transport concerns, not tool inputs.\n if (param.in !== \"path\" && param.in !== \"query\") continue;\n if (typeof param.name !== \"string\") continue;\n\n const fieldSchema = param.schema\n ? deepDeref(param.schema as JsonSchema, spec)\n : { type: \"string\" };\n properties[param.name] =\n typeof param.description === \"string\"\n ? { ...fieldSchema, description: param.description }\n : fieldSchema;\n // Path params are always required by definition; query params say so.\n if (param.in === \"path\" || param.required === true) required.add(param.name);\n }\n\n const body = extractJsonBody(operation.requestBody, spec);\n if (body) {\n if (body.schema.type === \"object\" || body.schema.properties) {\n // The common case: an object body flattens into the tool input.\n for (const [key, value] of Object.entries(body.schema.properties ?? {})) {\n properties[key] = value;\n }\n if (body.required) {\n for (const key of body.schema.required ?? []) required.add(key);\n }\n } else {\n // A non-object body (array, raw string, …) goes under a \"body\" field.\n properties.body = body.schema;\n if (body.required) required.add(\"body\");\n }\n }\n\n return { type: \"object\", properties, required: [...required] };\n}\n\n/** Find the operation's JSON request body schema, if it declares one. */\nfunction extractJsonBody(\n requestBody: unknown,\n spec: unknown,\n): { schema: JsonSchema; required: boolean } | undefined {\n if (!requestBody || typeof requestBody !== \"object\") return undefined;\n const body = deref(requestBody as JsonSchema, spec) as Record<string, unknown>;\n const content = body.content as Record<string, { schema?: JsonSchema }> | undefined;\n // Prefer application/json; accept any \"+json\" media type as a fallback.\n const jsonEntry =\n content?.[\"application/json\"] ??\n Object.entries(content ?? {}).find(([mediaType]) => mediaType.endsWith(\"+json\"))?.[1];\n if (!jsonEntry?.schema) return undefined;\n return {\n schema: deepDeref(jsonEntry.schema, spec),\n required: body.required === true,\n };\n}\n\n/** The response contract, used by the safety layer's PII scan (and later, docs). */\nfunction findOutputSchema(\n operation: Record<string, unknown>,\n spec: unknown,\n): JsonSchema | undefined {\n const responses = operation.responses as Record<string, unknown> | undefined;\n if (!responses) return undefined;\n // The first 2xx response with a JSON schema wins.\n for (const [status, rawResponse] of Object.entries(responses)) {\n if (!status.startsWith(\"2\")) continue;\n const response = deref(rawResponse as JsonSchema, spec) as Record<string, unknown>;\n const content = response.content as Record<string, { schema?: JsonSchema }> | undefined;\n const jsonEntry =\n content?.[\"application/json\"] ??\n Object.entries(content ?? {}).find(([mediaType]) => mediaType.endsWith(\"+json\"))?.[1];\n if (jsonEntry?.schema) return deepDeref(jsonEntry.schema, spec);\n }\n return undefined;\n}\n\n/**\n * The description is part of the agent's prompt, so we take the spec's own\n * words when they exist and only fall back to a plain template. Both paths\n * are marked so the audit report shows which descriptions need human love.\n */\nfunction pickDescription(operation: Record<string, unknown>, method: string, path: string): string {\n if (typeof operation.summary === \"string\" && operation.summary.trim().length > 0) {\n return operation.summary.trim();\n }\n if (typeof operation.description === \"string\" && operation.description.trim().length > 0) {\n // Use the first line only — long prose belongs in docs, not in a prompt.\n return operation.description.trim().split(\"\\n\")[0] as string;\n }\n return `${method} ${path}`;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The shared shapes that flow through the codegen pipeline.
3
+ *
4
+ * Every stage of the pipeline speaks in these types:
5
+ *
6
+ * Source → CandidateTool → (safety review) → ReviewedTool → Generator → GeneratedFile
7
+ *
8
+ * A source (OpenAPI today, tRPC/Zod later) only has to produce CandidateTools.
9
+ * A generator only has to turn ReviewedTools into files. Everything in between
10
+ * lives here so the stages stay independent.
11
+ */
12
+ /**
13
+ * A relaxed JSON Schema type. Real-world schemas (especially from OpenAPI)
14
+ * carry keywords we don't model individually, so unknown keywords are allowed
15
+ * through untouched instead of being rejected.
16
+ */
17
+ interface JsonSchema {
18
+ type?: string;
19
+ properties?: Record<string, JsonSchema>;
20
+ required?: string[];
21
+ items?: JsonSchema;
22
+ enum?: unknown[];
23
+ description?: string;
24
+ format?: string;
25
+ nullable?: boolean;
26
+ anyOf?: JsonSchema[];
27
+ oneOf?: JsonSchema[];
28
+ allOf?: JsonSchema[];
29
+ additionalProperties?: boolean | JsonSchema;
30
+ [keyword: string]: unknown;
31
+ }
32
+ /** Which source a candidate came from. Grows as new sources are added. */
33
+ type SourceKind = "openapi" | "trpc" | "zod" | "prisma" | "graphql" | "manual";
34
+ /**
35
+ * What the tool does to the world. The safety layer derives this from the
36
+ * HTTP method plus naming heuristics; it drives both the MCP hints and the
37
+ * risk tier.
38
+ */
39
+ type SideEffect = "read" | "write" | "destructive" | "unknown";
40
+ /** How dangerous the tool is to expose to an agent. */
41
+ type RiskTier = "safe-read" | "write-confirm" | "destructive-confirm";
42
+ /**
43
+ * A tool being considered for generation. Sources produce these; nothing is
44
+ * written to disk until the safety layer has reviewed every candidate.
45
+ */
46
+ interface CandidateTool {
47
+ /** Stable id used for diffing across regenerations. */
48
+ id: string;
49
+ /** The final tool name an agent will see (validated, de-duplicated). */
50
+ name: string;
51
+ /** Where this candidate came from, e.g. `{ kind: "openapi", ref: "GET /orders/{id}" }`. */
52
+ source: {
53
+ kind: SourceKind;
54
+ ref: string;
55
+ };
56
+ /** Always derived from the source contract, never hand-typed. */
57
+ inputSchema: JsonSchema;
58
+ /** Present when the source has response typing. */
59
+ outputSchema?: JsonSchema;
60
+ /** Name of the generated TypeScript input type, e.g. "GetOrderStatusInput". */
61
+ inputTypeName: string;
62
+ httpMethod?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
63
+ sideEffect: SideEffect;
64
+ requiresAuth: boolean;
65
+ /** Where the description text came from — always reviewable before commit. */
66
+ description: string;
67
+ descriptionSource: "openapi-summary" | "generated-template";
68
+ }
69
+ /** The MCP hints the spec defines for a tool, computed by the safety layer. */
70
+ interface ToolHints {
71
+ readOnlyHint: boolean;
72
+ destructiveHint: boolean;
73
+ idempotentHint: boolean;
74
+ }
75
+ /** A candidate after safety review: classified, hinted, and linted. */
76
+ interface ReviewedTool extends CandidateTool {
77
+ riskTier: RiskTier;
78
+ hints: ToolHints;
79
+ /**
80
+ * Output field paths the PII heuristics flagged (e.g. "user.email").
81
+ * These are fields that would leave the page and reach the agent.
82
+ */
83
+ piiInOutput: string[];
84
+ }
85
+ /** One audit finding. Errors block generation (unless --force); warnings don't. */
86
+ interface AuditFinding {
87
+ level: "error" | "warning";
88
+ /** Tool name this finding is about, or undefined for project-level findings. */
89
+ tool?: string;
90
+ message: string;
91
+ }
92
+ /** A file the generator wants to write. */
93
+ interface GeneratedFile {
94
+ /** Absolute path on disk. */
95
+ path: string;
96
+ /** Full new contents. */
97
+ contents: string;
98
+ /** What writing this file would do — used for the report and --dry-run. */
99
+ action: "create" | "update" | "unchanged";
100
+ /**
101
+ * Present when an existing file was edited by hand in the generated region,
102
+ * so we refused to touch it. The new contents go to a `.new` sibling instead.
103
+ */
104
+ conflict?: string;
105
+ }
106
+ /**
107
+ * A source reads an existing contract and produces candidate tools.
108
+ * Create one with a helper like `openapi({ spec: "./openapi.yaml" })`.
109
+ */
110
+ interface Source {
111
+ readonly kind: SourceKind;
112
+ collect(): Promise<CandidateTool[]>;
113
+ }
114
+ /**
115
+ * A generator turns reviewed tools into files.
116
+ * Named after what lands in your repo: `js`, `html`, `react`, `manifest`.
117
+ */
118
+ interface ToolGenerator {
119
+ readonly kind: string;
120
+ generate(tools: ReviewedTool[], cwd: string): Promise<GeneratedFile[]>;
121
+ }
122
+ /** Safety knobs. Everything here extends defaults; nothing is required. */
123
+ interface SafetyOptions {
124
+ /** Extra field names to treat as PII, on top of the built-in list. */
125
+ piiFields?: string[];
126
+ /** Tool names or source refs to skip entirely (substrings, case-insensitive). */
127
+ exclude?: string[];
128
+ }
129
+ /** The config file shape. Create it with `defineConfig` for type checking. */
130
+ interface CodegenConfig {
131
+ sources: Source[];
132
+ generate: ToolGenerator[];
133
+ safety?: SafetyOptions;
134
+ }
135
+
136
+ export type { AuditFinding as A, CodegenConfig as C, GeneratedFile as G, JsonSchema as J, ReviewedTool as R, Source as S, ToolGenerator as T, CandidateTool as a, RiskTier as b, SafetyOptions as c, SideEffect as d, SourceKind as e, ToolHints as f };
package/package.json CHANGED
@@ -1,17 +1,37 @@
1
1
  {
2
2
  "name": "webmcp-codegen",
3
- "version": "0.0.1",
4
- "description": "Generate safe, typed, human-reviewed WebMCP tools from the API contracts you already have (OpenAPI, tRPC, Zod). Under active development.",
3
+ "version": "0.1.0",
4
+ "description": "Generate safe, typed, human-reviewed WebMCP tools from the API contracts you already have (OpenAPI, tRPC, Zod).",
5
+ "license": "MIT",
5
6
  "type": "module",
6
- "main": "./src/index.js",
7
+ "sideEffects": false,
8
+ "bin": {
9
+ "webmcp-codegen": "./dist/cli.js"
10
+ },
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
7
13
  "exports": {
8
- ".": "./src/index.js"
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ },
18
+ "./sources": {
19
+ "types": "./dist/sources/index.d.ts",
20
+ "import": "./dist/sources/index.js"
21
+ },
22
+ "./generators": {
23
+ "types": "./dist/generators/index.d.ts",
24
+ "import": "./dist/generators/index.js"
25
+ }
9
26
  },
10
27
  "files": [
11
- "src"
28
+ "dist"
12
29
  ],
13
- "engines": {
14
- "node": ">=20"
30
+ "scripts": {
31
+ "build": "tsup src/index.ts src/cli.ts src/sources/index.ts src/generators/index.ts --format esm --dts --sourcemap --clean",
32
+ "dev": "tsup src/index.ts src/cli.ts src/sources/index.ts src/generators/index.ts --format esm --dts --sourcemap --watch",
33
+ "test": "vitest run",
34
+ "typecheck": "tsc --noEmit"
15
35
  },
16
36
  "keywords": [
17
37
  "webmcp",
@@ -22,10 +42,18 @@
22
42
  "zod",
23
43
  "model-context-protocol"
24
44
  ],
25
- "license": "MIT",
45
+ "engines": {
46
+ "node": ">=20"
47
+ },
26
48
  "repository": {
27
49
  "type": "git",
28
50
  "url": "git+https://github.com/SouravInsights/groundstate.git",
29
51
  "directory": "packages/codegen"
52
+ },
53
+ "dependencies": {
54
+ "yaml": "^2.8.0"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "^22.20.1"
30
58
  }
31
59
  }
package/src/index.js DELETED
@@ -1,4 +0,0 @@
1
- // webmcp-codegen — under active development.
2
- // This placeholder claims the npm name; the codegen pipeline (sources, safety
3
- // layer, emitters, CLI) lands here. See notes/webmcp-codegen-design.md.
4
- export {};