openapi-mcp-gen 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 openapi-to-mcp contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # openapi-to-mcp
2
+
3
+ Generate a fully working **MCP (Model Context Protocol) server** from any OpenAPI 3.x spec — JSON or YAML, local file or URL.
4
+
5
+ One command turns your REST API into an AI-ready MCP server with proper tool definitions, input validation, authentication, and error handling.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g openapi-to-mcp
11
+ # or use without installing:
12
+ npx openapi-to-mcp ./petstore.yaml
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ # From a local file
19
+ npx openapi-to-mcp ./petstore.yaml --output ./petstore-mcp
20
+
21
+ # From a URL
22
+ npx openapi-to-mcp https://petstore3.swagger.io/api/v3/openapi.json -o ./petstore-mcp
23
+
24
+ # Preview tools without generating files
25
+ npx openapi-to-mcp ./petstore.yaml --list-tools
26
+
27
+ # Dry run (parse only, no output)
28
+ npx openapi-to-mcp ./petstore.yaml --dry-run
29
+ ```
30
+
31
+ ## What Gets Generated
32
+
33
+ Given this input:
34
+
35
+ ```yaml
36
+ # petstore.yaml (OpenAPI 3.0)
37
+ openapi: "3.0.0"
38
+ info:
39
+ title: Petstore
40
+ version: "1.0.0"
41
+ servers:
42
+ - url: https://petstore3.swagger.io/api/v3
43
+ paths:
44
+ /pet/{petId}:
45
+ get:
46
+ operationId: getPetById
47
+ summary: Find pet by ID
48
+ parameters:
49
+ - name: petId
50
+ in: path
51
+ required: true
52
+ schema:
53
+ type: integer
54
+ ```
55
+
56
+ You get a ready-to-run MCP server:
57
+
58
+ ```
59
+ petstore-mcp/
60
+ ├── src/
61
+ │ └── server.ts # MCP server with all tools
62
+ ├── package.json
63
+ ├── tsconfig.json
64
+ ├── .env.example
65
+ └── README.md
66
+ ```
67
+
68
+ Then:
69
+
70
+ ```bash
71
+ cd petstore-mcp
72
+ npm install
73
+ npm run build
74
+ node dist/server.js
75
+ ```
76
+
77
+ ## CLI Reference
78
+
79
+ ```
80
+ openapi-to-mcp <spec> [options]
81
+
82
+ Arguments:
83
+ spec Path or URL to an OpenAPI 3.x spec (JSON or YAML)
84
+
85
+ Options:
86
+ -o, --output <dir> Output directory (default: ./mcp-server)
87
+ --dry-run Parse and show info, do not write files
88
+ --list-tools Print all tools that would be generated, then exit
89
+ -V, --version Show version
90
+ -h, --help Show help
91
+ ```
92
+
93
+ ## Authentication
94
+
95
+ `openapi-to-mcp` auto-detects auth from `securitySchemes` and generates the appropriate environment variable handling.
96
+
97
+ | Scheme | Detected as | Env var | Behaviour |
98
+ |--------|------------|---------|-----------|
99
+ | `http` + `bearer` | Bearer token | `API_BEARER_TOKEN` | `Authorization: Bearer $TOKEN` |
100
+ | `http` + `basic` | Basic auth | `API_BASIC_CREDENTIALS` | `Authorization: Basic $CREDS` |
101
+ | `apiKey` | API key | `API_KEY` | Header / query / cookie |
102
+ | (none) | No auth | — | No auth header added |
103
+
104
+ The generated server exits with a clear error message if a required env var is missing.
105
+
106
+ ## Generated Server
107
+
108
+ The generated `src/server.ts`:
109
+
110
+ - Uses the official `@modelcontextprotocol/sdk` package
111
+ - Runs on **stdio** transport (standard for MCP)
112
+ - Creates **one tool per API endpoint** (named by `operationId` or derived from method + path)
113
+ - Validates inputs via MCP's built-in schema support
114
+ - Calls the real API with `fetch`, including auth
115
+ - Returns human-readable errors on HTTP failures
116
+ - Flattens JSON request body top-level properties into tool parameters for ergonomic use
117
+
118
+ ## OpenAPI Feature Support
119
+
120
+ | Feature | Support |
121
+ |---------|---------|
122
+ | OpenAPI 3.0.x | ✅ |
123
+ | OpenAPI 3.1.x | ✅ |
124
+ | YAML specs | ✅ |
125
+ | JSON specs | ✅ |
126
+ | Remote URL specs | ✅ |
127
+ | Path parameters | ✅ |
128
+ | Query parameters | ✅ |
129
+ | Header parameters | ✅ |
130
+ | Request body (JSON) | ✅ |
131
+ | Bearer auth | ✅ |
132
+ | API key auth | ✅ |
133
+ | Basic auth | ✅ |
134
+ | `$ref` resolution | ✅ (local refs) |
135
+ | Deprecated endpoints | ✅ (annotated) |
136
+ | Duplicate operationIds | ✅ (auto-suffixed) |
137
+
138
+ ## Using with Claude Desktop
139
+
140
+ Add the generated server to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
141
+
142
+ ```json
143
+ {
144
+ "mcpServers": {
145
+ "petstore": {
146
+ "command": "node",
147
+ "args": ["/absolute/path/to/petstore-mcp/dist/server.js"],
148
+ "env": {
149
+ "API_BEARER_TOKEN": "your-token-here"
150
+ }
151
+ }
152
+ }
153
+ }
154
+ ```
155
+
156
+ ## Using with Claude Code
157
+
158
+ ```bash
159
+ claude mcp add petstore -- node /absolute/path/to/petstore-mcp/dist/server.js
160
+ ```
161
+
162
+ With environment variables:
163
+
164
+ ```bash
165
+ claude mcp add petstore \
166
+ -e API_BEARER_TOKEN=your-token \
167
+ -- node /absolute/path/to/petstore-mcp/dist/server.js
168
+ ```
169
+
170
+ ## Example: GitHub API
171
+
172
+ ```bash
173
+ npx openapi-to-mcp https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml \
174
+ --output ./github-mcp
175
+
176
+ cd github-mcp
177
+ npm install && npm run build
178
+ export API_BEARER_TOKEN=ghp_your_token
179
+ node dist/server.js
180
+ ```
181
+
182
+ ## Recommended Models
183
+
184
+ When using your generated MCP server with Claude, these model IDs are confirmed compatible:
185
+
186
+ - `claude-haiku-4.5` — fast, low-cost, great for simple API calls
187
+ - `claude-sonnet-4-6` — balanced performance (recommended default)
188
+ - `claude-opus-4-6` — most capable, best for complex multi-step API workflows
189
+
190
+ ## Contributing
191
+
192
+ Pull requests welcome. The core pipeline is:
193
+
194
+ 1. `src/parser.ts` — loads and parses the OpenAPI spec into a clean domain model
195
+ 2. `src/generator.ts` — maps the parsed spec to template data and writes files
196
+ 3. `src/templates.ts` — Handlebars rendering helpers
197
+ 4. `templates/server.ts.hbs` — the MCP server template
198
+ 5. `templates/package.json.hbs` — generated project package.json template
199
+
200
+ ## License
201
+
202
+ MIT
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,526 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import chalk from "chalk";
6
+ import path4 from "path";
7
+
8
+ // src/parser.ts
9
+ import fs from "fs-extra";
10
+ import yaml from "js-yaml";
11
+ import path from "path";
12
+ var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
13
+ function toCamelCase(str) {
14
+ return str.replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (c) => c.toLowerCase());
15
+ }
16
+ function toSafeIdentifier(str) {
17
+ const cleaned = str.replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_");
18
+ return toCamelCase(cleaned);
19
+ }
20
+ function deriveToolName(method, urlPath, operationId) {
21
+ if (operationId && operationId.trim()) {
22
+ return toSafeIdentifier(operationId.trim());
23
+ }
24
+ const segments = urlPath.split("/").filter(Boolean).map((s) => s.replace(/\{([^}]+)\}/, "By_$1")).map((s) => s.replace(/[^a-zA-Z0-9]/g, "_"));
25
+ const name = [method, ...segments].join("_");
26
+ return toSafeIdentifier(name);
27
+ }
28
+ function resolveRef(spec, ref) {
29
+ const parts = ref.replace(/^#\//, "").split("/");
30
+ let node = spec;
31
+ for (const part of parts) {
32
+ node = node?.[part];
33
+ }
34
+ return node ?? { type: "object" };
35
+ }
36
+ function resolveSchema(spec, schema) {
37
+ if (!schema) return { type: "object" };
38
+ if (schema["$ref"]) return resolveRef(spec, schema["$ref"]);
39
+ return schema;
40
+ }
41
+ function parseParam(spec, raw) {
42
+ const schema = resolveSchema(spec, raw.schema ?? { type: "string" });
43
+ return {
44
+ name: raw.name,
45
+ camelName: toCamelCase(raw.name),
46
+ in: raw.in,
47
+ description: raw.description ?? "",
48
+ required: raw.required ?? false,
49
+ schema
50
+ };
51
+ }
52
+ function detectAuth(spec) {
53
+ const schemes = spec.components?.securitySchemes ?? {};
54
+ for (const [, scheme] of Object.entries(schemes)) {
55
+ if (scheme.type === "http" && scheme.scheme === "bearer") {
56
+ return {
57
+ type: "bearer",
58
+ paramName: "Authorization",
59
+ paramIn: "header",
60
+ envVar: "API_BEARER_TOKEN",
61
+ description: scheme.description ?? "Bearer token authentication"
62
+ };
63
+ }
64
+ if (scheme.type === "http" && scheme.scheme === "basic") {
65
+ return {
66
+ type: "basic",
67
+ paramName: "Authorization",
68
+ paramIn: "header",
69
+ envVar: "API_BASIC_CREDENTIALS",
70
+ description: scheme.description ?? "Basic authentication (base64 user:pass)"
71
+ };
72
+ }
73
+ if (scheme.type === "apiKey") {
74
+ const paramIn = scheme.in ?? "header";
75
+ const paramName = scheme.name ?? "X-API-Key";
76
+ return {
77
+ type: "apiKey",
78
+ paramName,
79
+ paramIn,
80
+ envVar: "API_KEY",
81
+ description: scheme.description ?? `API key sent in ${paramIn} as "${paramName}"`
82
+ };
83
+ }
84
+ }
85
+ return {
86
+ type: "none",
87
+ paramName: "",
88
+ paramIn: "header",
89
+ envVar: "",
90
+ description: "No authentication"
91
+ };
92
+ }
93
+ function parseEndpoint(spec, urlPath, method, op, pathLevelParams) {
94
+ const rawParams = [...pathLevelParams];
95
+ for (const p of op.parameters ?? []) {
96
+ const resolved = p["$ref"] ? resolveRef(spec, p["$ref"]) : p;
97
+ const existing = rawParams.findIndex(
98
+ (x) => x.name === resolved.name && x.in === resolved.in
99
+ );
100
+ if (existing >= 0) rawParams[existing] = resolved;
101
+ else rawParams.push(resolved);
102
+ }
103
+ const parsedParams = rawParams.map((p) => parseParam(spec, p));
104
+ const pathParams = parsedParams.filter((p) => p.in === "path");
105
+ const queryParams = parsedParams.filter((p) => p.in === "query");
106
+ const headerParams = parsedParams.filter((p) => p.in === "header");
107
+ let hasBody = false;
108
+ let bodyRequired = false;
109
+ let bodyContentType = "application/json";
110
+ let bodySchema = null;
111
+ if (op.requestBody) {
112
+ const rb = op.requestBody;
113
+ hasBody = true;
114
+ bodyRequired = rb.required ?? false;
115
+ const contentTypes = Object.keys(rb.content ?? {});
116
+ bodyContentType = contentTypes.find((ct) => ct.includes("json")) ?? contentTypes[0] ?? "application/json";
117
+ const mediaType = rb.content?.[bodyContentType];
118
+ if (mediaType?.schema) {
119
+ bodySchema = resolveSchema(spec, mediaType.schema);
120
+ }
121
+ }
122
+ const allRequired = [
123
+ ...pathParams.filter((p) => p.required).map((p) => p.name),
124
+ ...queryParams.filter((p) => p.required).map((p) => p.name),
125
+ ...headerParams.filter((p) => p.required).map((p) => p.name)
126
+ ];
127
+ if (hasBody && bodyRequired) allRequired.push("body");
128
+ const operationId = op.operationId ?? "";
129
+ const toolName = deriveToolName(method, urlPath, operationId);
130
+ return {
131
+ toolName,
132
+ operationId,
133
+ method,
134
+ path: urlPath,
135
+ summary: op.summary ?? "",
136
+ description: op.description ?? op.summary ?? "",
137
+ tags: op.tags ?? [],
138
+ deprecated: op.deprecated ?? false,
139
+ pathParams,
140
+ queryParams,
141
+ headerParams,
142
+ hasBody,
143
+ bodyRequired,
144
+ bodyContentType,
145
+ bodySchema,
146
+ allRequired
147
+ };
148
+ }
149
+ async function loadSpec(input) {
150
+ let raw;
151
+ if (input.startsWith("http://") || input.startsWith("https://")) {
152
+ const res = await fetch(input);
153
+ if (!res.ok) {
154
+ throw new Error(`Failed to fetch spec: ${res.status} ${res.statusText}`);
155
+ }
156
+ raw = await res.text();
157
+ } else {
158
+ const resolved = path.resolve(input);
159
+ if (!fs.existsSync(resolved)) {
160
+ throw new Error(`Spec file not found: ${resolved}`);
161
+ }
162
+ raw = await fs.readFile(resolved, "utf-8");
163
+ }
164
+ let spec;
165
+ const trimmed = raw.trimStart();
166
+ if (trimmed.startsWith("{")) {
167
+ spec = JSON.parse(raw);
168
+ } else {
169
+ spec = yaml.load(raw);
170
+ }
171
+ if (!spec || typeof spec !== "object") {
172
+ throw new Error("Could not parse spec: result is not an object");
173
+ }
174
+ const s = spec;
175
+ if (!s.openapi) {
176
+ throw new Error("Not a valid OpenAPI 3.x spec (missing 'openapi' field)");
177
+ }
178
+ if (!s.openapi.startsWith("3.")) {
179
+ throw new Error(`Unsupported OpenAPI version: ${s.openapi} (only 3.x is supported)`);
180
+ }
181
+ return s;
182
+ }
183
+ function parseSpec(spec) {
184
+ const endpoints = [];
185
+ for (const [urlPath, pathItem] of Object.entries(spec.paths ?? {})) {
186
+ const pathLevelParams = (pathItem.parameters ?? []).map(
187
+ (p) => p["$ref"] ? resolveRef(spec, p["$ref"]) : p
188
+ );
189
+ for (const method of HTTP_METHODS) {
190
+ const op = pathItem[method];
191
+ if (!op) continue;
192
+ const endpoint = parseEndpoint(spec, urlPath, method, op, pathLevelParams);
193
+ endpoints.push(endpoint);
194
+ }
195
+ }
196
+ const seen = /* @__PURE__ */ new Map();
197
+ for (const ep of endpoints) {
198
+ const count = (seen.get(ep.toolName) ?? 0) + 1;
199
+ seen.set(ep.toolName, count);
200
+ if (count > 1) ep.toolName = `${ep.toolName}_${count}`;
201
+ }
202
+ const baseUrl = spec.servers?.[0]?.url?.replace(/\/$/, "") ?? "https://api.example.com";
203
+ return {
204
+ title: spec.info.title,
205
+ version: spec.info.version,
206
+ description: spec.info.description ?? "",
207
+ baseUrl,
208
+ endpoints,
209
+ auth: detectAuth(spec),
210
+ rawSchemas: spec.components?.schemas ?? {}
211
+ };
212
+ }
213
+
214
+ // src/generator.ts
215
+ import path3 from "path";
216
+ import fs3 from "fs-extra";
217
+
218
+ // src/templates.ts
219
+ import Handlebars from "handlebars";
220
+ import fs2 from "fs-extra";
221
+ import path2 from "path";
222
+ import { fileURLToPath } from "url";
223
+ var __filename = fileURLToPath(import.meta.url);
224
+ var __dirname = path2.dirname(__filename);
225
+ Handlebars.registerHelper("eq", (a, b) => a === b);
226
+ Handlebars.registerHelper("neq", (a, b) => a !== b);
227
+ Handlebars.registerHelper("and", (a, b) => Boolean(a) && Boolean(b));
228
+ Handlebars.registerHelper("or", (a, b) => Boolean(a) || Boolean(b));
229
+ Handlebars.registerHelper("not", (a) => !a);
230
+ Handlebars.registerHelper("gt", (a, b) => a > b);
231
+ Handlebars.registerHelper(
232
+ "json",
233
+ (value) => JSON.stringify(value, null, 2)
234
+ );
235
+ Handlebars.registerHelper(
236
+ "jsonInline",
237
+ (value) => JSON.stringify(value)
238
+ );
239
+ Handlebars.registerHelper(
240
+ "ucFirst",
241
+ (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : ""
242
+ );
243
+ function getTemplatesRoot() {
244
+ const candidates = [
245
+ path2.resolve(__dirname, "..", "templates"),
246
+ path2.resolve(__dirname, "..", "..", "templates")
247
+ ];
248
+ for (const candidate of candidates) {
249
+ if (fs2.existsSync(candidate)) return candidate;
250
+ }
251
+ throw new Error(
252
+ "Templates directory not found. Searched: " + candidates.join(", ")
253
+ );
254
+ }
255
+ function renderTemplate(templateFile, data) {
256
+ const templatesRoot = getTemplatesRoot();
257
+ const templatePath = path2.join(templatesRoot, templateFile);
258
+ if (!fs2.existsSync(templatePath)) {
259
+ throw new Error(`Template file not found: ${templatePath}`);
260
+ }
261
+ const raw = fs2.readFileSync(templatePath, "utf-8");
262
+ const compiled = Handlebars.compile(raw, { noEscape: true });
263
+ return compiled(data);
264
+ }
265
+
266
+ // src/generator.ts
267
+ function buildJsonSchema(schema) {
268
+ if (!schema) return { type: "object" };
269
+ const result = {};
270
+ if (schema.type) result.type = schema.type;
271
+ if (schema.description) result.description = schema.description;
272
+ if (schema.enum) result.enum = schema.enum;
273
+ if (schema.default !== void 0) result.default = schema.default;
274
+ if (schema.format) result.format = schema.format;
275
+ if (schema.minimum !== void 0) result.minimum = schema.minimum;
276
+ if (schema.maximum !== void 0) result.maximum = schema.maximum;
277
+ if (schema.minLength !== void 0) result.minLength = schema.minLength;
278
+ if (schema.maxLength !== void 0) result.maxLength = schema.maxLength;
279
+ if (schema.pattern) result.pattern = schema.pattern;
280
+ if (schema.properties) {
281
+ result.type = result.type ?? "object";
282
+ result.properties = Object.fromEntries(
283
+ Object.entries(schema.properties).map(([k, v]) => [k, buildJsonSchema(v)])
284
+ );
285
+ if (schema.required) result.required = schema.required;
286
+ }
287
+ if (schema.items) {
288
+ result.type = result.type ?? "array";
289
+ result.items = buildJsonSchema(schema.items);
290
+ }
291
+ if (schema.allOf) {
292
+ result.allOf = schema.allOf.map(buildJsonSchema);
293
+ }
294
+ if (schema.oneOf) {
295
+ result.oneOf = schema.oneOf.map(buildJsonSchema);
296
+ }
297
+ if (schema.anyOf) {
298
+ result.anyOf = schema.anyOf.map(buildJsonSchema);
299
+ }
300
+ if (result.type === void 0 && !schema.properties && !schema.items) {
301
+ result.type = "string";
302
+ }
303
+ return result;
304
+ }
305
+ function paramToJsonSchemaProp(param) {
306
+ const base = buildJsonSchema(param.schema);
307
+ if (param.description && !base.description) {
308
+ base.description = param.description;
309
+ }
310
+ return base;
311
+ }
312
+ function buildInputSchema(ep) {
313
+ const properties = {};
314
+ const required = [];
315
+ for (const p of ep.pathParams) {
316
+ properties[p.name] = {
317
+ ...paramToJsonSchemaProp(p),
318
+ description: (p.description ? p.description + " " : "") + `(path parameter)`
319
+ };
320
+ if (p.required) required.push(p.name);
321
+ }
322
+ for (const p of ep.queryParams) {
323
+ properties[p.name] = {
324
+ ...paramToJsonSchemaProp(p),
325
+ description: (p.description ? p.description + " " : "") + `(query parameter)`
326
+ };
327
+ if (p.required) required.push(p.name);
328
+ }
329
+ for (const p of ep.headerParams) {
330
+ properties[p.name] = {
331
+ ...paramToJsonSchemaProp(p),
332
+ description: (p.description ? p.description + " " : "") + `(header parameter)`
333
+ };
334
+ if (p.required) required.push(p.name);
335
+ }
336
+ if (ep.hasBody) {
337
+ if (ep.bodySchema?.properties) {
338
+ for (const [key, propSchema] of Object.entries(ep.bodySchema.properties)) {
339
+ properties[key] = buildJsonSchema(propSchema);
340
+ if (ep.bodySchema.required?.includes(key)) required.push(key);
341
+ }
342
+ } else {
343
+ properties["body"] = ep.bodySchema ? buildJsonSchema(ep.bodySchema) : { type: "object", description: "Request body" };
344
+ if (ep.bodyRequired) required.push("body");
345
+ }
346
+ }
347
+ return { type: "object", properties, required };
348
+ }
349
+ function buildToolData(ep) {
350
+ const inputSchema = buildInputSchema(ep);
351
+ const bodyHasTopLevelProps = Boolean(ep.bodySchema?.properties);
352
+ const bodyProps = bodyHasTopLevelProps ? Object.keys(ep.bodySchema.properties) : [];
353
+ return {
354
+ toolName: ep.toolName,
355
+ description: [ep.summary, ep.description].filter(Boolean).filter((v, i, a) => a.indexOf(v) === i).join(" \u2014 ").slice(0, 1024) || ep.path,
356
+ inputSchemaJson: JSON.stringify(inputSchema, null, 4),
357
+ method: ep.method.toUpperCase(),
358
+ urlPath: ep.path,
359
+ pathParams: ep.pathParams.map((p) => p.name),
360
+ pathReplacements: ep.pathParams.map((p) => ({
361
+ placeholder: `{${p.name}}`,
362
+ paramName: p.name
363
+ })),
364
+ queryParams: ep.queryParams.map((p) => p.name),
365
+ headerParams: ep.headerParams.map((p) => p.name),
366
+ hasBody: ep.hasBody,
367
+ bodyHasTopLevelProps,
368
+ bodyProps,
369
+ bodyContentType: ep.bodyContentType,
370
+ deprecated: ep.deprecated
371
+ };
372
+ }
373
+ async function generate(spec, options) {
374
+ const { outputDir } = options;
375
+ await fs3.ensureDir(outputDir);
376
+ await fs3.ensureDir(path3.join(outputDir, "src"));
377
+ const tools = spec.endpoints.map(buildToolData);
378
+ const serverName = spec.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "openapi-mcp";
379
+ const templateData = {
380
+ serverName,
381
+ serverTitle: spec.title,
382
+ serverVersion: spec.info ? spec.version : "1.0.0",
383
+ serverDescription: spec.description || spec.title,
384
+ baseUrl: spec.baseUrl,
385
+ tools,
386
+ toolCount: tools.length,
387
+ auth: spec.auth,
388
+ hasAuth: spec.auth.type !== "none",
389
+ isBearer: spec.auth.type === "bearer",
390
+ isApiKey: spec.auth.type === "apiKey",
391
+ isBasic: spec.auth.type === "basic",
392
+ apiKeyInHeader: spec.auth.paramIn === "header",
393
+ apiKeyInQuery: spec.auth.paramIn === "query"
394
+ };
395
+ const serverCode = renderTemplate("server.ts.hbs", templateData);
396
+ await fs3.writeFile(path3.join(outputDir, "src", "server.ts"), serverCode, "utf-8");
397
+ const pkgJson = renderTemplate("package.json.hbs", templateData);
398
+ await fs3.writeFile(path3.join(outputDir, "package.json"), pkgJson, "utf-8");
399
+ const tsconfigContent = JSON.stringify(
400
+ {
401
+ compilerOptions: {
402
+ target: "ES2022",
403
+ module: "ESNext",
404
+ moduleResolution: "bundler",
405
+ strict: true,
406
+ esModuleInterop: true,
407
+ skipLibCheck: true,
408
+ outDir: "dist",
409
+ rootDir: "src",
410
+ resolveJsonModule: true
411
+ },
412
+ include: ["src"],
413
+ exclude: ["node_modules", "dist"]
414
+ },
415
+ null,
416
+ 2
417
+ );
418
+ await fs3.writeFile(
419
+ path3.join(outputDir, "tsconfig.json"),
420
+ tsconfigContent,
421
+ "utf-8"
422
+ );
423
+ const envLines = ["# Generated by openapi-to-mcp", ""];
424
+ if (spec.auth.type !== "none") {
425
+ envLines.push(`# ${spec.auth.description}`);
426
+ envLines.push(`${spec.auth.envVar}=`);
427
+ envLines.push("");
428
+ }
429
+ envLines.push("# Override the base URL if needed");
430
+ envLines.push(`BASE_URL=${spec.baseUrl}`);
431
+ envLines.push("");
432
+ await fs3.writeFile(
433
+ path3.join(outputDir, ".env.example"),
434
+ envLines.join("\n"),
435
+ "utf-8"
436
+ );
437
+ const readme = buildGeneratedReadme(spec, serverName, tools.length);
438
+ await fs3.writeFile(path3.join(outputDir, "README.md"), readme, "utf-8");
439
+ }
440
+ function buildGeneratedReadme(spec, serverName, toolCount) {
441
+ const authSection = spec.auth.type === "none" ? "" : `## Authentication
442
+
443
+ Set the \`${spec.auth.envVar}\` environment variable:
444
+
445
+ \`\`\`bash
446
+ export ${spec.auth.envVar}=your_token_here
447
+ \`\`\`
448
+
449
+ ${spec.auth.description}
450
+ `;
451
+ return `# ${spec.title} MCP Server
452
+
453
+ Generated by [openapi-to-mcp](https://github.com/example/openapi-to-mcp).
454
+
455
+ **${spec.description || spec.title}**
456
+
457
+ - **Base URL:** \`${spec.baseUrl}\`
458
+ - **Tools:** ${toolCount}
459
+
460
+ ## Quick Start
461
+
462
+ \`\`\`bash
463
+ npm install
464
+ npm run build
465
+ node dist/server.js
466
+ \`\`\`
467
+
468
+ ${authSection}
469
+ ## Usage with Claude Desktop
470
+
471
+ Add to your \`claude_desktop_config.json\`:
472
+
473
+ \`\`\`json
474
+ {
475
+ "mcpServers": {
476
+ "${serverName}": {
477
+ "command": "node",
478
+ "args": ["/absolute/path/to/${serverName}/dist/server.js"]${spec.auth.type !== "none" ? `,
479
+ "env": {
480
+ "${spec.auth.envVar}": "your_token_here"
481
+ }` : ""}
482
+ }
483
+ }
484
+ }
485
+ \`\`\`
486
+
487
+ ## Available Tools (${toolCount})
488
+
489
+ | Tool | Method | Path | Description |
490
+ |------|--------|------|-------------|
491
+ ${spec.endpoints.map(
492
+ (ep) => `| \`${ep.toolName}\` | \`${ep.method.toUpperCase()}\` | \`${ep.path}\` | ${ep.summary || ep.description || ""} |`
493
+ ).join("\n")}
494
+ `;
495
+ }
496
+
497
+ // src/index.ts
498
+ var program = new Command();
499
+ program.name("mcp-from-openapi").description("Generate a fully working MCP server from an OpenAPI 3.x spec").version("1.0.0").argument("<spec>", "Path or URL to OpenAPI 3.x spec (JSON or YAML)").option("-o, --output <dir>", "Output directory for generated MCP server", "./mcp-server").action(async (specPath, opts) => {
500
+ try {
501
+ console.log(chalk.blue("Loading OpenAPI spec:"), specPath);
502
+ const spec = await loadSpec(specPath);
503
+ const parsed = parseSpec(spec);
504
+ console.log(chalk.green("Found"), `${parsed.endpoints.length} endpoints`);
505
+ if (parsed.auth) {
506
+ console.log(chalk.green("Auth:"), `${parsed.auth.type} (${parsed.auth.envVar})`);
507
+ }
508
+ const outputDir = path4.resolve(opts.output);
509
+ console.log(chalk.blue("Generating MCP server to:"), outputDir);
510
+ await generate(parsed, { outputDir });
511
+ console.log("");
512
+ console.log(chalk.green.bold("MCP server generated successfully!"));
513
+ console.log("");
514
+ console.log("Next steps:");
515
+ console.log(chalk.dim(` cd ${opts.output}`));
516
+ console.log(chalk.dim(" npm install"));
517
+ console.log(chalk.dim(" npm run build"));
518
+ console.log(chalk.dim(" npm start"));
519
+ console.log("");
520
+ } catch (err) {
521
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : err);
522
+ process.exit(1);
523
+ }
524
+ });
525
+ program.parse();
526
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/parser.ts","../src/generator.ts","../src/templates.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport path from \"node:path\";\nimport { loadSpec, parseSpec } from \"./parser.js\";\nimport { generate } from \"./generator.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"mcp-from-openapi\")\n .description(\"Generate a fully working MCP server from an OpenAPI 3.x spec\")\n .version(\"1.0.0\")\n .argument(\"<spec>\", \"Path or URL to OpenAPI 3.x spec (JSON or YAML)\")\n .option(\"-o, --output <dir>\", \"Output directory for generated MCP server\", \"./mcp-server\")\n .action(async (specPath: string, opts: { output: string }) => {\n try {\n console.log(chalk.blue(\"Loading OpenAPI spec:\"), specPath);\n\n const spec = await loadSpec(specPath);\n const parsed = parseSpec(spec);\n\n console.log(chalk.green(\"Found\"), `${parsed.endpoints.length} endpoints`);\n if (parsed.auth) {\n console.log(chalk.green(\"Auth:\"), `${parsed.auth.type} (${parsed.auth.envVar})`);\n }\n\n const outputDir = path.resolve(opts.output);\n console.log(chalk.blue(\"Generating MCP server to:\"), outputDir);\n\n await generate(parsed, { outputDir });\n\n console.log(\"\");\n console.log(chalk.green.bold(\"MCP server generated successfully!\"));\n console.log(\"\");\n console.log(\"Next steps:\");\n console.log(chalk.dim(` cd ${opts.output}`));\n console.log(chalk.dim(\" npm install\"));\n console.log(chalk.dim(\" npm run build\"));\n console.log(chalk.dim(\" npm start\"));\n console.log(\"\");\n } catch (err) {\n console.error(chalk.red(\"Error:\"), err instanceof Error ? err.message : err);\n process.exit(1);\n }\n });\n\nprogram.parse();\n","import fs from \"fs-extra\";\nimport yaml from \"js-yaml\";\nimport path from \"node:path\";\n\n// ─── OpenAPI 3.x types (minimal subset we care about) ────────────────────────\n\nexport interface OpenAPISpec {\n openapi: string;\n info: { title: string; version: string; description?: string };\n servers?: Array<{ url: string; description?: string }>;\n paths?: Record<string, PathItem>;\n components?: {\n schemas?: Record<string, SchemaObject>;\n securitySchemes?: Record<string, SecurityScheme>;\n };\n security?: SecurityRequirement[];\n}\n\nexport type HttpMethod = \"get\" | \"post\" | \"put\" | \"patch\" | \"delete\" | \"head\" | \"options\";\n\nexport interface PathItem {\n get?: OperationObject;\n post?: OperationObject;\n put?: OperationObject;\n patch?: OperationObject;\n delete?: OperationObject;\n head?: OperationObject;\n options?: OperationObject;\n parameters?: ParameterObject[];\n}\n\nexport interface OperationObject {\n operationId?: string;\n summary?: string;\n description?: string;\n tags?: string[];\n parameters?: ParameterObject[];\n requestBody?: RequestBodyObject;\n responses?: Record<string, ResponseObject>;\n security?: SecurityRequirement[];\n deprecated?: boolean;\n}\n\nexport interface ParameterObject {\n name: string;\n in: \"query\" | \"path\" | \"header\" | \"cookie\";\n description?: string;\n required?: boolean;\n schema?: SchemaObject;\n \"$ref\"?: string;\n}\n\nexport interface RequestBodyObject {\n description?: string;\n required?: boolean;\n content: Record<string, { schema?: SchemaObject }>;\n}\n\nexport interface ResponseObject {\n description?: string;\n content?: Record<string, { schema?: SchemaObject }>;\n}\n\nexport interface SchemaObject {\n type?: string;\n format?: string;\n description?: string;\n properties?: Record<string, SchemaObject>;\n items?: SchemaObject;\n required?: string[];\n enum?: unknown[];\n default?: unknown;\n nullable?: boolean;\n allOf?: SchemaObject[];\n oneOf?: SchemaObject[];\n anyOf?: SchemaObject[];\n \"$ref\"?: string;\n additionalProperties?: boolean | SchemaObject;\n minimum?: number;\n maximum?: number;\n minLength?: number;\n maxLength?: number;\n pattern?: string;\n example?: unknown;\n}\n\nexport interface SecurityScheme {\n type: \"apiKey\" | \"http\" | \"oauth2\" | \"openIdConnect\";\n scheme?: string; // for type=http: \"bearer\", \"basic\"\n in?: \"header\" | \"query\" | \"cookie\"; // for type=apiKey\n name?: string; // for type=apiKey\n description?: string;\n flows?: Record<string, unknown>;\n}\n\nexport type SecurityRequirement = Record<string, string[]>;\n\n// ─── Parsed domain types ──────────────────────────────────────────────────────\n\nexport interface ParsedEndpoint {\n /** Sanitised identifier, e.g. \"getPetById\" */\n toolName: string;\n /** Original operationId (may be empty) */\n operationId: string;\n method: HttpMethod;\n path: string;\n summary: string;\n description: string;\n tags: string[];\n deprecated: boolean;\n pathParams: ParsedParam[];\n queryParams: ParsedParam[];\n headerParams: ParsedParam[];\n hasBody: boolean;\n bodyRequired: boolean;\n bodyContentType: string;\n bodySchema: SchemaObject | null;\n allRequired: string[];\n}\n\nexport interface ParsedParam {\n name: string;\n camelName: string;\n in: \"query\" | \"path\" | \"header\" | \"cookie\";\n description: string;\n required: boolean;\n schema: SchemaObject;\n}\n\nexport type AuthType = \"apiKey\" | \"bearer\" | \"basic\" | \"none\";\n\nexport interface ParsedAuth {\n type: AuthType;\n /** For apiKey: header/query/cookie name */\n paramName: string;\n /** For apiKey: where the key is sent */\n paramIn: \"header\" | \"query\" | \"cookie\";\n envVar: string;\n description: string;\n}\n\nexport interface ParsedSpec {\n title: string;\n version: string;\n description: string;\n baseUrl: string;\n endpoints: ParsedEndpoint[];\n auth: ParsedAuth;\n rawSchemas: Record<string, SchemaObject>;\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nconst HTTP_METHODS: HttpMethod[] = [\"get\", \"post\", \"put\", \"patch\", \"delete\", \"head\", \"options\"];\n\nfunction toCamelCase(str: string): string {\n return str\n .replace(/[-_\\s]+(.)/g, (_, c: string) => c.toUpperCase())\n .replace(/^(.)/, (c: string) => c.toLowerCase());\n}\n\nfunction toSafeIdentifier(str: string): string {\n // Replace non-alphanumeric chars with underscores, then camelCase\n const cleaned = str.replace(/[^a-zA-Z0-9_]/g, \"_\").replace(/_+/g, \"_\");\n return toCamelCase(cleaned);\n}\n\nfunction deriveToolName(method: HttpMethod, urlPath: string, operationId?: string): string {\n if (operationId && operationId.trim()) {\n return toSafeIdentifier(operationId.trim());\n }\n // Generate from method + path segments\n const segments = urlPath\n .split(\"/\")\n .filter(Boolean)\n .map((s) => s.replace(/\\{([^}]+)\\}/, \"By_$1\"))\n .map((s) => s.replace(/[^a-zA-Z0-9]/g, \"_\"));\n const name = [method, ...segments].join(\"_\");\n return toSafeIdentifier(name);\n}\n\nfunction resolveRef(spec: OpenAPISpec, ref: string): SchemaObject {\n // Only supports local $ref: \"#/components/schemas/Foo\"\n const parts = ref.replace(/^#\\//, \"\").split(\"/\");\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let node: any = spec;\n for (const part of parts) {\n node = node?.[part];\n }\n return (node as SchemaObject) ?? { type: \"object\" };\n}\n\nfunction resolveSchema(spec: OpenAPISpec, schema?: SchemaObject): SchemaObject {\n if (!schema) return { type: \"object\" };\n if (schema[\"$ref\"]) return resolveRef(spec, schema[\"$ref\"]);\n return schema;\n}\n\nfunction parseParam(spec: OpenAPISpec, raw: ParameterObject): ParsedParam {\n const schema = resolveSchema(spec, raw.schema ?? { type: \"string\" });\n return {\n name: raw.name,\n camelName: toCamelCase(raw.name),\n in: raw.in as ParsedParam[\"in\"],\n description: raw.description ?? \"\",\n required: raw.required ?? false,\n schema,\n };\n}\n\nfunction detectAuth(spec: OpenAPISpec): ParsedAuth {\n const schemes = spec.components?.securitySchemes ?? {};\n\n for (const [, scheme] of Object.entries(schemes)) {\n if (scheme.type === \"http\" && scheme.scheme === \"bearer\") {\n return {\n type: \"bearer\",\n paramName: \"Authorization\",\n paramIn: \"header\",\n envVar: \"API_BEARER_TOKEN\",\n description: scheme.description ?? \"Bearer token authentication\",\n };\n }\n if (scheme.type === \"http\" && scheme.scheme === \"basic\") {\n return {\n type: \"basic\",\n paramName: \"Authorization\",\n paramIn: \"header\",\n envVar: \"API_BASIC_CREDENTIALS\",\n description: scheme.description ?? \"Basic authentication (base64 user:pass)\",\n };\n }\n if (scheme.type === \"apiKey\") {\n const paramIn = (scheme.in ?? \"header\") as \"header\" | \"query\" | \"cookie\";\n const paramName = scheme.name ?? \"X-API-Key\";\n return {\n type: \"apiKey\",\n paramName,\n paramIn,\n envVar: \"API_KEY\",\n description: scheme.description ?? `API key sent in ${paramIn} as \"${paramName}\"`,\n };\n }\n }\n\n return {\n type: \"none\",\n paramName: \"\",\n paramIn: \"header\",\n envVar: \"\",\n description: \"No authentication\",\n };\n}\n\nfunction parseEndpoint(\n spec: OpenAPISpec,\n urlPath: string,\n method: HttpMethod,\n op: OperationObject,\n pathLevelParams: ParameterObject[]\n): ParsedEndpoint {\n // Merge path-level params with operation-level (operation overrides)\n const rawParams: ParameterObject[] = [...pathLevelParams];\n for (const p of op.parameters ?? []) {\n const resolved = p[\"$ref\"]\n ? (resolveRef(spec, p[\"$ref\"]) as ParameterObject)\n : p;\n const existing = rawParams.findIndex(\n (x) => x.name === resolved.name && x.in === resolved.in\n );\n if (existing >= 0) rawParams[existing] = resolved;\n else rawParams.push(resolved);\n }\n\n const parsedParams = rawParams.map((p) => parseParam(spec, p));\n const pathParams = parsedParams.filter((p) => p.in === \"path\");\n const queryParams = parsedParams.filter((p) => p.in === \"query\");\n const headerParams = parsedParams.filter((p) => p.in === \"header\");\n\n // Request body\n let hasBody = false;\n let bodyRequired = false;\n let bodyContentType = \"application/json\";\n let bodySchema: SchemaObject | null = null;\n\n if (op.requestBody) {\n const rb = op.requestBody;\n hasBody = true;\n bodyRequired = rb.required ?? false;\n const contentTypes = Object.keys(rb.content ?? {});\n // Prefer JSON, fall back to first available\n bodyContentType =\n contentTypes.find((ct) => ct.includes(\"json\")) ??\n contentTypes[0] ??\n \"application/json\";\n const mediaType = rb.content?.[bodyContentType];\n if (mediaType?.schema) {\n bodySchema = resolveSchema(spec, mediaType.schema);\n }\n }\n\n // Required list for MCP inputSchema\n const allRequired: string[] = [\n ...pathParams.filter((p) => p.required).map((p) => p.name),\n ...queryParams.filter((p) => p.required).map((p) => p.name),\n ...headerParams.filter((p) => p.required).map((p) => p.name),\n ];\n if (hasBody && bodyRequired) allRequired.push(\"body\");\n\n const operationId = op.operationId ?? \"\";\n const toolName = deriveToolName(method, urlPath, operationId);\n\n return {\n toolName,\n operationId,\n method,\n path: urlPath,\n summary: op.summary ?? \"\",\n description: op.description ?? op.summary ?? \"\",\n tags: op.tags ?? [],\n deprecated: op.deprecated ?? false,\n pathParams,\n queryParams,\n headerParams,\n hasBody,\n bodyRequired,\n bodyContentType,\n bodySchema,\n allRequired,\n };\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\nexport async function loadSpec(input: string): Promise<OpenAPISpec> {\n let raw: string;\n\n if (input.startsWith(\"http://\") || input.startsWith(\"https://\")) {\n const res = await fetch(input);\n if (!res.ok) {\n throw new Error(`Failed to fetch spec: ${res.status} ${res.statusText}`);\n }\n raw = await res.text();\n } else {\n const resolved = path.resolve(input);\n if (!fs.existsSync(resolved)) {\n throw new Error(`Spec file not found: ${resolved}`);\n }\n raw = await fs.readFile(resolved, \"utf-8\");\n }\n\n let spec: unknown;\n const trimmed = raw.trimStart();\n if (trimmed.startsWith(\"{\")) {\n spec = JSON.parse(raw);\n } else {\n spec = yaml.load(raw);\n }\n\n if (!spec || typeof spec !== \"object\") {\n throw new Error(\"Could not parse spec: result is not an object\");\n }\n\n const s = spec as OpenAPISpec;\n if (!s.openapi) {\n throw new Error(\"Not a valid OpenAPI 3.x spec (missing 'openapi' field)\");\n }\n if (!s.openapi.startsWith(\"3.\")) {\n throw new Error(`Unsupported OpenAPI version: ${s.openapi} (only 3.x is supported)`);\n }\n\n return s;\n}\n\nexport function parseSpec(spec: OpenAPISpec): ParsedSpec {\n const endpoints: ParsedEndpoint[] = [];\n\n for (const [urlPath, pathItem] of Object.entries(spec.paths ?? {})) {\n const pathLevelParams = (pathItem.parameters ?? []).map((p) =>\n p[\"$ref\"] ? (resolveRef(spec, p[\"$ref\"]) as ParameterObject) : p\n );\n\n for (const method of HTTP_METHODS) {\n const op = pathItem[method];\n if (!op) continue;\n const endpoint = parseEndpoint(spec, urlPath, method, op, pathLevelParams);\n endpoints.push(endpoint);\n }\n }\n\n // Ensure unique tool names — suffix duplicates with _2, _3, …\n const seen = new Map<string, number>();\n for (const ep of endpoints) {\n const count = (seen.get(ep.toolName) ?? 0) + 1;\n seen.set(ep.toolName, count);\n if (count > 1) ep.toolName = `${ep.toolName}_${count}`;\n }\n\n const baseUrl =\n spec.servers?.[0]?.url?.replace(/\\/$/, \"\") ?? \"https://api.example.com\";\n\n return {\n title: spec.info.title,\n version: spec.info.version,\n description: spec.info.description ?? \"\",\n baseUrl,\n endpoints,\n auth: detectAuth(spec),\n rawSchemas: spec.components?.schemas ?? {},\n };\n}\n","import path from \"node:path\";\nimport fs from \"fs-extra\";\nimport { renderTemplate } from \"./templates.js\";\nimport type { ParsedSpec, ParsedEndpoint, SchemaObject, ParsedParam } from \"./parser.js\";\n\n// ─── JSON Schema builder from OpenAPI schema ──────────────────────────────────\n\nfunction buildJsonSchema(schema: SchemaObject | null): Record<string, unknown> {\n if (!schema) return { type: \"object\" };\n\n const result: Record<string, unknown> = {};\n\n if (schema.type) result.type = schema.type;\n if (schema.description) result.description = schema.description;\n if (schema.enum) result.enum = schema.enum;\n if (schema.default !== undefined) result.default = schema.default;\n if (schema.format) result.format = schema.format;\n if (schema.minimum !== undefined) result.minimum = schema.minimum;\n if (schema.maximum !== undefined) result.maximum = schema.maximum;\n if (schema.minLength !== undefined) result.minLength = schema.minLength;\n if (schema.maxLength !== undefined) result.maxLength = schema.maxLength;\n if (schema.pattern) result.pattern = schema.pattern;\n\n if (schema.properties) {\n result.type = result.type ?? \"object\";\n result.properties = Object.fromEntries(\n Object.entries(schema.properties).map(([k, v]) => [k, buildJsonSchema(v)])\n );\n if (schema.required) result.required = schema.required;\n }\n\n if (schema.items) {\n result.type = result.type ?? \"array\";\n result.items = buildJsonSchema(schema.items);\n }\n\n if (schema.allOf) {\n result.allOf = schema.allOf.map(buildJsonSchema);\n }\n if (schema.oneOf) {\n result.oneOf = schema.oneOf.map(buildJsonSchema);\n }\n if (schema.anyOf) {\n result.anyOf = schema.anyOf.map(buildJsonSchema);\n }\n\n if (result.type === undefined && !schema.properties && !schema.items) {\n result.type = \"string\";\n }\n\n return result;\n}\n\nfunction paramToJsonSchemaProp(param: ParsedParam): Record<string, unknown> {\n const base = buildJsonSchema(param.schema);\n if (param.description && !base.description) {\n base.description = param.description;\n }\n return base;\n}\n\n// ─── Build the full inputSchema for a tool ────────────────────────────────────\n\ninterface ToolInputSchema {\n type: \"object\";\n properties: Record<string, unknown>;\n required: string[];\n}\n\nfunction buildInputSchema(ep: ParsedEndpoint): ToolInputSchema {\n const properties: Record<string, unknown> = {};\n const required: string[] = [];\n\n // Path params\n for (const p of ep.pathParams) {\n properties[p.name] = {\n ...paramToJsonSchemaProp(p),\n description:\n (p.description ? p.description + \" \" : \"\") + `(path parameter)`,\n };\n if (p.required) required.push(p.name);\n }\n\n // Query params\n for (const p of ep.queryParams) {\n properties[p.name] = {\n ...paramToJsonSchemaProp(p),\n description:\n (p.description ? p.description + \" \" : \"\") + `(query parameter)`,\n };\n if (p.required) required.push(p.name);\n }\n\n // Header params (non-auth)\n for (const p of ep.headerParams) {\n properties[p.name] = {\n ...paramToJsonSchemaProp(p),\n description:\n (p.description ? p.description + \" \" : \"\") + `(header parameter)`,\n };\n if (p.required) required.push(p.name);\n }\n\n // Request body\n if (ep.hasBody) {\n if (ep.bodySchema?.properties) {\n // Flatten top-level body properties into the tool input\n for (const [key, propSchema] of Object.entries(ep.bodySchema.properties)) {\n properties[key] = buildJsonSchema(propSchema);\n if (ep.bodySchema.required?.includes(key)) required.push(key);\n }\n } else {\n // Treat body as opaque \"body\" parameter\n properties[\"body\"] = ep.bodySchema\n ? buildJsonSchema(ep.bodySchema)\n : { type: \"object\", description: \"Request body\" };\n if (ep.bodyRequired) required.push(\"body\");\n }\n }\n\n return { type: \"object\", properties, required };\n}\n\n// ─── Template data shapes ─────────────────────────────────────────────────────\n\ninterface PathReplacement {\n /** The literal OpenAPI placeholder, e.g. \"{petId}\" */\n placeholder: string;\n /** The JS arg name, e.g. \"petId\" */\n paramName: string;\n}\n\ninterface ToolData {\n toolName: string;\n description: string;\n inputSchemaJson: string;\n method: string;\n urlPath: string;\n pathParams: string[]; // param names\n pathReplacements: PathReplacement[];\n queryParams: string[]; // param names\n headerParams: string[]; // param names\n hasBody: boolean;\n bodyHasTopLevelProps: boolean;\n bodyProps: string[]; // top-level body prop names (when flattened)\n bodyContentType: string;\n deprecated: boolean;\n}\n\nfunction buildToolData(ep: ParsedEndpoint): ToolData {\n const inputSchema = buildInputSchema(ep);\n const bodyHasTopLevelProps = Boolean(ep.bodySchema?.properties);\n const bodyProps = bodyHasTopLevelProps\n ? Object.keys(ep.bodySchema!.properties!)\n : [];\n\n return {\n toolName: ep.toolName,\n description: [ep.summary, ep.description]\n .filter(Boolean)\n .filter((v, i, a) => a.indexOf(v) === i) // deduplicate\n .join(\" — \")\n .slice(0, 1024) || ep.path,\n inputSchemaJson: JSON.stringify(inputSchema, null, 4),\n method: ep.method.toUpperCase(),\n urlPath: ep.path,\n pathParams: ep.pathParams.map((p) => p.name),\n pathReplacements: ep.pathParams.map((p) => ({\n placeholder: `{${p.name}}`,\n paramName: p.name,\n })),\n queryParams: ep.queryParams.map((p) => p.name),\n headerParams: ep.headerParams.map((p) => p.name),\n hasBody: ep.hasBody,\n bodyHasTopLevelProps,\n bodyProps,\n bodyContentType: ep.bodyContentType,\n deprecated: ep.deprecated,\n };\n}\n\n// ─── Main generator ───────────────────────────────────────────────────────────\n\nexport interface GenerateOptions {\n outputDir: string;\n}\n\nexport async function generate(\n spec: ParsedSpec,\n options: GenerateOptions\n): Promise<void> {\n const { outputDir } = options;\n\n await fs.ensureDir(outputDir);\n await fs.ensureDir(path.join(outputDir, \"src\"));\n\n const tools = spec.endpoints.map(buildToolData);\n\n const serverName = spec.title\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\") || \"openapi-mcp\";\n\n const templateData = {\n serverName,\n serverTitle: spec.title,\n serverVersion: spec.info ? spec.version : \"1.0.0\",\n serverDescription: spec.description || spec.title,\n baseUrl: spec.baseUrl,\n tools,\n toolCount: tools.length,\n auth: spec.auth,\n hasAuth: spec.auth.type !== \"none\",\n isBearer: spec.auth.type === \"bearer\",\n isApiKey: spec.auth.type === \"apiKey\",\n isBasic: spec.auth.type === \"basic\",\n apiKeyInHeader: spec.auth.paramIn === \"header\",\n apiKeyInQuery: spec.auth.paramIn === \"query\",\n };\n\n // Generate src/server.ts\n const serverCode = renderTemplate(\"server.ts.hbs\", templateData);\n await fs.writeFile(path.join(outputDir, \"src\", \"server.ts\"), serverCode, \"utf-8\");\n\n // Generate package.json\n const pkgJson = renderTemplate(\"package.json.hbs\", templateData);\n await fs.writeFile(path.join(outputDir, \"package.json\"), pkgJson, \"utf-8\");\n\n // Generate tsconfig.json\n const tsconfigContent = JSON.stringify(\n {\n compilerOptions: {\n target: \"ES2022\",\n module: \"ESNext\",\n moduleResolution: \"bundler\",\n strict: true,\n esModuleInterop: true,\n skipLibCheck: true,\n outDir: \"dist\",\n rootDir: \"src\",\n resolveJsonModule: true,\n },\n include: [\"src\"],\n exclude: [\"node_modules\", \"dist\"],\n },\n null,\n 2\n );\n await fs.writeFile(\n path.join(outputDir, \"tsconfig.json\"),\n tsconfigContent,\n \"utf-8\"\n );\n\n // Generate .env.example\n const envLines: string[] = [\"# Generated by openapi-to-mcp\", \"\"];\n if (spec.auth.type !== \"none\") {\n envLines.push(`# ${spec.auth.description}`);\n envLines.push(`${spec.auth.envVar}=`);\n envLines.push(\"\");\n }\n envLines.push(\"# Override the base URL if needed\");\n envLines.push(`BASE_URL=${spec.baseUrl}`);\n envLines.push(\"\");\n await fs.writeFile(\n path.join(outputDir, \".env.example\"),\n envLines.join(\"\\n\"),\n \"utf-8\"\n );\n\n // Generate README.md for the generated server\n const readme = buildGeneratedReadme(spec, serverName, tools.length);\n await fs.writeFile(path.join(outputDir, \"README.md\"), readme, \"utf-8\");\n}\n\nfunction buildGeneratedReadme(\n spec: ParsedSpec,\n serverName: string,\n toolCount: number\n): string {\n const authSection =\n spec.auth.type === \"none\"\n ? \"\"\n : `## Authentication\n\nSet the \\`${spec.auth.envVar}\\` environment variable:\n\n\\`\\`\\`bash\nexport ${spec.auth.envVar}=your_token_here\n\\`\\`\\`\n\n${spec.auth.description}\n`;\n\n return `# ${spec.title} MCP Server\n\nGenerated by [openapi-to-mcp](https://github.com/example/openapi-to-mcp).\n\n**${spec.description || spec.title}**\n\n- **Base URL:** \\`${spec.baseUrl}\\`\n- **Tools:** ${toolCount}\n\n## Quick Start\n\n\\`\\`\\`bash\nnpm install\nnpm run build\nnode dist/server.js\n\\`\\`\\`\n\n${authSection}\n## Usage with Claude Desktop\n\nAdd to your \\`claude_desktop_config.json\\`:\n\n\\`\\`\\`json\n{\n \"mcpServers\": {\n \"${serverName}\": {\n \"command\": \"node\",\n \"args\": [\"/absolute/path/to/${serverName}/dist/server.js\"]${spec.auth.type !== \"none\" ? `,\n \"env\": {\n \"${spec.auth.envVar}\": \"your_token_here\"\n }` : \"\"}\n }\n }\n}\n\\`\\`\\`\n\n## Available Tools (${toolCount})\n\n| Tool | Method | Path | Description |\n|------|--------|------|-------------|\n${spec.endpoints\n .map(\n (ep) =>\n `| \\`${ep.toolName}\\` | \\`${ep.method.toUpperCase()}\\` | \\`${ep.path}\\` | ${ep.summary || ep.description || \"\"} |`\n )\n .join(\"\\n\")}\n`;\n}\n","import Handlebars from \"handlebars\";\nimport fs from \"fs-extra\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nHandlebars.registerHelper(\"eq\", (a: unknown, b: unknown) => a === b);\nHandlebars.registerHelper(\"neq\", (a: unknown, b: unknown) => a !== b);\nHandlebars.registerHelper(\"and\", (a: unknown, b: unknown) => Boolean(a) && Boolean(b));\nHandlebars.registerHelper(\"or\", (a: unknown, b: unknown) => Boolean(a) || Boolean(b));\nHandlebars.registerHelper(\"not\", (a: unknown) => !a);\nHandlebars.registerHelper(\"gt\", (a: number, b: number) => a > b);\n\n/** JSON-stringify a value for embedding in a template */\nHandlebars.registerHelper(\"json\", (value: unknown) =>\n JSON.stringify(value, null, 2)\n);\n\n/** JSON-stringify inline (no pretty-print) */\nHandlebars.registerHelper(\"jsonInline\", (value: unknown) =>\n JSON.stringify(value)\n);\n\n/** Upper-case first letter */\nHandlebars.registerHelper(\"ucFirst\", (s: string) =>\n s ? s.charAt(0).toUpperCase() + s.slice(1) : \"\"\n);\n\n// ─── Template resolution ──────────────────────────────────────────────────────\n\nfunction getTemplatesRoot(): string {\n const candidates = [\n path.resolve(__dirname, \"..\", \"templates\"),\n path.resolve(__dirname, \"..\", \"..\", \"templates\"),\n ];\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) return candidate;\n }\n throw new Error(\n \"Templates directory not found. Searched: \" + candidates.join(\", \")\n );\n}\n\nexport function renderTemplate(\n templateFile: string,\n data: Record<string, unknown>\n): string {\n const templatesRoot = getTemplatesRoot();\n const templatePath = path.join(templatesRoot, templateFile);\n\n if (!fs.existsSync(templatePath)) {\n throw new Error(`Template file not found: ${templatePath}`);\n }\n\n const raw = fs.readFileSync(templatePath, \"utf-8\");\n const compiled = Handlebars.compile(raw, { noEscape: true });\n return compiled(data);\n}\n"],"mappings":"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","names":["path","path","fs","fs","path","fs","path","path"]}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "openapi-mcp-gen",
3
+ "version": "1.0.0",
4
+ "description": "Generate a fully working MCP server from an OpenAPI 3.x spec",
5
+ "type": "module",
6
+ "bin": {
7
+ "openapi-to-mcp": "./dist/index.js",
8
+ "mcp-from-openapi": "./dist/index.js"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "files": [
12
+ "dist",
13
+ "templates"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "dev": "tsup --watch",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest",
20
+ "lint": "tsc --noEmit",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "keywords": [
24
+ "mcp",
25
+ "model-context-protocol",
26
+ "openapi",
27
+ "swagger",
28
+ "codegen",
29
+ "cli",
30
+ "generator",
31
+ "ai",
32
+ "llm"
33
+ ],
34
+ "author": "",
35
+ "license": "MIT",
36
+ "dependencies": {
37
+ "chalk": "^5.3.0",
38
+ "commander": "^12.1.0",
39
+ "fs-extra": "^11.2.0",
40
+ "handlebars": "^4.7.8",
41
+ "js-yaml": "^4.1.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/fs-extra": "^11.0.4",
45
+ "@types/js-yaml": "^4.0.9",
46
+ "@types/node": "^20.14.0",
47
+ "tsup": "^8.1.0",
48
+ "typescript": "^5.5.0",
49
+ "vitest": "^1.6.0"
50
+ }
51
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "{{{serverName}}}",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for {{{serverTitle}}} — generated by openapi-to-mcp",
5
+ "type": "module",
6
+ "main": "./dist/server.js",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "start": "node dist/server.js",
10
+ "dev": "node --watch dist/server.js"
11
+ },
12
+ "dependencies": {
13
+ "@modelcontextprotocol/sdk": "^1.0.0",
14
+ "zod": "^3.23.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^20.14.0",
18
+ "typescript": "^5.5.0"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "{{{serverName}}}"
24
+ ],
25
+ "license": "MIT"
26
+ }
@@ -0,0 +1,212 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+
5
+ // ─── Configuration ────────────────────────────────────────────────────────────
6
+
7
+ const BASE_URL = process.env.BASE_URL?.replace(/\/$/, "") ?? "{{{baseUrl}}}";
8
+ {{#if hasAuth}}
9
+
10
+ {{#if isBearer}}
11
+ const BEARER_TOKEN = process.env.{{auth.envVar}};
12
+ if (!BEARER_TOKEN) {
13
+ console.error("Error: {{auth.envVar}} environment variable is required.");
14
+ console.error("Set it to your Bearer token before starting the server.");
15
+ process.exit(1);
16
+ }
17
+ {{/if}}
18
+ {{#if isApiKey}}
19
+ const API_KEY = process.env.{{auth.envVar}};
20
+ if (!API_KEY) {
21
+ console.error("Error: {{auth.envVar}} environment variable is required.");
22
+ console.error("Set it to your API key before starting the server.");
23
+ process.exit(1);
24
+ }
25
+ {{/if}}
26
+ {{#if isBasic}}
27
+ const BASIC_CREDENTIALS = process.env.{{auth.envVar}};
28
+ if (!BASIC_CREDENTIALS) {
29
+ console.error("Error: {{auth.envVar}} environment variable is required.");
30
+ console.error("Set it to base64-encoded 'username:password' before starting the server.");
31
+ process.exit(1);
32
+ }
33
+ {{/if}}
34
+ {{/if}}
35
+
36
+ // ─── HTTP client helpers ──────────────────────────────────────────────────────
37
+
38
+ function buildAuthHeaders(): Record<string, string> {
39
+ {{#if isBearer}}
40
+ return { Authorization: `Bearer ${BEARER_TOKEN}` };
41
+ {{else if isBasic}}
42
+ return { Authorization: `Basic ${BASIC_CREDENTIALS}` };
43
+ {{else}}
44
+ return {};
45
+ {{/if}}
46
+ }
47
+
48
+ {{#if isApiKey}}
49
+ {{#if apiKeyInQuery}}
50
+ function buildAuthQuery(): Record<string, string> {
51
+ return { {{{auth.paramName}}}: API_KEY! };
52
+ }
53
+ {{/if}}
54
+ {{/if}}
55
+
56
+ interface FetchOptions {
57
+ method: string;
58
+ headers?: Record<string, string>;
59
+ body?: string;
60
+ }
61
+
62
+ async function apiFetch(
63
+ urlPath: string,
64
+ options: FetchOptions
65
+ ): Promise<{ ok: boolean; status: number; data: unknown }> {
66
+ const headers: Record<string, string> = {
67
+ "Content-Type": "application/json",
68
+ Accept: "application/json",
69
+ ...buildAuthHeaders(),
70
+ ...(options.headers ?? {}),
71
+ };
72
+ {{#if isApiKey}}
73
+ {{#if apiKeyInHeader}}
74
+ headers["{{{auth.paramName}}}"] = API_KEY!;
75
+ {{/if}}
76
+ {{/if}}
77
+
78
+ const url = new URL(BASE_URL + urlPath);
79
+ {{#if isApiKey}}
80
+ {{#if apiKeyInQuery}}
81
+ const authQuery = buildAuthQuery();
82
+ for (const [k, v] of Object.entries(authQuery)) url.searchParams.set(k, v);
83
+ {{/if}}
84
+ {{/if}}
85
+
86
+ const res = await fetch(url.toString(), {
87
+ method: options.method,
88
+ headers,
89
+ body: options.body,
90
+ });
91
+
92
+ let data: unknown;
93
+ const contentType = res.headers.get("content-type") ?? "";
94
+ if (contentType.includes("application/json")) {
95
+ data = await res.json();
96
+ } else {
97
+ data = await res.text();
98
+ }
99
+
100
+ return { ok: res.ok, status: res.status, data };
101
+ }
102
+
103
+ // ─── Server setup ─────────────────────────────────────────────────────────────
104
+
105
+ const server = new McpServer({
106
+ name: "{{{serverName}}}",
107
+ version: "{{{serverVersion}}}",
108
+ });
109
+
110
+ // ─── Tools ────────────────────────────────────────────────────────────────────
111
+ {{#each tools}}
112
+
113
+ {{#if deprecated}}
114
+ // ⚠ DEPRECATED
115
+ {{/if}}
116
+ // {{method}} {{urlPath}}
117
+ server.tool(
118
+ "{{{toolName}}}",
119
+ "{{{description}}}",
120
+ {{{inputSchemaJson}}},
121
+ async (args) => {
122
+ // Build URL with path parameters
123
+ let urlPath = "{{{urlPath}}}";
124
+ {{#each pathParams}}
125
+ urlPath = urlPath.replace("{{{{}}}}{{{this}}}{{{{}}}}", encodeURIComponent(String(args.{{{this}}})));
126
+ {{/each}}
127
+
128
+ // Build query string
129
+ const query = new URLSearchParams();
130
+ {{#each queryParams}}
131
+ if (args.{{{this}}} !== undefined && args.{{{this}}} !== null) {
132
+ query.set("{{{this}}}", String(args.{{{this}}}));
133
+ }
134
+ {{/each}}
135
+ const queryString = query.toString();
136
+ if (queryString) urlPath += "?" + queryString;
137
+
138
+ // Build extra headers
139
+ const extraHeaders: Record<string, string> = {};
140
+ {{#each headerParams}}
141
+ if (args.{{{this}}} !== undefined) {
142
+ extraHeaders["{{{this}}}"] = String(args.{{{this}}});
143
+ }
144
+ {{/each}}
145
+
146
+ {{#if hasBody}}
147
+ // Build request body
148
+ {{#if bodyHasTopLevelProps}}
149
+ const bodyObj: Record<string, unknown> = {};
150
+ {{#each bodyProps}}
151
+ if (args.{{{this}}} !== undefined) bodyObj["{{{this}}}"] = args.{{{this}}};
152
+ {{/each}}
153
+ const bodyStr = JSON.stringify(bodyObj);
154
+ {{else}}
155
+ const bodyStr = args.body !== undefined ? JSON.stringify(args.body) : undefined;
156
+ {{/if}}
157
+ {{/if}}
158
+
159
+ try {
160
+ const result = await apiFetch(urlPath, {
161
+ method: "{{{method}}}",
162
+ headers: Object.keys(extraHeaders).length > 0 ? extraHeaders : undefined,
163
+ {{#if hasBody}}
164
+ body: bodyStr,
165
+ {{/if}}
166
+ });
167
+
168
+ if (!result.ok) {
169
+ return {
170
+ content: [
171
+ {
172
+ type: "text",
173
+ text: `API error ${result.status}: ${JSON.stringify(result.data, null, 2)}`,
174
+ },
175
+ ],
176
+ isError: true,
177
+ };
178
+ }
179
+
180
+ return {
181
+ content: [
182
+ {
183
+ type: "text",
184
+ text: typeof result.data === "string"
185
+ ? result.data
186
+ : JSON.stringify(result.data, null, 2),
187
+ },
188
+ ],
189
+ };
190
+ } catch (err) {
191
+ const message = err instanceof Error ? err.message : String(err);
192
+ return {
193
+ content: [{ type: "text", text: `Request failed: ${message}` }],
194
+ isError: true,
195
+ };
196
+ }
197
+ }
198
+ );
199
+ {{/each}}
200
+
201
+ // ─── Start ────────────────────────────────────────────────────────────────────
202
+
203
+ async function main() {
204
+ const transport = new StdioServerTransport();
205
+ await server.connect(transport);
206
+ console.error("{{{serverTitle}}} MCP server running on stdio");
207
+ }
208
+
209
+ main().catch((err) => {
210
+ console.error("Fatal error:", err);
211
+ process.exit(1);
212
+ });