mcp-api-translator 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,2330 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/ir/validate.ts
4
+ var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "PUT", "POST", "DELETE", "PATCH", "OPTIONS", "HEAD", "TRACE"]);
5
+ var PARAM_LOCATIONS = /* @__PURE__ */ new Set(["path", "query", "header", "cookie"]);
6
+ var SCHEME_TYPES = /* @__PURE__ */ new Set(["apiKey", "http", "oauth2", "openIdConnect", "mutualTLS"]);
7
+ var TOOL_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
8
+ var MAX_TOOL_NAME_LENGTH = 64;
9
+ function isPlainObject(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+ function validateApiModel(model) {
13
+ const issues = [];
14
+ if (typeof model.title !== "string" || model.title.trim() === "") {
15
+ issues.push("title must be a non-empty string");
16
+ }
17
+ if (typeof model.version !== "string") {
18
+ issues.push("version must be a string");
19
+ }
20
+ if (!Array.isArray(model.servers)) {
21
+ issues.push("servers must be an array");
22
+ } else {
23
+ model.servers.forEach((s, i) => {
24
+ if (typeof s !== "string" || s.trim() === "")
25
+ issues.push(`servers[${i}] must be a non-empty string`);
26
+ });
27
+ }
28
+ if (!Array.isArray(model.securitySchemes)) {
29
+ issues.push("securitySchemes must be an array");
30
+ } else {
31
+ model.securitySchemes.forEach((s, i) => {
32
+ const at = `securitySchemes[${i}]`;
33
+ if (!s || typeof s.name !== "string" || s.name === "")
34
+ issues.push(`${at}.name must be a non-empty string`);
35
+ if (!SCHEME_TYPES.has(s?.type))
36
+ issues.push(`${at}.type "${s?.type}" is not a known security scheme type`);
37
+ if (!Array.isArray(s?.envVars) || s.envVars.length === 0) {
38
+ issues.push(`${at}.envVars must be a non-empty array`);
39
+ } else if (!s.envVars.every((v) => typeof v === "string" && v !== "")) {
40
+ issues.push(`${at}.envVars must contain only non-empty strings`);
41
+ }
42
+ });
43
+ }
44
+ if (!Array.isArray(model.operations)) {
45
+ issues.push("operations must be an array");
46
+ } else {
47
+ model.operations.forEach((op, i) => {
48
+ const at = `operations[${i}]`;
49
+ if (typeof op?.toolName !== "string" || !TOOL_NAME_RE.test(op.toolName)) {
50
+ issues.push(
51
+ `${at}.toolName "${op?.toolName}" is not a valid identifier (^[A-Za-z_][A-Za-z0-9_]*$)`
52
+ );
53
+ } else if (op.toolName.length > MAX_TOOL_NAME_LENGTH) {
54
+ issues.push(`${at}.toolName exceeds ${MAX_TOOL_NAME_LENGTH} characters`);
55
+ }
56
+ if (typeof op?.method !== "string" || !HTTP_METHODS.has(op.method)) {
57
+ issues.push(`${at}.method "${op?.method}" is not a supported HTTP method`);
58
+ }
59
+ if (typeof op?.path !== "string" || !op.path.startsWith("/")) {
60
+ issues.push(`${at}.path "${op?.path}" must be a string starting with "/"`);
61
+ }
62
+ if (op?.summary !== void 0 && typeof op.summary !== "string") {
63
+ issues.push(`${at}.summary must be a string when present`);
64
+ }
65
+ if (op?.description !== void 0 && typeof op.description !== "string") {
66
+ issues.push(`${at}.description must be a string when present`);
67
+ }
68
+ if (!Array.isArray(op?.parameters)) {
69
+ issues.push(`${at}.parameters must be an array`);
70
+ } else {
71
+ op.parameters.forEach((p, j) => {
72
+ const pat = `${at}.parameters[${j}]`;
73
+ if (typeof p?.name !== "string" || p.name === "")
74
+ issues.push(`${pat}.name must be a non-empty string`);
75
+ if (!PARAM_LOCATIONS.has(p?.in))
76
+ issues.push(`${pat}.in "${p?.in}" is not a valid parameter location`);
77
+ if (!isPlainObject(p?.schema)) issues.push(`${pat}.schema must be an object`);
78
+ });
79
+ }
80
+ if (op?.requestBody !== void 0 && !isPlainObject(op.requestBody?.schema)) {
81
+ issues.push(`${at}.requestBody.schema must be an object`);
82
+ }
83
+ if (!Array.isArray(op?.security)) {
84
+ issues.push(`${at}.security must be an array`);
85
+ }
86
+ });
87
+ }
88
+ return issues;
89
+ }
90
+ function assertValidApiModel(model) {
91
+ const issues = validateApiModel(model);
92
+ if (issues.length > 0) {
93
+ throw new Error(
94
+ `Parsed API model failed validation:
95
+ ${issues.map((i) => ` - ${i}`).join("\n")}`
96
+ );
97
+ }
98
+ }
99
+
100
+ // src/parsers/index.ts
101
+ import { readFile } from "fs/promises";
102
+ import { parse as parseYaml } from "yaml";
103
+
104
+ // src/parsers/openapi.ts
105
+ import { dereference } from "@readme/openapi-parser";
106
+
107
+ // src/curation/naming.ts
108
+ var MAX_NAME_LENGTH = 64;
109
+ function sanitizeToolName(raw) {
110
+ let name = raw.replace(/[^a-zA-Z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
111
+ if (name.length === 0) name = "operation";
112
+ if (/^[0-9]/.test(name)) name = `op_${name}`;
113
+ if (name.length > MAX_NAME_LENGTH) name = name.slice(0, MAX_NAME_LENGTH).replace(/_+$/, "");
114
+ return name;
115
+ }
116
+ function nameFromMethodPath(method, path3) {
117
+ const segments = path3.split("/").filter(Boolean).map((seg) => {
118
+ const m = seg.match(/^\{(.+)\}$/);
119
+ return m ? `by_${m[1]}` : seg;
120
+ });
121
+ return sanitizeToolName(`${method.toLowerCase()}_${segments.join("_")}`);
122
+ }
123
+ function uniqueToolName(base, taken) {
124
+ const sanitized = sanitizeToolName(base);
125
+ if (!taken.has(sanitized)) {
126
+ taken.add(sanitized);
127
+ return sanitized;
128
+ }
129
+ let i = 2;
130
+ while (taken.has(`${sanitized}_${i}`)) i++;
131
+ const result = `${sanitized}_${i}`;
132
+ taken.add(result);
133
+ return result;
134
+ }
135
+
136
+ // src/parsers/security.ts
137
+ function envPrefix(schemeName) {
138
+ return schemeName.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase() || "AUTH";
139
+ }
140
+ function envNamespace(source) {
141
+ return source.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase() || "API";
142
+ }
143
+ function friendlyEnvVars(raw) {
144
+ if (raw.type === "http" && raw.scheme?.toLowerCase() === "basic") {
145
+ return ["API_USERNAME", "API_PASSWORD"];
146
+ }
147
+ if (raw.tokenUrl && (raw.type === "oauth2" || raw.type === "openIdConnect")) {
148
+ return ["API_CLIENT_ID", "API_CLIENT_SECRET"];
149
+ }
150
+ if (raw.type === "http") return ["API_TOKEN"];
151
+ if (raw.type === "oauth2" || raw.type === "openIdConnect") return ["API_TOKEN"];
152
+ return ["API_KEY"];
153
+ }
154
+ function assignEnvVars(rawSchemes) {
155
+ const used = /* @__PURE__ */ new Set();
156
+ return rawSchemes.map((raw) => {
157
+ const friendly = friendlyEnvVars(raw);
158
+ const collides = friendly.some((v) => used.has(v));
159
+ const envVars = collides ? friendly.map((v) => `${envPrefix(raw.name)}_${v.replace(/^API_/, "")}`) : friendly;
160
+ for (const v of envVars) used.add(v);
161
+ return {
162
+ name: raw.name,
163
+ type: raw.type,
164
+ in: raw.in,
165
+ paramName: raw.paramName,
166
+ scheme: raw.scheme,
167
+ tokenUrl: raw.tokenUrl,
168
+ envVars
169
+ };
170
+ });
171
+ }
172
+
173
+ // src/parsers/openapi.ts
174
+ var HTTP_METHODS2 = ["get", "put", "post", "delete", "patch", "options", "head", "trace"];
175
+ function normalizeSchema(schema, isV30) {
176
+ if (!schema || typeof schema !== "object") return schema ?? {};
177
+ const out = Array.isArray(schema) ? [...schema] : { ...schema };
178
+ if (isV30 && out.nullable === true) {
179
+ if (typeof out.type === "string") out.type = [out.type, "null"];
180
+ else if (Array.isArray(out.type) && !out.type.includes("null"))
181
+ out.type = [...out.type, "null"];
182
+ delete out.nullable;
183
+ } else if (isV30 && out.nullable === false) {
184
+ delete out.nullable;
185
+ }
186
+ if (out.properties && typeof out.properties === "object") {
187
+ const props = {};
188
+ for (const [k, v] of Object.entries(out.properties)) props[k] = normalizeSchema(v, isV30);
189
+ out.properties = props;
190
+ }
191
+ if (out.items) out.items = normalizeSchema(out.items, isV30);
192
+ if (out.additionalProperties && typeof out.additionalProperties === "object") {
193
+ out.additionalProperties = normalizeSchema(out.additionalProperties, isV30);
194
+ }
195
+ for (const key of ["allOf", "anyOf", "oneOf"]) {
196
+ if (Array.isArray(out[key])) out[key] = out[key].map((s) => normalizeSchema(s, isV30));
197
+ }
198
+ return out;
199
+ }
200
+ function serversFrom(doc) {
201
+ if (Array.isArray(doc.servers) && doc.servers.length > 0) {
202
+ return doc.servers.map((s) => String(s.url)).filter(Boolean);
203
+ }
204
+ if (doc.host) {
205
+ const scheme = Array.isArray(doc.schemes) && doc.schemes.length > 0 ? doc.schemes[0] : "https";
206
+ return [`${scheme}://${doc.host}${doc.basePath ?? ""}`];
207
+ }
208
+ return [];
209
+ }
210
+ function paramFrom(p, isV30) {
211
+ const schema = p.schema ? normalizeSchema(p.schema, isV30) : normalizeSchema(
212
+ {
213
+ type: p.type,
214
+ format: p.format,
215
+ enum: p.enum,
216
+ items: p.items,
217
+ default: p.default
218
+ },
219
+ isV30
220
+ );
221
+ return {
222
+ name: String(p.name),
223
+ in: p.in ?? "query",
224
+ required: Boolean(p.required) || p.in === "path",
225
+ description: p.description,
226
+ schema
227
+ };
228
+ }
229
+ function requestBodyFrom(op, params, isV30) {
230
+ if (op.requestBody?.content) {
231
+ const content = op.requestBody.content;
232
+ const contentType = Object.keys(content).find((c) => c.includes("json")) ?? Object.keys(content)[0];
233
+ if (contentType) {
234
+ return {
235
+ required: Boolean(op.requestBody.required),
236
+ contentType,
237
+ description: op.requestBody.description,
238
+ schema: normalizeSchema(content[contentType]?.schema ?? {}, isV30)
239
+ };
240
+ }
241
+ }
242
+ const body = params.find((p) => p.in === "body");
243
+ if (body?.schema) {
244
+ return {
245
+ required: Boolean(body.required),
246
+ contentType: "application/json",
247
+ description: body.description,
248
+ schema: normalizeSchema(body.schema, isV30)
249
+ };
250
+ }
251
+ return void 0;
252
+ }
253
+ function securitySchemesFrom(doc) {
254
+ const defs = doc.components?.securitySchemes ?? doc.securityDefinitions ?? {};
255
+ const raw = Object.entries(defs).map(([name, def]) => {
256
+ const d = def;
257
+ let type = d.type;
258
+ let scheme = d.scheme;
259
+ if (d.type === "basic") {
260
+ type = "http";
261
+ scheme = "basic";
262
+ }
263
+ const tokenUrl = typeof d.flows?.clientCredentials?.tokenUrl === "string" ? d.flows.clientCredentials.tokenUrl : d.flow === "application" && typeof d.tokenUrl === "string" ? d.tokenUrl : void 0;
264
+ return {
265
+ name,
266
+ type,
267
+ in: d.in,
268
+ paramName: d.name,
269
+ scheme,
270
+ tokenUrl
271
+ };
272
+ });
273
+ return assignEnvVars(raw);
274
+ }
275
+ async function parseOpenApi(raw) {
276
+ const doc = await dereference(
277
+ raw,
278
+ { resolve: { external: false } }
279
+ );
280
+ const isV30 = typeof doc.openapi === "string" ? doc.openapi.startsWith("3.0") : !!doc.swagger;
281
+ const securitySchemes = securitySchemesFrom(doc);
282
+ const rootSecurity = securityNames(doc.security);
283
+ const operations = [];
284
+ const paths = doc.paths ?? {};
285
+ for (const [path3, pathItemRaw] of Object.entries(paths)) {
286
+ const pathItem = pathItemRaw;
287
+ const pathLevelParams = Array.isArray(pathItem.parameters) ? pathItem.parameters : [];
288
+ for (const method of HTTP_METHODS2) {
289
+ const op = pathItem[method];
290
+ if (!op) continue;
291
+ const rawParams = [
292
+ ...pathLevelParams,
293
+ ...Array.isArray(op.parameters) ? op.parameters : []
294
+ ];
295
+ const parameters = rawParams.filter((p) => p.in && p.in !== "body" && p.in !== "formData").map((p) => paramFrom(p, isV30));
296
+ const baseName = op.operationId ? sanitizeToolName(op.operationId) : nameFromMethodPath(method, path3);
297
+ operations.push({
298
+ toolName: baseName,
299
+ operationId: op.operationId,
300
+ method: method.toUpperCase(),
301
+ path: path3,
302
+ summary: typeof op.summary === "string" ? op.summary : void 0,
303
+ description: typeof op.description === "string" ? op.description : void 0,
304
+ tags: Array.isArray(op.tags) ? op.tags.map(String) : [],
305
+ parameters,
306
+ requestBody: requestBodyFrom(op, rawParams, isV30),
307
+ security: op.security ? securityNames(op.security) : rootSecurity
308
+ });
309
+ }
310
+ }
311
+ return {
312
+ title: doc.info?.title ?? "API",
313
+ version: doc.info?.version ?? "0.0.0",
314
+ description: doc.info?.description,
315
+ servers: serversFrom(doc),
316
+ securitySchemes,
317
+ operations,
318
+ sourceFormat: "openapi"
319
+ };
320
+ }
321
+ function securityNames(security) {
322
+ if (!Array.isArray(security)) return [];
323
+ const names = /* @__PURE__ */ new Set();
324
+ for (const requirement of security) {
325
+ if (requirement && typeof requirement === "object") {
326
+ for (const key of Object.keys(requirement)) names.add(key);
327
+ }
328
+ }
329
+ return [...names];
330
+ }
331
+
332
+ // src/parsers/postman.ts
333
+ var SKIP_HEADERS = /* @__PURE__ */ new Set(["content-type", "accept", "authorization"]);
334
+ function asDescription(value) {
335
+ if (typeof value === "string") return value;
336
+ if (value && typeof value === "object" && typeof value.content === "string") {
337
+ return value.content;
338
+ }
339
+ return void 0;
340
+ }
341
+ function inferSchema(value) {
342
+ if (value === null) return {};
343
+ if (Array.isArray(value)) return { type: "array", items: inferSchema(value[0] ?? {}) };
344
+ switch (typeof value) {
345
+ case "string":
346
+ return { type: "string" };
347
+ case "number":
348
+ return { type: Number.isInteger(value) ? "integer" : "number" };
349
+ case "boolean":
350
+ return { type: "boolean" };
351
+ case "object": {
352
+ const properties = {};
353
+ for (const [k, v] of Object.entries(value)) properties[k] = inferSchema(v);
354
+ return { type: "object", properties };
355
+ }
356
+ default:
357
+ return {};
358
+ }
359
+ }
360
+ function rawUrl(url) {
361
+ if (typeof url === "string") {
362
+ return { path: normalizePath(url), pathVars: [], query: [] };
363
+ }
364
+ const segments = Array.isArray(url?.path) ? url.path.map(String) : [];
365
+ const path3 = normalizePath("/" + segments.join("/"));
366
+ return {
367
+ path: path3,
368
+ pathVars: Array.isArray(url?.variable) ? url.variable : [],
369
+ query: Array.isArray(url?.query) ? url.query : []
370
+ };
371
+ }
372
+ function normalizePath(p) {
373
+ let path3 = p.replace(/^https?:\/\/[^/]+/i, "");
374
+ path3 = path3.replace(/\?.*$/, "");
375
+ path3 = path3.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
376
+ if (!path3.startsWith("/")) path3 = "/" + path3;
377
+ return path3 || "/";
378
+ }
379
+ function pathParamNames(path3) {
380
+ return [...path3.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]);
381
+ }
382
+ function mapAuth(auth) {
383
+ if (!auth?.type) return null;
384
+ switch (auth.type) {
385
+ case "bearer":
386
+ return { name: "bearerAuth", type: "http", scheme: "bearer" };
387
+ case "basic":
388
+ return { name: "basicAuth", type: "http", scheme: "basic" };
389
+ case "apikey": {
390
+ const entries = Array.isArray(auth.apikey) ? auth.apikey : [];
391
+ const keyEntry = entries.find((e) => e.key === "key");
392
+ const inEntry = entries.find((e) => e.key === "in");
393
+ return {
394
+ name: "apiKeyAuth",
395
+ type: "apiKey",
396
+ in: inEntry?.value ?? "header",
397
+ paramName: keyEntry?.value ?? "X-API-Key"
398
+ };
399
+ }
400
+ case "oauth2":
401
+ return { name: "oauth2", type: "oauth2" };
402
+ default:
403
+ return null;
404
+ }
405
+ }
406
+ function parsePostman(raw) {
407
+ const doc = raw;
408
+ const operations = [];
409
+ const rawSchemes = /* @__PURE__ */ new Map();
410
+ const collectionAuth = mapAuth(doc.auth);
411
+ if (collectionAuth) rawSchemes.set(collectionAuth.name, collectionAuth);
412
+ const walk = (items, tags) => {
413
+ for (const item of items ?? []) {
414
+ if (Array.isArray(item.item)) {
415
+ walk(item.item, [...tags, String(item.name)]);
416
+ continue;
417
+ }
418
+ const req = item.request;
419
+ if (!req) continue;
420
+ const method = String(req.method ?? "GET").toUpperCase();
421
+ const { path: path3, pathVars, query } = rawUrl(req.url);
422
+ const effectiveAuth = mapAuth(req.auth) ?? collectionAuth;
423
+ if (req.auth) {
424
+ const s = mapAuth(req.auth);
425
+ if (s) rawSchemes.set(s.name, s);
426
+ }
427
+ const parameters = [];
428
+ for (const name2 of pathParamNames(path3)) {
429
+ const v = pathVars.find((pv) => pv.key === name2);
430
+ parameters.push({
431
+ name: name2,
432
+ in: "path",
433
+ required: true,
434
+ description: v?.description,
435
+ schema: { type: "string" }
436
+ });
437
+ }
438
+ for (const q of query) {
439
+ if (q.disabled) continue;
440
+ parameters.push({
441
+ name: String(q.key),
442
+ in: "query",
443
+ required: false,
444
+ description: q.description,
445
+ schema: { type: "string" }
446
+ });
447
+ }
448
+ for (const h of Array.isArray(req.header) ? req.header : []) {
449
+ if (h.disabled || SKIP_HEADERS.has(String(h.key).toLowerCase())) continue;
450
+ parameters.push({
451
+ name: String(h.key),
452
+ in: "header",
453
+ required: false,
454
+ description: h.description,
455
+ schema: { type: "string" }
456
+ });
457
+ }
458
+ const requestBody = bodyFrom(req.body);
459
+ const name = item.name ? sanitizeToolName(String(item.name)) : nameFromMethodPath(method, path3);
460
+ operations.push({
461
+ toolName: name,
462
+ operationId: item.name ? String(item.name) : void 0,
463
+ method,
464
+ path: path3,
465
+ summary: typeof item.name === "string" ? item.name : void 0,
466
+ description: asDescription(req.description) ?? asDescription(item.description),
467
+ tags,
468
+ parameters,
469
+ requestBody,
470
+ security: effectiveAuth ? [effectiveAuth.name] : []
471
+ });
472
+ }
473
+ };
474
+ walk(Array.isArray(doc.item) ? doc.item : [], []);
475
+ const securitySchemes = assignEnvVars(
476
+ [...rawSchemes.values()].filter((s) => s !== null)
477
+ );
478
+ return {
479
+ title: doc.info?.name ?? "API",
480
+ version: "1.0.0",
481
+ description: typeof doc.info?.description === "string" ? doc.info.description : void 0,
482
+ servers: [],
483
+ // Postman base URLs typically use {{variables}}; resolved via API_BASE_URL.
484
+ securitySchemes,
485
+ operations,
486
+ sourceFormat: "postman"
487
+ };
488
+ }
489
+ function bodyFrom(body) {
490
+ if (!body || !body.mode) return void 0;
491
+ if (body.mode === "raw" && typeof body.raw === "string" && body.raw.trim()) {
492
+ try {
493
+ return {
494
+ required: false,
495
+ contentType: "application/json",
496
+ schema: inferSchema(JSON.parse(body.raw))
497
+ };
498
+ } catch {
499
+ return { required: false, contentType: "text/plain", schema: { type: "string" } };
500
+ }
501
+ }
502
+ if (body.mode === "urlencoded" || body.mode === "formdata") {
503
+ const entries = Array.isArray(body[body.mode]) ? body[body.mode] : [];
504
+ const properties = {};
505
+ for (const e of entries) if (e.key) properties[String(e.key)] = { type: "string" };
506
+ return {
507
+ required: false,
508
+ contentType: body.mode === "urlencoded" ? "application/x-www-form-urlencoded" : "multipart/form-data",
509
+ schema: { type: "object", properties }
510
+ };
511
+ }
512
+ return void 0;
513
+ }
514
+
515
+ // src/parsers/index.ts
516
+ var MAX_SPEC_BYTES = 16 * 1024 * 1024;
517
+ async function loadRawSpec(input) {
518
+ let text2;
519
+ if (input.spec && input.spec.trim().length > 0) {
520
+ text2 = input.spec;
521
+ } else if (input.specPath) {
522
+ text2 = await readFile(input.specPath, "utf8");
523
+ } else {
524
+ throw new Error("Provide either `spec` (inline text) or `specPath` (a local file path).");
525
+ }
526
+ const bytes = Buffer.byteLength(text2, "utf8");
527
+ if (bytes > MAX_SPEC_BYTES) {
528
+ throw new Error(
529
+ `Spec is ${bytes} bytes, exceeding the ${MAX_SPEC_BYTES}-byte limit. Split it or raise MAX_SPEC_BYTES.`
530
+ );
531
+ }
532
+ try {
533
+ return parseYaml(text2);
534
+ } catch (err) {
535
+ throw new Error(`Could not parse spec as JSON or YAML: ${err.message}`);
536
+ }
537
+ }
538
+ function detectFormat(raw) {
539
+ if (raw && typeof raw === "object") {
540
+ const obj = raw;
541
+ if ("openapi" in obj || "swagger" in obj) return "openapi";
542
+ const info = obj.info;
543
+ const schema = typeof info?.schema === "string" ? info.schema : "";
544
+ if (schema.includes("getpostman.com") || "_postman_id" in (info ?? {}) || Array.isArray(obj.item)) {
545
+ return "postman";
546
+ }
547
+ }
548
+ throw new Error(
549
+ 'Could not detect the spec format. Pass `format: "openapi"` or `format: "postman"` explicitly.'
550
+ );
551
+ }
552
+ async function parseSource(input) {
553
+ const raw = await loadRawSpec(input);
554
+ const format = !input.format || input.format === "auto" ? detectFormat(raw) : input.format;
555
+ let model;
556
+ switch (format) {
557
+ case "openapi":
558
+ model = await parseOpenApi(raw);
559
+ break;
560
+ case "postman":
561
+ model = parsePostman(raw);
562
+ break;
563
+ default:
564
+ throw new Error(`Unsupported format: ${format}`);
565
+ }
566
+ assertValidApiModel(model);
567
+ return model;
568
+ }
569
+ var SUPPORTED_FORMATS = ["openapi", "postman"];
570
+
571
+ // src/emitters/project.ts
572
+ import { mkdir, writeFile as writeFile2, readFile as readFile3, readdir, access } from "fs/promises";
573
+ import path2 from "path";
574
+
575
+ // src/curation/describe.ts
576
+ var MAX_DESCRIPTION_LENGTH = 1024;
577
+ function truncate(text2, max) {
578
+ if (text2.length <= max) return text2;
579
+ return `${text2.slice(0, max - 1).trimEnd()}\u2026`;
580
+ }
581
+ function buildDescription(op) {
582
+ const parts = [];
583
+ const headline = op.summary?.trim() || op.description?.trim();
584
+ if (headline) parts.push(headline);
585
+ else parts.push(`${op.method} ${op.path}`);
586
+ if (op.description && op.description.trim() && op.description.trim() !== headline) {
587
+ parts.push(op.description.trim());
588
+ }
589
+ parts.push(`Calls ${op.method} ${op.path}.`);
590
+ const documentedParams = op.parameters.filter((p) => p.description?.trim());
591
+ if (documentedParams.length > 0) {
592
+ const lines = documentedParams.map(
593
+ (p) => `- ${p.name} (${p.in}${p.required ? ", required" : ""}): ${p.description.trim()}`
594
+ );
595
+ parts.push(`Parameters:
596
+ ${lines.join("\n")}`);
597
+ }
598
+ return truncate(parts.join("\n\n"), MAX_DESCRIPTION_LENGTH);
599
+ }
600
+
601
+ // src/curation/filter.ts
602
+ function globToRegExp(glob) {
603
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
604
+ const pattern = escaped.replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(//g, ".*");
605
+ return new RegExp(`^${pattern}$`);
606
+ }
607
+ function applyFilters(operations, opts) {
608
+ const includeTags = opts.includeTags?.map((t) => t.toLowerCase());
609
+ const exclude = opts.excludeOperations?.map((t) => t.toLowerCase());
610
+ const methods = opts.methods?.map((m) => m.toUpperCase());
611
+ const pathRe = opts.pathGlob ? globToRegExp(opts.pathGlob) : void 0;
612
+ return operations.filter((op) => {
613
+ if (methods && !methods.includes(op.method.toUpperCase())) return false;
614
+ if (pathRe && !pathRe.test(op.path)) return false;
615
+ if (includeTags && includeTags.length > 0) {
616
+ const opTags = op.tags.map((t) => t.toLowerCase());
617
+ if (!opTags.some((t) => includeTags.includes(t))) return false;
618
+ }
619
+ if (exclude && exclude.length > 0) {
620
+ const candidates = [op.operationId, op.toolName, ...op.tags].filter((v) => Boolean(v)).map((v) => v.toLowerCase());
621
+ if (candidates.some((c) => exclude.includes(c))) return false;
622
+ }
623
+ return true;
624
+ });
625
+ }
626
+
627
+ // src/curation/index.ts
628
+ var TOOL_COUNT_WARN_THRESHOLD = 40;
629
+ function curate(model, filters, reservedNames = /* @__PURE__ */ new Set()) {
630
+ const before = model.operations.length;
631
+ const kept = applyFilters(model.operations, filters);
632
+ kept.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
633
+ for (const op of kept) {
634
+ op.toolName = uniqueToolName(op.toolName, reservedNames);
635
+ }
636
+ return { operations: kept, filteredOut: before - kept.length };
637
+ }
638
+
639
+ // src/manifest.ts
640
+ import { readFile as readFile2, writeFile } from "fs/promises";
641
+ import path from "path";
642
+ var MANIFEST_FILENAME = ".mcp-translator.json";
643
+ var MANIFEST_VERSION = 1;
644
+ function manifestPath(projectDir) {
645
+ return path.join(projectDir, MANIFEST_FILENAME);
646
+ }
647
+ async function readManifest(projectDir) {
648
+ try {
649
+ const text2 = await readFile2(manifestPath(projectDir), "utf8");
650
+ return JSON.parse(text2);
651
+ } catch (err) {
652
+ if (err.code === "ENOENT") return null;
653
+ throw err;
654
+ }
655
+ }
656
+ function operationKey(method, opPath) {
657
+ return `${method.toUpperCase()} ${opPath}`;
658
+ }
659
+
660
+ // src/version.ts
661
+ var GENERATOR_VERSION = "0.1.0";
662
+ var GENERATOR_NAME = "mcp-api-translator";
663
+
664
+ // src/emitters/templates.ts
665
+ var GENERATED_BANNER = `// Generated by ${GENERATOR_NAME} v${GENERATOR_VERSION}. Safe to edit: tool files are
666
+ // preserved on re-generation/append unless --force is used.`;
667
+ function packageJson(name, version, transport) {
668
+ const scripts = {
669
+ build: "tsc",
670
+ start: "node dist/index.js"
671
+ };
672
+ if (transport === "http" || transport === "both") {
673
+ scripts["start:http"] = "node dist/index.http.js";
674
+ }
675
+ const pkg = {
676
+ name,
677
+ version,
678
+ private: true,
679
+ type: "module",
680
+ bin: { [name]: "dist/index.js" },
681
+ scripts,
682
+ dependencies: { "@modelcontextprotocol/sdk": "^1.29.0" },
683
+ devDependencies: { typescript: "^5.6.0", "@types/node": "^22.0.0" }
684
+ };
685
+ return JSON.stringify(pkg, null, 2) + "\n";
686
+ }
687
+ function tsconfigJson() {
688
+ return JSON.stringify(
689
+ {
690
+ compilerOptions: {
691
+ target: "ES2022",
692
+ module: "NodeNext",
693
+ moduleResolution: "NodeNext",
694
+ lib: ["ES2022"],
695
+ strict: true,
696
+ esModuleInterop: true,
697
+ skipLibCheck: true,
698
+ outDir: "dist",
699
+ rootDir: "src",
700
+ declaration: false,
701
+ sourceMap: true
702
+ },
703
+ include: ["src"]
704
+ },
705
+ null,
706
+ 2
707
+ ) + "\n";
708
+ }
709
+ function gitignore() {
710
+ return ["node_modules/", "dist/", ".env", "*.log", ""].join("\n");
711
+ }
712
+ function typesTs() {
713
+ return [
714
+ GENERATED_BANNER,
715
+ "",
716
+ "export interface ToolDef {",
717
+ " name: string;",
718
+ " description: string;",
719
+ " inputSchema: Record<string, unknown>;",
720
+ " handler: (args: Record<string, unknown>) => Promise<string>;",
721
+ "}",
722
+ ""
723
+ ].join("\n");
724
+ }
725
+ function configTs(defaultBaseUrl) {
726
+ return [
727
+ GENERATED_BANNER,
728
+ "",
729
+ "export const config = {",
730
+ ` baseUrl: process.env.API_BASE_URL ?? ${JSON.stringify(defaultBaseUrl)},`,
731
+ "};",
732
+ ""
733
+ ].join("\n");
734
+ }
735
+ function commentSafe(value) {
736
+ return value.replace(/\s+/g, " ").trim();
737
+ }
738
+ function authBlock(scheme) {
739
+ const lines = [];
740
+ const guard = ` if (security.includes(${JSON.stringify(scheme.name)})) {`;
741
+ const label = commentSafe(
742
+ `${scheme.name} (${scheme.type}${scheme.scheme ? "/" + scheme.scheme : ""})`
743
+ );
744
+ lines.push(` // ${label}`);
745
+ lines.push(guard);
746
+ if (scheme.type === "apiKey") {
747
+ const env = scheme.envVars[0];
748
+ const param = scheme.paramName ?? "X-API-Key";
749
+ lines.push(` const value = process.env[${JSON.stringify(env)}];`);
750
+ lines.push(" if (value) {");
751
+ if (scheme.in === "query") {
752
+ lines.push(` url.searchParams.set(${JSON.stringify(param)}, value);`);
753
+ } else if (scheme.in === "cookie") {
754
+ lines.push(
755
+ ` headers["cookie"] = (headers["cookie"] ? headers["cookie"] + "; " : "") + ${JSON.stringify(
756
+ param + "="
757
+ )} + value;`
758
+ );
759
+ } else {
760
+ lines.push(` headers[${JSON.stringify(param)}] = value;`);
761
+ }
762
+ lines.push(" }");
763
+ } else if (scheme.type === "http" && scheme.scheme?.toLowerCase() === "basic") {
764
+ const [u, p] = scheme.envVars;
765
+ lines.push(` const user = process.env[${JSON.stringify(u)}];`);
766
+ lines.push(` const pass = process.env[${JSON.stringify(p)}];`);
767
+ lines.push(" if (user && pass) {");
768
+ lines.push(
769
+ ' headers["authorization"] = "Basic " + Buffer.from(user + ":" + pass).toString("base64");'
770
+ );
771
+ lines.push(" }");
772
+ } else if (scheme.tokenUrl) {
773
+ const [id, secret] = scheme.envVars;
774
+ lines.push(` const clientId = process.env[${JSON.stringify(id)}];`);
775
+ lines.push(` const clientSecret = process.env[${JSON.stringify(secret)}];`);
776
+ lines.push(" if (clientId && clientSecret) {");
777
+ lines.push(
778
+ ` const token = await getClientCredentialsToken(${JSON.stringify(scheme.tokenUrl)}, clientId, clientSecret);`
779
+ );
780
+ lines.push(' headers["authorization"] = "Bearer " + token;');
781
+ lines.push(" }");
782
+ } else {
783
+ const env = scheme.envVars[0];
784
+ lines.push(` const value = process.env[${JSON.stringify(env)}];`);
785
+ lines.push(' if (value) headers["authorization"] = "Bearer " + value;');
786
+ }
787
+ lines.push(" }");
788
+ return lines;
789
+ }
790
+ function oauthHelperTs() {
791
+ return [
792
+ "const _tokenCache = new Map<string, { token: string; expiresAt: number }>();",
793
+ "",
794
+ "async function getClientCredentialsToken(",
795
+ " tokenUrl: string,",
796
+ " clientId: string,",
797
+ " clientSecret: string,",
798
+ "): Promise<string> {",
799
+ ' const key = tokenUrl + "|" + clientId;',
800
+ " const hit = _tokenCache.get(key);",
801
+ " if (hit && hit.expiresAt > Date.now()) return hit.token;",
802
+ " const body = new URLSearchParams({",
803
+ ' grant_type: "client_credentials",',
804
+ " client_id: clientId,",
805
+ " client_secret: clientSecret,",
806
+ " }).toString();",
807
+ " const res = await fetch(tokenUrl, {",
808
+ ' method: "POST",',
809
+ ' headers: { "content-type": "application/x-www-form-urlencoded" },',
810
+ " body,",
811
+ " });",
812
+ " const text = await res.text();",
813
+ " if (!res.ok)",
814
+ ' throw new Error("OAuth token request failed: HTTP " + res.status + " " + text.slice(0, 300));',
815
+ " const json = JSON.parse(text) as { access_token?: string; expires_in?: number };",
816
+ ' if (!json.access_token) throw new Error("OAuth token response had no access_token");',
817
+ ' const ttl = typeof json.expires_in === "number" ? json.expires_in : 3600;',
818
+ " _tokenCache.set(key, { token: json.access_token, expiresAt: Date.now() + ttl * 1000 - 30000 });",
819
+ " return json.access_token;",
820
+ "}",
821
+ ""
822
+ ];
823
+ }
824
+ function authTs(schemes) {
825
+ const body = schemes.flatMap(authBlock);
826
+ const hasOAuth = schemes.some((s) => s.tokenUrl);
827
+ const signature = hasOAuth ? [
828
+ "export async function applyAuth(",
829
+ " headers: Record<string, string>,",
830
+ " url: URL,",
831
+ " security: string[],",
832
+ "): Promise<void> {"
833
+ ] : [
834
+ "export function applyAuth(",
835
+ " headers: Record<string, string>,",
836
+ " url: URL,",
837
+ " security: string[],",
838
+ "): void {"
839
+ ];
840
+ return [
841
+ GENERATED_BANNER,
842
+ "",
843
+ "// Injects credentials read from the environment. Secrets are never embedded.",
844
+ ...hasOAuth ? oauthHelperTs() : [],
845
+ ...signature,
846
+ ...body.length > 0 ? body : [" // No security schemes detected on the source API."],
847
+ "}",
848
+ ""
849
+ ].join("\n");
850
+ }
851
+ function httpClientTs() {
852
+ return [
853
+ GENERATED_BANNER,
854
+ "",
855
+ 'import { config } from "../config.js";',
856
+ 'import { applyAuth } from "../auth.js";',
857
+ "",
858
+ "export interface RequestPlan {",
859
+ " method: string;",
860
+ " pathTemplate: string;",
861
+ " pathParams: string[];",
862
+ " queryParams: string[];",
863
+ " headerParams: string[];",
864
+ " cookieParams: string[];",
865
+ " bodyParam: string | null;",
866
+ " contentType: string | null;",
867
+ " security: string[];",
868
+ "}",
869
+ "",
870
+ "function joinUrl(base: string, path: string): string {",
871
+ ' if (!base) throw new Error("API base URL is not set. Set API_BASE_URL in the environment.");',
872
+ ' return base.replace(/\\/+$/, "") + "/" + path.replace(/^\\/+/, "");',
873
+ "}",
874
+ "",
875
+ "export async function callOperation(",
876
+ " plan: RequestPlan,",
877
+ " args: Record<string, unknown>,",
878
+ "): Promise<string> {",
879
+ " let path = plan.pathTemplate;",
880
+ " for (const name of plan.pathParams) {",
881
+ " const value = args[name];",
882
+ ' path = path.split("{" + name + "}").join(encodeURIComponent(String(value ?? "")));',
883
+ " }",
884
+ " const url = new URL(joinUrl(config.baseUrl, path));",
885
+ " for (const name of plan.queryParams) {",
886
+ " const value = args[name];",
887
+ " if (value === undefined || value === null) continue;",
888
+ " // Array query values are repeated (?k=1&k=2), the OpenAPI default (form/explode).",
889
+ " if (Array.isArray(value)) {",
890
+ " for (const v of value) {",
891
+ " if (v !== undefined && v !== null) url.searchParams.append(name, String(v));",
892
+ " }",
893
+ " } else {",
894
+ " url.searchParams.set(name, String(value));",
895
+ " }",
896
+ " }",
897
+ " const headers: Record<string, string> = {};",
898
+ " for (const name of plan.headerParams) {",
899
+ " const value = args[name];",
900
+ " if (value === undefined || value === null) continue;",
901
+ ' headers[name] = Array.isArray(value) ? value.map(String).join(",") : String(value);',
902
+ " }",
903
+ " const cookies: string[] = [];",
904
+ " for (const name of plan.cookieParams) {",
905
+ " const value = args[name];",
906
+ " if (value !== undefined && value !== null) {",
907
+ ' cookies.push(name + "=" + encodeURIComponent(String(value)));',
908
+ " }",
909
+ " }",
910
+ " if (cookies.length > 0) {",
911
+ ' headers["cookie"] = (headers["cookie"] ? headers["cookie"] + "; " : "") + cookies.join("; ");',
912
+ " }",
913
+ " let body: string | undefined;",
914
+ " if (plan.bodyParam && args[plan.bodyParam] !== undefined) {",
915
+ " const raw = args[plan.bodyParam];",
916
+ ' if (plan.contentType && plan.contentType.indexOf("json") >= 0) {',
917
+ ' headers["content-type"] = "application/json";',
918
+ " body = JSON.stringify(raw);",
919
+ " } else {",
920
+ ' if (plan.contentType) headers["content-type"] = plan.contentType;',
921
+ ' body = typeof raw === "string" ? raw : JSON.stringify(raw);',
922
+ " }",
923
+ " }",
924
+ " await applyAuth(headers, url, plan.security);",
925
+ " const response = await fetch(url, { method: plan.method, headers, body });",
926
+ " const text = await response.text();",
927
+ " if (!response.ok) {",
928
+ ' throw new Error("HTTP " + response.status + " " + response.statusText + ": " + text.slice(0, 800));',
929
+ " }",
930
+ " return text;",
931
+ "}",
932
+ ""
933
+ ].join("\n");
934
+ }
935
+ function toolFileTs(tool) {
936
+ return [
937
+ GENERATED_BANNER,
938
+ "",
939
+ 'import type { ToolDef } from "../types.js";',
940
+ 'import { callOperation } from "../http/client.js";',
941
+ "",
942
+ `const inputSchema = ${JSON.stringify(tool.inputSchema, null, 2)} as Record<string, unknown>;`,
943
+ "",
944
+ `const plan = ${JSON.stringify(tool.plan, null, 2)};`,
945
+ "",
946
+ `export const ${tool.name}: ToolDef = {`,
947
+ ` name: ${JSON.stringify(tool.name)},`,
948
+ ` description: ${JSON.stringify(tool.description)},`,
949
+ " inputSchema,",
950
+ " handler: (args) => callOperation(plan, args),",
951
+ "};",
952
+ ""
953
+ ].join("\n");
954
+ }
955
+ function toolsIndexTs(toolNames) {
956
+ const imports = toolNames.map((n) => `import { ${n} } from "./${n}.js";`);
957
+ return [
958
+ GENERATED_BANNER,
959
+ "",
960
+ 'import type { ToolDef } from "../types.js";',
961
+ ...imports,
962
+ "",
963
+ `export const tools: ToolDef[] = [${toolNames.join(", ")}];`,
964
+ ""
965
+ ].join("\n");
966
+ }
967
+ function serverTs(name, version) {
968
+ return [
969
+ GENERATED_BANNER,
970
+ "",
971
+ 'import { Server } from "@modelcontextprotocol/sdk/server/index.js";',
972
+ "import {",
973
+ " ListToolsRequestSchema,",
974
+ " CallToolRequestSchema,",
975
+ '} from "@modelcontextprotocol/sdk/types.js";',
976
+ 'import { tools } from "./tools/index.js";',
977
+ "",
978
+ "export function createServer(): Server {",
979
+ " const server = new Server(",
980
+ ` { name: ${JSON.stringify(name)}, version: ${JSON.stringify(version)} },`,
981
+ " { capabilities: { tools: {} } },",
982
+ " );",
983
+ "",
984
+ " server.setRequestHandler(ListToolsRequestSchema, async () => ({",
985
+ " tools: tools.map((t) => ({",
986
+ " name: t.name,",
987
+ " description: t.description,",
988
+ " inputSchema: t.inputSchema,",
989
+ " })),",
990
+ " }));",
991
+ "",
992
+ " server.setRequestHandler(CallToolRequestSchema, async (request) => {",
993
+ " const tool = tools.find((t) => t.name === request.params.name);",
994
+ " if (!tool) {",
995
+ " return {",
996
+ ' content: [{ type: "text", text: "Unknown tool: " + request.params.name }],',
997
+ " isError: true,",
998
+ " };",
999
+ " }",
1000
+ " try {",
1001
+ " const text = await tool.handler(",
1002
+ " (request.params.arguments ?? {}) as Record<string, unknown>,",
1003
+ " );",
1004
+ ' return { content: [{ type: "text", text }] };',
1005
+ " } catch (err) {",
1006
+ " return {",
1007
+ ' content: [{ type: "text", text: "Error: " + (err instanceof Error ? err.message : String(err)) }],',
1008
+ " isError: true,",
1009
+ " };",
1010
+ " }",
1011
+ " });",
1012
+ "",
1013
+ " return server;",
1014
+ "}",
1015
+ ""
1016
+ ].join("\n");
1017
+ }
1018
+ function indexStdioTs(name) {
1019
+ return [
1020
+ GENERATED_BANNER,
1021
+ "",
1022
+ 'import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";',
1023
+ 'import { createServer } from "./server.js";',
1024
+ "",
1025
+ "async function main(): Promise<void> {",
1026
+ " const server = createServer();",
1027
+ " const transport = new StdioServerTransport();",
1028
+ " await server.connect(transport);",
1029
+ ` console.error(${JSON.stringify(name + " MCP server running on stdio")});`,
1030
+ "}",
1031
+ "",
1032
+ "main().catch((err) => {",
1033
+ " console.error(err);",
1034
+ " process.exit(1);",
1035
+ "});",
1036
+ ""
1037
+ ].join("\n");
1038
+ }
1039
+ function indexHttpTs(name) {
1040
+ return [
1041
+ GENERATED_BANNER,
1042
+ "",
1043
+ 'import { createServer as createHttpServer } from "node:http";',
1044
+ 'import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";',
1045
+ 'import { createServer } from "./server.js";',
1046
+ "",
1047
+ "const port = Number(process.env.PORT ?? 3000);",
1048
+ "",
1049
+ "// DNS-rebinding protection: only accept requests whose Host header is in this allowlist, so a",
1050
+ "// malicious web page cannot rebind to localhost and drive this server. Override for non-local",
1051
+ "// deployments via MCP_ALLOWED_HOSTS (comma-separated host[:port] values).",
1052
+ 'const defaultHosts = "127.0.0.1:" + port + ",localhost:" + port;',
1053
+ "const allowedHostList = (process.env.MCP_ALLOWED_HOSTS ?? defaultHosts)",
1054
+ ' .split(",")',
1055
+ " .map((h) => h.trim())",
1056
+ " .filter(Boolean);",
1057
+ "",
1058
+ "// Stateless Streamable HTTP: a fresh server+transport per request.",
1059
+ "createHttpServer(async (req, res) => {",
1060
+ ' if (!req.url || !req.url.startsWith("/mcp")) {',
1061
+ " res.statusCode = 404;",
1062
+ ' res.end("Not found");',
1063
+ " return;",
1064
+ " }",
1065
+ " const server = createServer();",
1066
+ " const transport = new StreamableHTTPServerTransport({",
1067
+ " sessionIdGenerator: undefined,",
1068
+ " enableDnsRebindingProtection: true,",
1069
+ " allowedHosts: allowedHostList,",
1070
+ " });",
1071
+ ' res.on("close", () => {',
1072
+ " void transport.close();",
1073
+ " void server.close();",
1074
+ " });",
1075
+ " await server.connect(transport);",
1076
+ " await transport.handleRequest(req, res);",
1077
+ "}).listen(port, () => {",
1078
+ ` console.error(${JSON.stringify(name + " MCP server on http://localhost:")} + port + "/mcp");`,
1079
+ "});",
1080
+ ""
1081
+ ].join("\n");
1082
+ }
1083
+ function envExample(baseUrl, schemes) {
1084
+ const lines = ["# Environment for the generated MCP server.", ""];
1085
+ lines.push("# Base URL of the upstream API.");
1086
+ lines.push(`API_BASE_URL=${baseUrl || "https://api.example.com"}`);
1087
+ if (schemes.length > 0) {
1088
+ lines.push("");
1089
+ lines.push("# Credentials (read at runtime; never commit real values).");
1090
+ for (const s of schemes) {
1091
+ for (const v of s.envVars) lines.push(`${v}=`);
1092
+ }
1093
+ }
1094
+ lines.push("");
1095
+ return lines.join("\n");
1096
+ }
1097
+ function serverJson(name, version, description) {
1098
+ const manifest = {
1099
+ $schema: "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
1100
+ name: `io.github.OWNER/${name}`,
1101
+ description,
1102
+ version,
1103
+ packages: [
1104
+ {
1105
+ registryType: "npm",
1106
+ identifier: name,
1107
+ version,
1108
+ transport: { type: "stdio" }
1109
+ }
1110
+ ]
1111
+ };
1112
+ return JSON.stringify(manifest, null, 2) + "\n";
1113
+ }
1114
+ function clientConfigMd(name) {
1115
+ return [
1116
+ "# Add this server to an MCP client",
1117
+ "",
1118
+ "After `npm install && npm run build`, register the server.",
1119
+ "",
1120
+ "## Claude Desktop / Claude Code (`mcp.json` or `claude_desktop_config.json`)",
1121
+ "",
1122
+ "```json",
1123
+ "{",
1124
+ ' "mcpServers": {',
1125
+ ` "${name}": {`,
1126
+ ' "command": "node",',
1127
+ ` "args": ["./dist/index.js"],`,
1128
+ ' "env": {',
1129
+ ' "API_BASE_URL": "https://api.example.com"',
1130
+ " }",
1131
+ " }",
1132
+ " }",
1133
+ "}",
1134
+ "```",
1135
+ "",
1136
+ "## Cursor (`.cursor/mcp.json`) and Codex use the same `mcpServers` shape.",
1137
+ "",
1138
+ "Set any required credential env vars (see `.env.example`).",
1139
+ ""
1140
+ ].join("\n");
1141
+ }
1142
+ function readmeMd(data) {
1143
+ const envLines = data.schemes.flatMap((s) => s.envVars).map((v) => "- `" + v + "`");
1144
+ const lines = [
1145
+ `# ${data.serverName}`,
1146
+ "",
1147
+ `An MCP server for **${data.apiTitle}**, generated by [${GENERATOR_NAME}](https://github.com/krishgok/mcp-api-translator).`,
1148
+ `It exposes ${data.toolCount} tool(s) that call the upstream API directly.`,
1149
+ "",
1150
+ "## Quickstart",
1151
+ "",
1152
+ "```bash",
1153
+ "npm install",
1154
+ "npm run build",
1155
+ "cp .env.example .env # then fill in values",
1156
+ "npm start # runs over stdio",
1157
+ "```",
1158
+ "",
1159
+ "## Configuration",
1160
+ "",
1161
+ "- `API_BASE_URL` \u2014 base URL of the upstream API.",
1162
+ ...envLines.length > 0 ? ["", "Credentials:", ...envLines] : [],
1163
+ "",
1164
+ "## Connecting a client",
1165
+ "",
1166
+ "See `client-config.md` for ready-to-paste Claude / Cursor / Codex config.",
1167
+ "",
1168
+ ...data.transport === "http" || data.transport === "both" ? [
1169
+ "## HTTP transport",
1170
+ "",
1171
+ "```bash",
1172
+ "npm run start:http # Streamable HTTP on PORT (default 3000) at /mcp",
1173
+ "```",
1174
+ ""
1175
+ ] : [],
1176
+ "## Publishing for discovery",
1177
+ "",
1178
+ "Edit `server.json` (set your owner/namespace) and publish to the official MCP Registry",
1179
+ "so AI clients can discover this server.",
1180
+ "",
1181
+ "## Regenerating / extending",
1182
+ "",
1183
+ "This project carries a `.mcp-translator.json` manifest. Point mcp-api-translator's",
1184
+ "`extend_mcp_server` tool at this directory with another spec to add more tools; your",
1185
+ "hand-edited tool files are preserved.",
1186
+ ""
1187
+ ];
1188
+ return lines.join("\n");
1189
+ }
1190
+ function dockerfile() {
1191
+ return [
1192
+ "# Build stage: needs devDependencies (typescript) to compile.",
1193
+ "FROM node:20-alpine AS build",
1194
+ "WORKDIR /app",
1195
+ "COPY package*.json ./",
1196
+ "RUN npm install",
1197
+ "COPY . .",
1198
+ "RUN npm run build",
1199
+ "",
1200
+ "# Runtime stage: production deps only, runs as the non-root 'node' user.",
1201
+ "FROM node:20-alpine",
1202
+ "ENV NODE_ENV=production",
1203
+ "WORKDIR /app",
1204
+ "COPY package*.json ./",
1205
+ "RUN npm install --omit=dev && npm cache clean --force",
1206
+ "COPY --from=build /app/dist ./dist",
1207
+ "USER node",
1208
+ 'CMD ["node", "dist/index.js"]',
1209
+ ""
1210
+ ].join("\n");
1211
+ }
1212
+
1213
+ // src/emitters/python.ts
1214
+ var BANNER = `# Generated by ${GENERATOR_NAME} v${GENERATOR_VERSION}. Safe to edit.`;
1215
+ function commentSafe2(value) {
1216
+ return value.replace(/\s+/g, " ").trim();
1217
+ }
1218
+ function toPackageModule(serverName) {
1219
+ let mod = serverName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
1220
+ if (mod.length === 0) mod = "api_mcp";
1221
+ if (/^[0-9]/.test(mod)) mod = `pkg_${mod}`;
1222
+ return mod;
1223
+ }
1224
+ function pyprojectToml(name, version, pkg) {
1225
+ return [
1226
+ "[project]",
1227
+ `name = ${JSON.stringify(name)}`,
1228
+ `version = ${JSON.stringify(version)}`,
1229
+ 'requires-python = ">=3.10"',
1230
+ 'dependencies = ["mcp>=1.2.0", "anyio>=4.0"]',
1231
+ "",
1232
+ "[project.scripts]",
1233
+ `${name} = "${pkg}.__main__:run"`,
1234
+ "",
1235
+ "[build-system]",
1236
+ 'requires = ["hatchling"]',
1237
+ 'build-backend = "hatchling.build"',
1238
+ "",
1239
+ "[tool.hatch.build.targets.wheel]",
1240
+ `packages = ["${pkg}"]`,
1241
+ ""
1242
+ ].join("\n");
1243
+ }
1244
+ function gitignorePy() {
1245
+ return ["__pycache__/", "*.pyc", ".venv/", ".env", "dist/", "*.egg-info/", ""].join("\n");
1246
+ }
1247
+ function initPy() {
1248
+ return `${BANNER}
1249
+ """Generated MCP server package."""
1250
+ `;
1251
+ }
1252
+ function configPy(defaultBaseUrl) {
1253
+ return [
1254
+ BANNER,
1255
+ "import os",
1256
+ "",
1257
+ `BASE_URL = os.environ.get("API_BASE_URL", ${JSON.stringify(defaultBaseUrl)})`,
1258
+ ""
1259
+ ].join("\n");
1260
+ }
1261
+ function authBlockPy(scheme) {
1262
+ const lines = [];
1263
+ const label = commentSafe2(
1264
+ `${scheme.name} (${scheme.type}${scheme.scheme ? "/" + scheme.scheme : ""})`
1265
+ );
1266
+ lines.push(` # ${label}`);
1267
+ lines.push(` if ${JSON.stringify(scheme.name)} in security:`);
1268
+ if (scheme.type === "apiKey") {
1269
+ const env = scheme.envVars[0];
1270
+ const param = scheme.paramName ?? "X-API-Key";
1271
+ lines.push(` value = os.environ.get(${JSON.stringify(env)})`);
1272
+ lines.push(" if value:");
1273
+ if (scheme.in === "query") {
1274
+ lines.push(` query[${JSON.stringify(param)}] = value`);
1275
+ } else if (scheme.in === "cookie") {
1276
+ lines.push(
1277
+ ` headers["cookie"] = (headers["cookie"] + "; " if headers.get("cookie") else "") + ${JSON.stringify(
1278
+ param + "="
1279
+ )} + value`
1280
+ );
1281
+ } else {
1282
+ lines.push(` headers[${JSON.stringify(param)}] = value`);
1283
+ }
1284
+ } else if (scheme.type === "http" && scheme.scheme?.toLowerCase() === "basic") {
1285
+ const [u, p] = scheme.envVars;
1286
+ lines.push(` user = os.environ.get(${JSON.stringify(u)})`);
1287
+ lines.push(` password = os.environ.get(${JSON.stringify(p)})`);
1288
+ lines.push(" if user and password:");
1289
+ lines.push(' token = base64.b64encode((user + ":" + password).encode()).decode()');
1290
+ lines.push(' headers["authorization"] = "Basic " + token');
1291
+ } else if (scheme.tokenUrl) {
1292
+ const [id, secret] = scheme.envVars;
1293
+ lines.push(` client_id = os.environ.get(${JSON.stringify(id)})`);
1294
+ lines.push(` client_secret = os.environ.get(${JSON.stringify(secret)})`);
1295
+ lines.push(" if client_id and client_secret:");
1296
+ lines.push(
1297
+ ` token = _get_token(${JSON.stringify(scheme.tokenUrl)}, client_id, client_secret)`
1298
+ );
1299
+ lines.push(' headers["authorization"] = "Bearer " + token');
1300
+ } else {
1301
+ const env = scheme.envVars[0];
1302
+ lines.push(` value = os.environ.get(${JSON.stringify(env)})`);
1303
+ lines.push(" if value:");
1304
+ lines.push(' headers["authorization"] = "Bearer " + value');
1305
+ }
1306
+ return lines;
1307
+ }
1308
+ function oauthHelperPy() {
1309
+ return [
1310
+ "_token_cache = {}",
1311
+ "",
1312
+ "",
1313
+ "def _get_token(token_url, client_id, client_secret):",
1314
+ " key = token_url + '|' + client_id",
1315
+ " cached = _token_cache.get(key)",
1316
+ " if cached and cached[1] > time.time():",
1317
+ " return cached[0]",
1318
+ " data = urllib.parse.urlencode(",
1319
+ ' {"grant_type": "client_credentials", "client_id": client_id, "client_secret": client_secret}',
1320
+ " ).encode()",
1321
+ " req = urllib.request.Request(",
1322
+ " token_url,",
1323
+ " data=data,",
1324
+ ' headers={"content-type": "application/x-www-form-urlencoded"},',
1325
+ ' method="POST",',
1326
+ " )",
1327
+ " with urllib.request.urlopen(req) as resp:",
1328
+ " payload = json.loads(resp.read().decode())",
1329
+ ' token = payload.get("access_token")',
1330
+ " if not token:",
1331
+ ' raise RuntimeError("OAuth token response had no access_token")',
1332
+ ' ttl = payload.get("expires_in", 3600)',
1333
+ " _token_cache[key] = (token, time.time() + ttl - 30)",
1334
+ " return token",
1335
+ ""
1336
+ ];
1337
+ }
1338
+ function authPy(schemes) {
1339
+ const body = schemes.flatMap(authBlockPy);
1340
+ const hasOAuth = schemes.some((s) => s.tokenUrl);
1341
+ const imports = hasOAuth ? [
1342
+ "import base64",
1343
+ "import json",
1344
+ "import os",
1345
+ "import time",
1346
+ "import urllib.parse",
1347
+ "import urllib.request"
1348
+ ] : ["import base64", "import os"];
1349
+ return [
1350
+ BANNER,
1351
+ ...imports,
1352
+ "",
1353
+ "",
1354
+ ...hasOAuth ? oauthHelperPy() : [],
1355
+ ...hasOAuth ? [""] : [],
1356
+ "def apply_auth(headers, query, security):",
1357
+ ' """Inject env-backed credentials. Secrets are never embedded."""',
1358
+ ...body.length > 0 ? body : [" # No security schemes detected on the source API.", " pass"],
1359
+ ""
1360
+ ].join("\n");
1361
+ }
1362
+ function httpClientPy() {
1363
+ return [
1364
+ BANNER,
1365
+ "import json",
1366
+ "import urllib.error",
1367
+ "import urllib.parse",
1368
+ "import urllib.request",
1369
+ "",
1370
+ "from .auth import apply_auth",
1371
+ "from .config import BASE_URL",
1372
+ "",
1373
+ "",
1374
+ "def call_operation(plan, args):",
1375
+ " if not BASE_URL:",
1376
+ ' raise RuntimeError("API base URL is not set. Set API_BASE_URL in the environment.")',
1377
+ ' path = plan["pathTemplate"]',
1378
+ ' for name in plan["pathParams"]:',
1379
+ " value = args.get(name)",
1380
+ ' encoded = urllib.parse.quote(str(value if value is not None else ""), safe="")',
1381
+ ' path = path.replace("{" + name + "}", encoded)',
1382
+ " query = {}",
1383
+ ' for name in plan["queryParams"]:',
1384
+ " value = args.get(name)",
1385
+ " if value is not None:",
1386
+ " query[name] = str(value)",
1387
+ " headers = {}",
1388
+ ' for name in plan["headerParams"]:',
1389
+ " value = args.get(name)",
1390
+ " if value is not None:",
1391
+ " headers[name] = str(value)",
1392
+ " data = None",
1393
+ ' body_param = plan.get("bodyParam")',
1394
+ " if body_param and args.get(body_param) is not None:",
1395
+ " raw = args[body_param]",
1396
+ ' content_type = plan.get("contentType")',
1397
+ ' if content_type and "json" in content_type:',
1398
+ ' headers["content-type"] = "application/json"',
1399
+ " data = json.dumps(raw).encode()",
1400
+ " else:",
1401
+ " if content_type:",
1402
+ ' headers["content-type"] = content_type',
1403
+ " data = raw.encode() if isinstance(raw, str) else json.dumps(raw).encode()",
1404
+ ' apply_auth(headers, query, plan["security"])',
1405
+ ' url = BASE_URL.rstrip("/") + "/" + path.lstrip("/")',
1406
+ " if query:",
1407
+ ' url += "?" + urllib.parse.urlencode(query)',
1408
+ ' request = urllib.request.Request(url, data=data, headers=headers, method=plan["method"])',
1409
+ " try:",
1410
+ " with urllib.request.urlopen(request) as response:",
1411
+ " return response.read().decode()",
1412
+ " except urllib.error.HTTPError as error:",
1413
+ " detail = error.read().decode()[:800]",
1414
+ ' raise RuntimeError("HTTP " + str(error.code) + ": " + detail)',
1415
+ ""
1416
+ ].join("\n");
1417
+ }
1418
+ function toolsPy() {
1419
+ return [
1420
+ BANNER,
1421
+ "import json",
1422
+ "import pathlib",
1423
+ "",
1424
+ 'TOOLS = json.loads((pathlib.Path(__file__).parent / "tools.json").read_text())',
1425
+ ""
1426
+ ].join("\n");
1427
+ }
1428
+ function serverPy(name) {
1429
+ return [
1430
+ BANNER,
1431
+ "import mcp.types as types",
1432
+ "from mcp.server.lowlevel import Server",
1433
+ "",
1434
+ "from .http_client import call_operation",
1435
+ "from .tools import TOOLS",
1436
+ "",
1437
+ `server = Server(${JSON.stringify(name)})`,
1438
+ "",
1439
+ "",
1440
+ "@server.list_tools()",
1441
+ "async def list_tools():",
1442
+ " return [",
1443
+ ' types.Tool(name=t["name"], description=t["description"], inputSchema=t["inputSchema"])',
1444
+ " for t in TOOLS",
1445
+ " ]",
1446
+ "",
1447
+ "",
1448
+ "@server.call_tool()",
1449
+ "async def call_tool(name, arguments):",
1450
+ ' tool = next((t for t in TOOLS if t["name"] == name), None)',
1451
+ " if tool is None:",
1452
+ ' return [types.TextContent(type="text", text="Unknown tool: " + name)]',
1453
+ " try:",
1454
+ ' text = call_operation(tool["plan"], arguments or {})',
1455
+ ' return [types.TextContent(type="text", text=text)]',
1456
+ " except Exception as error: # noqa: BLE001",
1457
+ ' return [types.TextContent(type="text", text="Error: " + str(error))]',
1458
+ ""
1459
+ ].join("\n");
1460
+ }
1461
+ function mainPy() {
1462
+ return [
1463
+ BANNER,
1464
+ "import anyio",
1465
+ "from mcp.server.stdio import stdio_server",
1466
+ "",
1467
+ "from .server import server",
1468
+ "",
1469
+ "",
1470
+ "async def main():",
1471
+ " async with stdio_server() as (read_stream, write_stream):",
1472
+ " await server.run(read_stream, write_stream, server.create_initialization_options())",
1473
+ "",
1474
+ "",
1475
+ "def run():",
1476
+ " anyio.run(main)",
1477
+ "",
1478
+ "",
1479
+ 'if __name__ == "__main__":',
1480
+ " run()",
1481
+ ""
1482
+ ].join("\n");
1483
+ }
1484
+ function dockerfilePy(pkg) {
1485
+ return [
1486
+ "FROM python:3.12-slim",
1487
+ "WORKDIR /app",
1488
+ "COPY . .",
1489
+ "RUN pip install --no-cache-dir .",
1490
+ `CMD ["python", "-m", "${pkg}"]`,
1491
+ ""
1492
+ ].join("\n");
1493
+ }
1494
+ function readmePy(data) {
1495
+ const envLines = data.schemes.flatMap((s) => s.envVars).map((v) => "- `" + v + "`");
1496
+ return [
1497
+ `# ${data.serverName}`,
1498
+ "",
1499
+ `A Python MCP server for **${data.apiTitle}**, generated by ${GENERATOR_NAME}.`,
1500
+ `It exposes ${data.toolCount} tool(s) that call the upstream API directly.`,
1501
+ "",
1502
+ "## Quickstart",
1503
+ "",
1504
+ "```bash",
1505
+ "python -m venv .venv && source .venv/bin/activate",
1506
+ "pip install -e .",
1507
+ "cp .env.example .env # then fill in values",
1508
+ `python -m ${data.pkg} # runs over stdio`,
1509
+ "```",
1510
+ "",
1511
+ "## Configuration",
1512
+ "",
1513
+ "- `API_BASE_URL` \u2014 base URL of the upstream API.",
1514
+ ...envLines.length > 0 ? ["", "Credentials:", ...envLines] : [],
1515
+ "",
1516
+ "## Connecting a client",
1517
+ "",
1518
+ "```json",
1519
+ "{",
1520
+ ' "mcpServers": {',
1521
+ ` "${data.serverName}": {`,
1522
+ ' "command": "python",',
1523
+ ` "args": ["-m", "${data.pkg}"],`,
1524
+ ' "env": { "API_BASE_URL": "https://api.example.com" }',
1525
+ " }",
1526
+ " }",
1527
+ "}",
1528
+ "```",
1529
+ ""
1530
+ ].join("\n");
1531
+ }
1532
+
1533
+ // src/emitters/project.ts
1534
+ var FileWriter = class {
1535
+ constructor(root) {
1536
+ this.root = root;
1537
+ }
1538
+ root;
1539
+ written = [];
1540
+ async write(relPath, content) {
1541
+ const full = path2.join(this.root, relPath);
1542
+ await mkdir(path2.dirname(full), { recursive: true });
1543
+ await writeFile2(full, content, "utf8");
1544
+ this.written.push(relPath);
1545
+ }
1546
+ async writeIfAbsent(relPath, content, force) {
1547
+ if (!force && await exists(path2.join(this.root, relPath))) return false;
1548
+ await this.write(relPath, content);
1549
+ return true;
1550
+ }
1551
+ };
1552
+ async function exists(p) {
1553
+ try {
1554
+ await access(p);
1555
+ return true;
1556
+ } catch {
1557
+ return false;
1558
+ }
1559
+ }
1560
+ async function isEmptyDir(dir) {
1561
+ try {
1562
+ const entries = await readdir(dir);
1563
+ return entries.length === 0;
1564
+ } catch (err) {
1565
+ if (err.code === "ENOENT") return true;
1566
+ throw err;
1567
+ }
1568
+ }
1569
+ var MAX_GENERATED_TOOLS = 1e3;
1570
+ function assertToolCount(count) {
1571
+ if (count > MAX_GENERATED_TOOLS) {
1572
+ throw new Error(
1573
+ `Refusing to generate ${count} tools (limit ${MAX_GENERATED_TOOLS}). Narrow the spec with includeTags / methods / pathGlob / excludeOperations.`
1574
+ );
1575
+ }
1576
+ }
1577
+ function toPackageName(title) {
1578
+ const base = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1579
+ return (base || "api") + "-mcp";
1580
+ }
1581
+ function operationToToolEmit(op, sourceTitle) {
1582
+ const properties = {};
1583
+ const required = [];
1584
+ for (const p of op.parameters) {
1585
+ const schema = { ...p.schema };
1586
+ if (p.description && !("description" in schema)) schema.description = p.description;
1587
+ properties[p.name] = schema;
1588
+ if (p.required) required.push(p.name);
1589
+ }
1590
+ const pathTokens = [...op.path.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]);
1591
+ for (const token of pathTokens) {
1592
+ if (!(token in properties)) {
1593
+ properties[token] = { type: "string" };
1594
+ if (!required.includes(token)) required.push(token);
1595
+ }
1596
+ }
1597
+ let bodyParam = null;
1598
+ if (op.requestBody) {
1599
+ bodyParam = "body" in properties ? "requestBody" : "body";
1600
+ const bodySchema = { ...op.requestBody.schema };
1601
+ if (op.requestBody.description && !("description" in bodySchema)) {
1602
+ bodySchema.description = op.requestBody.description;
1603
+ }
1604
+ properties[bodyParam] = bodySchema;
1605
+ if (op.requestBody.required) required.push(bodyParam);
1606
+ }
1607
+ const inputSchema = {
1608
+ type: "object",
1609
+ properties,
1610
+ ...required.length > 0 ? { required } : {}
1611
+ };
1612
+ return {
1613
+ name: op.toolName,
1614
+ method: op.method,
1615
+ path: op.path,
1616
+ description: buildDescription(op),
1617
+ inputSchema,
1618
+ sourceTitle,
1619
+ plan: {
1620
+ method: op.method,
1621
+ pathTemplate: op.path,
1622
+ pathParams: pathTokens,
1623
+ queryParams: op.parameters.filter((p) => p.in === "query").map((p) => p.name),
1624
+ headerParams: op.parameters.filter((p) => p.in === "header").map((p) => p.name),
1625
+ cookieParams: op.parameters.filter((p) => p.in === "cookie").map((p) => p.name),
1626
+ bodyParam,
1627
+ contentType: op.requestBody?.contentType ?? null,
1628
+ security: op.security
1629
+ }
1630
+ };
1631
+ }
1632
+ async function emitShared(fw, serverName, serverVersion, description, servers, schemes, transport, allToolNames, toolCount) {
1633
+ const baseUrl = servers[0] ?? "";
1634
+ await fw.write("package.json", packageJson(serverName, serverVersion, transport));
1635
+ await fw.write("tsconfig.json", tsconfigJson());
1636
+ await fw.write(".gitignore", gitignore());
1637
+ await fw.write(".env.example", envExample(baseUrl, schemes));
1638
+ await fw.write("Dockerfile", dockerfile());
1639
+ await fw.write("server.json", serverJson(serverName, serverVersion, description));
1640
+ await fw.write("client-config.md", clientConfigMd(serverName));
1641
+ await fw.write(
1642
+ "README.md",
1643
+ readmeMd({ serverName, apiTitle: description, toolCount, schemes, transport })
1644
+ );
1645
+ await fw.write("src/types.ts", typesTs());
1646
+ await fw.write("src/config.ts", configTs(baseUrl));
1647
+ await fw.write("src/auth.ts", authTs(schemes));
1648
+ await fw.write("src/http/client.ts", httpClientTs());
1649
+ await fw.write("src/server.ts", serverTs(serverName, serverVersion));
1650
+ await fw.write("src/index.ts", indexStdioTs(serverName));
1651
+ if (transport === "http" || transport === "both") {
1652
+ await fw.write("src/index.http.ts", indexHttpTs(serverName));
1653
+ }
1654
+ await fw.write("src/tools/index.ts", toolsIndexTs(allToolNames));
1655
+ }
1656
+ function toPyToolRecord(tool) {
1657
+ return {
1658
+ name: tool.name,
1659
+ description: tool.description,
1660
+ inputSchema: tool.inputSchema,
1661
+ plan: tool.plan
1662
+ };
1663
+ }
1664
+ function pythonToolsJsonPath(serverName) {
1665
+ return `${toPackageModule(serverName)}/tools.json`;
1666
+ }
1667
+ async function emitPythonShared(fw, serverName, serverVersion, description, servers, schemes, toolCount) {
1668
+ const baseUrl = servers[0] ?? "";
1669
+ const pkg = toPackageModule(serverName);
1670
+ await fw.write("pyproject.toml", pyprojectToml(serverName, serverVersion, pkg));
1671
+ await fw.write(".gitignore", gitignorePy());
1672
+ await fw.write(".env.example", envExample(baseUrl, schemes));
1673
+ await fw.write("Dockerfile", dockerfilePy(pkg));
1674
+ await fw.write("server.json", serverJson(serverName, serverVersion, description));
1675
+ await fw.write(
1676
+ "README.md",
1677
+ readmePy({ serverName, pkg, apiTitle: description, toolCount, schemes })
1678
+ );
1679
+ await fw.write(`${pkg}/__init__.py`, initPy());
1680
+ await fw.write(`${pkg}/config.py`, configPy(baseUrl));
1681
+ await fw.write(`${pkg}/auth.py`, authPy(schemes));
1682
+ await fw.write(`${pkg}/http_client.py`, httpClientPy());
1683
+ await fw.write(`${pkg}/server.py`, serverPy(serverName));
1684
+ await fw.write(`${pkg}/__main__.py`, mainPy());
1685
+ await fw.write(`${pkg}/tools.py`, toolsPy());
1686
+ }
1687
+ async function generateProject(model, options) {
1688
+ const dir = path2.resolve(options.outputDir);
1689
+ const existingManifest = await readManifest(dir);
1690
+ if (existingManifest && !options.force) {
1691
+ throw new Error(
1692
+ `${dir} is already a generated project. Use extend_mcp_server to add tools, or pass force: true to overwrite.`
1693
+ );
1694
+ }
1695
+ if (!existingManifest && !options.force && !await isEmptyDir(dir)) {
1696
+ throw new Error(`${dir} is not empty. Pass force: true to overwrite.`);
1697
+ }
1698
+ const serverName = options.serverName ?? toPackageName(model.title);
1699
+ const serverVersion = options.serverVersion ?? model.version ?? "0.1.0";
1700
+ const transport = options.transport ?? "stdio";
1701
+ const language = options.language ?? "typescript";
1702
+ const description = model.title + (model.version ? ` (v${model.version})` : "");
1703
+ const { operations, filteredOut } = curate(model, options.filters ?? {});
1704
+ assertToolCount(operations.length);
1705
+ const tools = operations.map((op) => operationToToolEmit(op, model.title));
1706
+ const fw = new FileWriter(dir);
1707
+ if (language === "python") {
1708
+ await emitPythonShared(
1709
+ fw,
1710
+ serverName,
1711
+ serverVersion,
1712
+ description,
1713
+ model.servers,
1714
+ model.securitySchemes,
1715
+ tools.length
1716
+ );
1717
+ await fw.write(
1718
+ pythonToolsJsonPath(serverName),
1719
+ JSON.stringify(tools.map(toPyToolRecord), null, 2) + "\n"
1720
+ );
1721
+ } else {
1722
+ await emitShared(
1723
+ fw,
1724
+ serverName,
1725
+ serverVersion,
1726
+ description,
1727
+ model.servers,
1728
+ model.securitySchemes,
1729
+ transport,
1730
+ tools.map((tool) => tool.name),
1731
+ tools.length
1732
+ );
1733
+ for (const tool of tools) {
1734
+ await fw.write(`src/tools/${tool.name}.ts`, toolFileTs(tool));
1735
+ }
1736
+ }
1737
+ const manifest = {
1738
+ manifestVersion: MANIFEST_VERSION,
1739
+ generator: GENERATOR_NAME,
1740
+ generatorVersion: GENERATOR_VERSION,
1741
+ serverName,
1742
+ serverVersion,
1743
+ description,
1744
+ language,
1745
+ transport,
1746
+ servers: model.servers,
1747
+ securitySchemes: model.securitySchemes,
1748
+ sources: [
1749
+ {
1750
+ format: model.sourceFormat,
1751
+ title: model.title,
1752
+ version: model.version,
1753
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
1754
+ }
1755
+ ],
1756
+ tools: tools.map(
1757
+ (tool) => ({
1758
+ name: tool.name,
1759
+ method: tool.method,
1760
+ path: tool.path,
1761
+ sourceTitle: tool.sourceTitle
1762
+ })
1763
+ )
1764
+ };
1765
+ await fw.write(MANIFEST_FILENAME, JSON.stringify(manifest, null, 2) + "\n");
1766
+ return {
1767
+ projectDir: dir,
1768
+ serverName,
1769
+ toolsAdded: tools.length,
1770
+ toolsSkipped: filteredOut,
1771
+ totalTools: tools.length,
1772
+ files: fw.written,
1773
+ warnings: toolCountWarnings(tools.length)
1774
+ };
1775
+ }
1776
+ async function appendToProject(model, options) {
1777
+ const dir = path2.resolve(options.projectDir);
1778
+ const manifest = await readManifest(dir);
1779
+ if (!manifest) {
1780
+ throw new Error(
1781
+ `${dir} has no ${MANIFEST_FILENAME}; it is not a generated project. Use generate_mcp_server instead.`
1782
+ );
1783
+ }
1784
+ if ((manifest.manifestVersion ?? 1) > MANIFEST_VERSION) {
1785
+ throw new Error(
1786
+ `${MANIFEST_FILENAME} is schema version ${manifest.manifestVersion}, newer than this generator supports (${MANIFEST_VERSION}). Upgrade mcp-api-translator.`
1787
+ );
1788
+ }
1789
+ const language = manifest.language ?? "typescript";
1790
+ const servers = [.../* @__PURE__ */ new Set([...manifest.servers, ...model.servers])];
1791
+ const schemeMap = /* @__PURE__ */ new Map();
1792
+ for (const s of manifest.securitySchemes) schemeMap.set(s.name, s);
1793
+ for (const s of model.securitySchemes) if (!schemeMap.has(s.name)) schemeMap.set(s.name, s);
1794
+ const schemes = assignEnvVars([...schemeMap.values()]);
1795
+ const existingKeys = new Set(manifest.tools.map((tool) => operationKey(tool.method, tool.path)));
1796
+ const reserved = new Set(manifest.tools.map((tool) => tool.name));
1797
+ const filtered = applyFilters(model.operations, options.filters ?? {});
1798
+ const newOps = filtered.filter((op) => !existingKeys.has(operationKey(op.method, op.path)));
1799
+ newOps.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
1800
+ for (const op of newOps) op.toolName = uniqueToolName(op.toolName, reserved);
1801
+ const newTools = newOps.map((op) => operationToToolEmit(op, model.title));
1802
+ const allToolNames = [
1803
+ ...manifest.tools.map((tool) => tool.name),
1804
+ ...newTools.map((tl) => tl.name)
1805
+ ];
1806
+ assertToolCount(allToolNames.length);
1807
+ const fw = new FileWriter(dir);
1808
+ const description = manifest.description ?? manifest.serverName;
1809
+ let skippedFiles = 0;
1810
+ if (language === "python") {
1811
+ const toolsJson = pythonToolsJsonPath(manifest.serverName);
1812
+ let existingRecords;
1813
+ try {
1814
+ existingRecords = JSON.parse(
1815
+ await readFile3(path2.join(dir, toolsJson), "utf8")
1816
+ );
1817
+ } catch {
1818
+ throw new Error(
1819
+ `${path2.join(dir, toolsJson)} is missing or unreadable; cannot extend this Python project.`
1820
+ );
1821
+ }
1822
+ const allRecords = [...existingRecords, ...newTools.map(toPyToolRecord)];
1823
+ await emitPythonShared(
1824
+ fw,
1825
+ manifest.serverName,
1826
+ manifest.serverVersion,
1827
+ description,
1828
+ servers,
1829
+ schemes,
1830
+ allRecords.length
1831
+ );
1832
+ await fw.write(toolsJson, JSON.stringify(allRecords, null, 2) + "\n");
1833
+ } else {
1834
+ await emitShared(
1835
+ fw,
1836
+ manifest.serverName,
1837
+ manifest.serverVersion,
1838
+ description,
1839
+ servers,
1840
+ schemes,
1841
+ manifest.transport,
1842
+ allToolNames,
1843
+ allToolNames.length
1844
+ );
1845
+ for (const tool of newTools) {
1846
+ const wrote = await fw.writeIfAbsent(
1847
+ `src/tools/${tool.name}.ts`,
1848
+ toolFileTs(tool),
1849
+ options.force ?? false
1850
+ );
1851
+ if (!wrote) skippedFiles++;
1852
+ }
1853
+ }
1854
+ const merged = {
1855
+ ...manifest,
1856
+ manifestVersion: MANIFEST_VERSION,
1857
+ generatorVersion: GENERATOR_VERSION,
1858
+ servers,
1859
+ securitySchemes: schemes,
1860
+ sources: [
1861
+ ...manifest.sources,
1862
+ {
1863
+ format: model.sourceFormat,
1864
+ title: model.title,
1865
+ version: model.version,
1866
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
1867
+ }
1868
+ ],
1869
+ tools: [
1870
+ ...manifest.tools,
1871
+ ...newTools.map(
1872
+ (tool) => ({
1873
+ name: tool.name,
1874
+ method: tool.method,
1875
+ path: tool.path,
1876
+ sourceTitle: tool.sourceTitle
1877
+ })
1878
+ )
1879
+ ]
1880
+ };
1881
+ await fw.write(MANIFEST_FILENAME, JSON.stringify(merged, null, 2) + "\n");
1882
+ return {
1883
+ projectDir: dir,
1884
+ serverName: manifest.serverName,
1885
+ toolsAdded: newTools.length,
1886
+ toolsSkipped: filtered.length - newOps.length + skippedFiles,
1887
+ totalTools: allToolNames.length,
1888
+ files: fw.written,
1889
+ warnings: toolCountWarnings(allToolNames.length)
1890
+ };
1891
+ }
1892
+ function toolCountWarnings(count) {
1893
+ if (count > TOOL_COUNT_WARN_THRESHOLD) {
1894
+ return [
1895
+ `This server exposes ${count} tools (> ${TOOL_COUNT_WARN_THRESHOLD}). Large tool counts hurt model tool-selection. Consider narrowing with includeTags / methods / pathGlob / excludeOperations.`
1896
+ ];
1897
+ }
1898
+ return [];
1899
+ }
1900
+
1901
+ // src/server.ts
1902
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1903
+
1904
+ // src/tools.ts
1905
+ import { z } from "zod";
1906
+ var sourceShape = {
1907
+ spec: z.string().optional().describe("Inline spec text (OpenAPI or Postman; JSON or YAML)."),
1908
+ specPath: z.string().optional().describe("Path to a local spec file (JSON or YAML)."),
1909
+ format: z.enum(["openapi", "postman", "auto"]).optional().describe("Force a format; defaults to auto-detect.")
1910
+ };
1911
+ var filterShape = {
1912
+ includeTags: z.array(z.string()).optional().describe("Keep only operations with these tags."),
1913
+ excludeOperations: z.array(z.string()).optional().describe("Drop operations by operationId, tool name, or tag."),
1914
+ methods: z.array(z.string()).optional().describe("Keep only these HTTP methods, e.g. [GET, POST]."),
1915
+ pathGlob: z.string().optional().describe('Keep only matching paths, e.g. "/v1/**".')
1916
+ };
1917
+ function text(value) {
1918
+ return { content: [{ type: "text", text: value }] };
1919
+ }
1920
+ function filtersFrom(args) {
1921
+ return {
1922
+ includeTags: args.includeTags,
1923
+ excludeOperations: args.excludeOperations,
1924
+ methods: args.methods,
1925
+ pathGlob: args.pathGlob
1926
+ };
1927
+ }
1928
+ function sourceFrom(args) {
1929
+ return {
1930
+ spec: args.spec,
1931
+ specPath: args.specPath,
1932
+ format: args.format
1933
+ };
1934
+ }
1935
+ function summaryText(verb, s) {
1936
+ const lines = [
1937
+ `${verb} MCP server "${s.serverName}" at ${s.projectDir}`,
1938
+ `Tools added: ${s.toolsAdded} | skipped: ${s.toolsSkipped} | total now: ${s.totalTools}`,
1939
+ "",
1940
+ "Files written:",
1941
+ ...s.files.map((f) => ` - ${f}`)
1942
+ ];
1943
+ if (s.warnings.length > 0) lines.push("", "Warnings:", ...s.warnings.map((w) => ` ! ${w}`));
1944
+ lines.push(
1945
+ "",
1946
+ "Next steps:",
1947
+ ` cd ${s.projectDir}`,
1948
+ " npm install && npm run build",
1949
+ " cp .env.example .env # fill in API_BASE_URL and any credentials",
1950
+ " npm start",
1951
+ " # See client-config.md to add it to Claude / Cursor / Codex."
1952
+ );
1953
+ return lines.join("\n");
1954
+ }
1955
+ function registerTools(server) {
1956
+ server.registerTool(
1957
+ "analyze_spec",
1958
+ {
1959
+ title: "Analyze API spec (preview)",
1960
+ description: "Parse an OpenAPI or Postman spec and preview the MCP tools that would be generated, without writing any files. Use this to curate (filter/inspect) before generating.",
1961
+ inputSchema: { ...sourceShape, ...filterShape }
1962
+ },
1963
+ async (args) => {
1964
+ const model = await parseSource(sourceFrom(args));
1965
+ const { operations, filteredOut } = curate(model, filtersFrom(args));
1966
+ const report = {
1967
+ title: model.title,
1968
+ version: model.version,
1969
+ sourceFormat: model.sourceFormat,
1970
+ servers: model.servers,
1971
+ authSchemes: model.securitySchemes.map((s) => ({
1972
+ name: s.name,
1973
+ type: s.type,
1974
+ envVars: s.envVars
1975
+ })),
1976
+ operationsTotal: model.operations.length,
1977
+ operationsKept: operations.length,
1978
+ operationsFilteredOut: filteredOut,
1979
+ proposedTools: operations.map((op) => ({
1980
+ name: op.toolName,
1981
+ method: op.method,
1982
+ path: op.path,
1983
+ summary: op.summary ?? ""
1984
+ }))
1985
+ };
1986
+ const warn = operations.length > TOOL_COUNT_WARN_THRESHOLD ? `
1987
+
1988
+ ! ${operations.length} tools exceed the ${TOOL_COUNT_WARN_THRESHOLD}-tool guideline; consider includeTags / methods / pathGlob.` : "";
1989
+ return text(JSON.stringify(report, null, 2) + warn);
1990
+ }
1991
+ );
1992
+ server.registerTool(
1993
+ "generate_mcp_server",
1994
+ {
1995
+ title: "Generate MCP server",
1996
+ description: "Generate a complete, runnable TypeScript MCP server project from an OpenAPI or Postman spec. Writes the project to outputDir. Use force to overwrite a non-empty directory.",
1997
+ inputSchema: {
1998
+ ...sourceShape,
1999
+ outputDir: z.string().describe("Directory to write the generated project into."),
2000
+ serverName: z.string().optional().describe("npm/package name; defaults from the API title."),
2001
+ serverVersion: z.string().optional(),
2002
+ language: z.enum(["typescript", "python"]).optional().describe("Output language. Defaults to typescript."),
2003
+ transport: z.enum(["stdio", "http", "both"]).optional().describe("Transport(s) to generate (TypeScript only). Defaults to stdio."),
2004
+ force: z.boolean().optional().describe("Overwrite a non-empty / existing project."),
2005
+ ...filterShape
2006
+ }
2007
+ },
2008
+ async (args) => {
2009
+ const model = await parseSource(sourceFrom(args));
2010
+ const summary = await generateProject(model, {
2011
+ outputDir: args.outputDir,
2012
+ serverName: args.serverName,
2013
+ serverVersion: args.serverVersion,
2014
+ language: args.language,
2015
+ transport: args.transport,
2016
+ force: args.force,
2017
+ filters: filtersFrom(args)
2018
+ });
2019
+ return text(summaryText("Generated", summary));
2020
+ }
2021
+ );
2022
+ server.registerTool(
2023
+ "extend_mcp_server",
2024
+ {
2025
+ title: "Extend an existing generated MCP server",
2026
+ description: "Append the operations from another spec to an already-generated project (identified by its .mcp-translator.json manifest). Idempotent: operations already present are skipped, and hand-edited tool files are preserved unless force is set. Enables aggregating multiple APIs into one MCP server.",
2027
+ inputSchema: {
2028
+ projectDir: z.string().describe("Path to an existing generated project."),
2029
+ ...sourceShape,
2030
+ force: z.boolean().optional().describe("Overwrite existing tool files of the same name."),
2031
+ ...filterShape
2032
+ }
2033
+ },
2034
+ async (args) => {
2035
+ const model = await parseSource(sourceFrom(args));
2036
+ const summary = await appendToProject(model, {
2037
+ projectDir: args.projectDir,
2038
+ force: args.force,
2039
+ filters: filtersFrom(args)
2040
+ });
2041
+ return text(summaryText("Extended", summary));
2042
+ }
2043
+ );
2044
+ server.registerTool(
2045
+ "list_supported_features",
2046
+ {
2047
+ title: "List supported features",
2048
+ description: "Report the input formats, auth schemes, transports, and limits this tool supports.",
2049
+ inputSchema: {}
2050
+ },
2051
+ async () => {
2052
+ const features = {
2053
+ inputFormats: SUPPORTED_FORMATS,
2054
+ outputLanguages: ["typescript", "python"],
2055
+ modes: {
2056
+ generate: "Scaffold an ownable TypeScript MCP-server project (generate_mcp_server).",
2057
+ serve: "Run a live runtime proxy, no codegen: `mcp-api-translator serve --spec <path>` mounts the spec's operations as MCP tools in-process (repeat --spec to aggregate multiple APIs)."
2058
+ },
2059
+ transports: ["stdio", "http", "both"],
2060
+ authSchemes: [
2061
+ "apiKey (header/query/cookie)",
2062
+ "http bearer",
2063
+ "http basic",
2064
+ "oauth2 (client-credentials grant, or pre-obtained token)"
2065
+ ],
2066
+ curation: ["includeTags", "excludeOperations", "methods", "pathGlob"],
2067
+ append: true,
2068
+ toolCountWarnThreshold: TOOL_COUNT_WARN_THRESHOLD,
2069
+ limitations: [
2070
+ "OAuth2 client-credentials grant is supported (token fetched + cached); no interactive authorization-code flows.",
2071
+ "Generated handlers return JSON/text; no upstream streaming or auto-pagination.",
2072
+ "Postman parameter types are inferred from examples.",
2073
+ "Output quality tracks spec quality (operationIds, descriptions)."
2074
+ ]
2075
+ };
2076
+ return text(JSON.stringify(features, null, 2));
2077
+ }
2078
+ );
2079
+ }
2080
+
2081
+ // src/server.ts
2082
+ function createServer() {
2083
+ const server = new McpServer({
2084
+ name: GENERATOR_NAME,
2085
+ version: GENERATOR_VERSION
2086
+ });
2087
+ registerTools(server);
2088
+ return server;
2089
+ }
2090
+
2091
+ // src/runtime/oauth.ts
2092
+ var cache = /* @__PURE__ */ new Map();
2093
+ var EARLY_REFRESH_MS = 3e4;
2094
+ var DEFAULT_TTL_SECONDS = 3600;
2095
+ async function getClientCredentialsToken(tokenUrl, clientId, clientSecret, fetchImpl, now = () => Date.now()) {
2096
+ const key = `${tokenUrl}|${clientId}`;
2097
+ const hit = cache.get(key);
2098
+ if (hit && hit.expiresAt > now()) return hit.token;
2099
+ const body = new URLSearchParams({
2100
+ grant_type: "client_credentials",
2101
+ client_id: clientId,
2102
+ client_secret: clientSecret
2103
+ }).toString();
2104
+ const response = await fetchImpl(new URL(tokenUrl), {
2105
+ method: "POST",
2106
+ headers: { "content-type": "application/x-www-form-urlencoded" },
2107
+ body
2108
+ });
2109
+ const text2 = await response.text();
2110
+ if (!response.ok) {
2111
+ throw new Error(`OAuth token request failed: HTTP ${response.status} ${text2.slice(0, 300)}`);
2112
+ }
2113
+ const json = JSON.parse(text2);
2114
+ if (!json.access_token) throw new Error("OAuth token response had no access_token");
2115
+ const ttl = typeof json.expires_in === "number" ? json.expires_in : DEFAULT_TTL_SECONDS;
2116
+ cache.set(key, { token: json.access_token, expiresAt: now() + ttl * 1e3 - EARLY_REFRESH_MS });
2117
+ return json.access_token;
2118
+ }
2119
+
2120
+ // src/runtime/client.ts
2121
+ function joinUrl(base, path3) {
2122
+ if (!base) throw new Error("API base URL is not set. Set API_BASE_URL in the environment.");
2123
+ return base.replace(/\/+$/, "") + "/" + path3.replace(/^\/+/, "");
2124
+ }
2125
+ async function applyAuth(headers, url, security, ctx, fetchImpl) {
2126
+ const readEnv = (name) => (ctx.sourceNamespace ? ctx.env[`${ctx.sourceNamespace}_${name}`] : void 0) ?? ctx.env[name];
2127
+ for (const scheme of ctx.securitySchemes) {
2128
+ if (!security.includes(scheme.name)) continue;
2129
+ if (scheme.type === "apiKey") {
2130
+ const value = readEnv(scheme.envVars[0]);
2131
+ if (!value) continue;
2132
+ const param = scheme.paramName ?? "X-API-Key";
2133
+ if (scheme.in === "query") {
2134
+ url.searchParams.set(param, value);
2135
+ } else if (scheme.in === "cookie") {
2136
+ headers["cookie"] = (headers["cookie"] ? headers["cookie"] + "; " : "") + param + "=" + value;
2137
+ } else {
2138
+ headers[param] = value;
2139
+ }
2140
+ } else if (scheme.type === "http" && scheme.scheme?.toLowerCase() === "basic") {
2141
+ const [u, p] = scheme.envVars;
2142
+ const user = readEnv(u);
2143
+ const pass = readEnv(p);
2144
+ if (user && pass) {
2145
+ headers["authorization"] = "Basic " + Buffer.from(user + ":" + pass).toString("base64");
2146
+ }
2147
+ } else if (scheme.tokenUrl) {
2148
+ const clientId = readEnv(scheme.envVars[0]);
2149
+ const clientSecret = readEnv(scheme.envVars[1]);
2150
+ if (clientId && clientSecret) {
2151
+ const token = await getClientCredentialsToken(
2152
+ scheme.tokenUrl,
2153
+ clientId,
2154
+ clientSecret,
2155
+ fetchImpl
2156
+ );
2157
+ headers["authorization"] = "Bearer " + token;
2158
+ }
2159
+ } else {
2160
+ const value = readEnv(scheme.envVars[0]);
2161
+ if (value) headers["authorization"] = "Bearer " + value;
2162
+ }
2163
+ }
2164
+ }
2165
+ async function executePlan(plan, args, ctx, fetchImpl = fetch) {
2166
+ let path3 = plan.pathTemplate;
2167
+ for (const name of plan.pathParams) {
2168
+ const value = args[name];
2169
+ path3 = path3.split("{" + name + "}").join(encodeURIComponent(String(value ?? "")));
2170
+ }
2171
+ const url = new URL(joinUrl(ctx.baseUrl, path3));
2172
+ for (const name of plan.queryParams) {
2173
+ const value = args[name];
2174
+ if (value === void 0 || value === null) continue;
2175
+ if (Array.isArray(value)) {
2176
+ for (const v of value) {
2177
+ if (v !== void 0 && v !== null) url.searchParams.append(name, String(v));
2178
+ }
2179
+ } else {
2180
+ url.searchParams.set(name, String(value));
2181
+ }
2182
+ }
2183
+ const headers = {};
2184
+ for (const name of plan.headerParams) {
2185
+ const value = args[name];
2186
+ if (value === void 0 || value === null) continue;
2187
+ headers[name] = Array.isArray(value) ? value.map(String).join(",") : String(value);
2188
+ }
2189
+ const cookies = [];
2190
+ for (const name of plan.cookieParams) {
2191
+ const value = args[name];
2192
+ if (value !== void 0 && value !== null) {
2193
+ cookies.push(name + "=" + encodeURIComponent(String(value)));
2194
+ }
2195
+ }
2196
+ if (cookies.length > 0) {
2197
+ headers["cookie"] = (headers["cookie"] ? headers["cookie"] + "; " : "") + cookies.join("; ");
2198
+ }
2199
+ let body;
2200
+ if (plan.bodyParam && args[plan.bodyParam] !== void 0) {
2201
+ const raw = args[plan.bodyParam];
2202
+ if (plan.contentType && plan.contentType.indexOf("json") >= 0) {
2203
+ headers["content-type"] = "application/json";
2204
+ body = JSON.stringify(raw);
2205
+ } else {
2206
+ if (plan.contentType) headers["content-type"] = plan.contentType;
2207
+ body = typeof raw === "string" ? raw : JSON.stringify(raw);
2208
+ }
2209
+ }
2210
+ await applyAuth(headers, url, plan.security, ctx, fetchImpl);
2211
+ const response = await fetchImpl(url, { method: plan.method, headers, body });
2212
+ const text2 = await response.text();
2213
+ if (!response.ok) {
2214
+ throw new Error(
2215
+ "HTTP " + response.status + " " + response.statusText + ": " + text2.slice(0, 800)
2216
+ );
2217
+ }
2218
+ return text2;
2219
+ }
2220
+
2221
+ // src/runtime/server.ts
2222
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2223
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
2224
+ var ApiProxy = class {
2225
+ tools = /* @__PURE__ */ new Map();
2226
+ /** Curate a model's operations and register them as live tools (deduping names across mounts). */
2227
+ mount(model, filters = {}) {
2228
+ const reserved = new Set(this.tools.keys());
2229
+ const { operations, filteredOut } = curate(model, filters, reserved);
2230
+ const defaultBaseUrl = model.servers[0] ?? "";
2231
+ const sourceNamespace = envNamespace(model.title);
2232
+ const added = [];
2233
+ for (const op of operations) {
2234
+ const emit = operationToToolEmit(op, model.title);
2235
+ this.tools.set(emit.name, {
2236
+ name: emit.name,
2237
+ description: emit.description,
2238
+ inputSchema: emit.inputSchema,
2239
+ plan: emit.plan,
2240
+ defaultBaseUrl,
2241
+ securitySchemes: model.securitySchemes,
2242
+ sourceNamespace
2243
+ });
2244
+ added.push(emit.name);
2245
+ }
2246
+ const warnings = this.tools.size > TOOL_COUNT_WARN_THRESHOLD ? [
2247
+ `This proxy now exposes ${this.tools.size} tools (> ${TOOL_COUNT_WARN_THRESHOLD}). Large tool counts hurt model tool-selection; narrow with includeTags / methods / pathGlob / excludeOperations, or rely on a client that supports dynamic tool discovery (e.g. Tool Search).`
2248
+ ] : [];
2249
+ const envVars = [
2250
+ `${sourceNamespace}_API_BASE_URL`,
2251
+ ...model.securitySchemes.flatMap((s) => s.envVars).map((v) => `${sourceNamespace}_${v}`)
2252
+ ];
2253
+ return {
2254
+ mounted: added.length,
2255
+ filteredOut,
2256
+ toolNames: added,
2257
+ warnings,
2258
+ sourceNamespace,
2259
+ envVars
2260
+ };
2261
+ }
2262
+ /** Tool descriptors for an MCP `tools/list` response. */
2263
+ listTools() {
2264
+ return [...this.tools.values()].map((t) => ({
2265
+ name: t.name,
2266
+ description: t.description,
2267
+ inputSchema: t.inputSchema
2268
+ }));
2269
+ }
2270
+ get size() {
2271
+ return this.tools.size;
2272
+ }
2273
+ /** Execute a mounted tool against the live upstream. */
2274
+ async call(name, args, env, fetchImpl) {
2275
+ const tool = this.tools.get(name);
2276
+ if (!tool) throw new Error(`Unknown tool: ${name}`);
2277
+ const baseUrl = env[`${tool.sourceNamespace}_API_BASE_URL`] ?? env.API_BASE_URL ?? tool.defaultBaseUrl;
2278
+ const ctx = {
2279
+ baseUrl,
2280
+ securitySchemes: tool.securitySchemes,
2281
+ env,
2282
+ sourceNamespace: tool.sourceNamespace
2283
+ };
2284
+ return executePlan(tool.plan, args, ctx, fetchImpl);
2285
+ }
2286
+ };
2287
+ function createProxyServer() {
2288
+ const proxy = new ApiProxy();
2289
+ const server = new Server(
2290
+ { name: `${GENERATOR_NAME}-proxy`, version: GENERATOR_VERSION },
2291
+ { capabilities: { tools: {} } }
2292
+ );
2293
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: proxy.listTools() }));
2294
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2295
+ try {
2296
+ const text2 = await proxy.call(
2297
+ request.params.name,
2298
+ request.params.arguments ?? {},
2299
+ process.env
2300
+ );
2301
+ return { content: [{ type: "text", text: text2 }] };
2302
+ } catch (err) {
2303
+ return {
2304
+ content: [
2305
+ {
2306
+ type: "text",
2307
+ text: "Error: " + (err instanceof Error ? err.message : String(err))
2308
+ }
2309
+ ],
2310
+ isError: true
2311
+ };
2312
+ }
2313
+ });
2314
+ return { server, proxy };
2315
+ }
2316
+
2317
+ export {
2318
+ validateApiModel,
2319
+ assertValidApiModel,
2320
+ detectFormat,
2321
+ parseSource,
2322
+ SUPPORTED_FORMATS,
2323
+ generateProject,
2324
+ appendToProject,
2325
+ createServer,
2326
+ executePlan,
2327
+ ApiProxy,
2328
+ createProxyServer
2329
+ };
2330
+ //# sourceMappingURL=chunk-KSWSDM52.js.map