dynamic-openapi-mcp 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +324 -0
- package/dist/cli.js +1452 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +134 -0
- package/dist/index.js +1350 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1452 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/server.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
|
|
7
|
+
// src/utils/fetch.ts
|
|
8
|
+
var DEFAULT_OPTIONS = {
|
|
9
|
+
timeout: 3e4,
|
|
10
|
+
retries: 3,
|
|
11
|
+
retryDelay: 1e3,
|
|
12
|
+
retryOn: [429, 500, 502, 503, 504]
|
|
13
|
+
};
|
|
14
|
+
async function fetchWithRetry(url, init, opts) {
|
|
15
|
+
const { timeout, retries, retryDelay, retryOn } = { ...DEFAULT_OPTIONS, ...opts };
|
|
16
|
+
let lastError;
|
|
17
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
18
|
+
try {
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
21
|
+
const response = await fetch(url, {
|
|
22
|
+
...init,
|
|
23
|
+
signal: controller.signal
|
|
24
|
+
});
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
if (retryOn.includes(response.status) && attempt < retries) {
|
|
27
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
28
|
+
const delay = retryAfter ? parseRetryAfter(retryAfter) : retryDelay * Math.pow(2, attempt);
|
|
29
|
+
await sleep(delay);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
return response;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
35
|
+
if (lastError.name === "AbortError") {
|
|
36
|
+
lastError = new Error(`Request timed out after ${timeout}ms: ${url}`);
|
|
37
|
+
}
|
|
38
|
+
if (attempt < retries) {
|
|
39
|
+
await sleep(retryDelay * Math.pow(2, attempt));
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
throw lastError ?? new Error(`Request failed after ${retries + 1} attempts: ${url}`);
|
|
45
|
+
}
|
|
46
|
+
function parseRetryAfter(value) {
|
|
47
|
+
const seconds = Number(value);
|
|
48
|
+
if (!Number.isNaN(seconds)) {
|
|
49
|
+
return Math.min(seconds * 1e3, 6e4);
|
|
50
|
+
}
|
|
51
|
+
const date = Date.parse(value);
|
|
52
|
+
if (!Number.isNaN(date)) {
|
|
53
|
+
return Math.min(Math.max(date - Date.now(), 0), 6e4);
|
|
54
|
+
}
|
|
55
|
+
return 1e3;
|
|
56
|
+
}
|
|
57
|
+
function sleep(ms) {
|
|
58
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/auth/strategies.ts
|
|
62
|
+
var BearerAuth = class {
|
|
63
|
+
constructor(token) {
|
|
64
|
+
this.token = token;
|
|
65
|
+
}
|
|
66
|
+
async apply(_url, init) {
|
|
67
|
+
const headers = new Headers(init.headers);
|
|
68
|
+
headers.set("Authorization", `Bearer ${this.token}`);
|
|
69
|
+
return { ...init, headers };
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var ApiKeyAuth = class {
|
|
73
|
+
constructor(key, paramName, location) {
|
|
74
|
+
this.key = key;
|
|
75
|
+
this.paramName = paramName;
|
|
76
|
+
this.location = location;
|
|
77
|
+
}
|
|
78
|
+
async apply(url, init) {
|
|
79
|
+
switch (this.location) {
|
|
80
|
+
case "header": {
|
|
81
|
+
const headers = new Headers(init.headers);
|
|
82
|
+
headers.set(this.paramName, this.key);
|
|
83
|
+
return { ...init, headers };
|
|
84
|
+
}
|
|
85
|
+
case "query": {
|
|
86
|
+
url.searchParams.set(this.paramName, this.key);
|
|
87
|
+
return init;
|
|
88
|
+
}
|
|
89
|
+
case "cookie": {
|
|
90
|
+
const headers = new Headers(init.headers);
|
|
91
|
+
const existing = headers.get("Cookie") ?? "";
|
|
92
|
+
const encodedKey = encodeURIComponent(this.paramName);
|
|
93
|
+
const encodedValue = encodeURIComponent(this.key);
|
|
94
|
+
const cookie = existing ? `${existing}; ${encodedKey}=${encodedValue}` : `${encodedKey}=${encodedValue}`;
|
|
95
|
+
headers.set("Cookie", cookie);
|
|
96
|
+
return { ...init, headers };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
var BasicAuth = class {
|
|
102
|
+
constructor(username, password) {
|
|
103
|
+
this.username = username;
|
|
104
|
+
this.password = password;
|
|
105
|
+
}
|
|
106
|
+
async apply(_url, init) {
|
|
107
|
+
const credentials = `${this.username}:${this.password}`;
|
|
108
|
+
const encoded = Buffer.from(credentials, "utf-8").toString("base64");
|
|
109
|
+
const headers = new Headers(init.headers);
|
|
110
|
+
headers.set("Authorization", `Basic ${encoded}`);
|
|
111
|
+
return { ...init, headers };
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
var OAuth2ClientCredentials = class {
|
|
115
|
+
constructor(clientId, clientSecret, tokenUrl, scopes = []) {
|
|
116
|
+
this.clientId = clientId;
|
|
117
|
+
this.clientSecret = clientSecret;
|
|
118
|
+
this.tokenUrl = tokenUrl;
|
|
119
|
+
this.scopes = scopes;
|
|
120
|
+
}
|
|
121
|
+
tokenCache = null;
|
|
122
|
+
pendingRefresh = null;
|
|
123
|
+
async apply(_url, init) {
|
|
124
|
+
const token = await this.getToken();
|
|
125
|
+
const headers = new Headers(init.headers);
|
|
126
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
127
|
+
return { ...init, headers };
|
|
128
|
+
}
|
|
129
|
+
async getToken() {
|
|
130
|
+
if (this.tokenCache && Date.now() < this.tokenCache.expiresAt) {
|
|
131
|
+
return this.tokenCache.token;
|
|
132
|
+
}
|
|
133
|
+
if (this.pendingRefresh) {
|
|
134
|
+
return this.pendingRefresh;
|
|
135
|
+
}
|
|
136
|
+
this.pendingRefresh = this.fetchToken();
|
|
137
|
+
try {
|
|
138
|
+
return await this.pendingRefresh;
|
|
139
|
+
} finally {
|
|
140
|
+
this.pendingRefresh = null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async fetchToken() {
|
|
144
|
+
const body = new URLSearchParams({
|
|
145
|
+
grant_type: "client_credentials",
|
|
146
|
+
client_id: this.clientId,
|
|
147
|
+
client_secret: this.clientSecret
|
|
148
|
+
});
|
|
149
|
+
if (this.scopes.length > 0) {
|
|
150
|
+
body.set("scope", this.scopes.join(" "));
|
|
151
|
+
}
|
|
152
|
+
const res = await fetchWithRetry(this.tokenUrl, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
155
|
+
body
|
|
156
|
+
}, { retries: 2, timeout: 15e3 });
|
|
157
|
+
if (!res.ok) {
|
|
158
|
+
const errorBody = await res.text().catch(() => "");
|
|
159
|
+
throw new Error(
|
|
160
|
+
`OAuth2 token request failed: ${res.status} ${res.statusText}${errorBody ? ` - ${errorBody}` : ""}`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
let data;
|
|
164
|
+
try {
|
|
165
|
+
data = await res.json();
|
|
166
|
+
} catch {
|
|
167
|
+
throw new Error("OAuth2 token response is not valid JSON");
|
|
168
|
+
}
|
|
169
|
+
if (typeof data.access_token !== "string" || !data.access_token) {
|
|
170
|
+
throw new Error('OAuth2 token response missing "access_token" field');
|
|
171
|
+
}
|
|
172
|
+
const expiresIn = typeof data.expires_in === "number" ? data.expires_in : 3600;
|
|
173
|
+
const bufferSeconds = Math.min(60, expiresIn * 0.1);
|
|
174
|
+
this.tokenCache = {
|
|
175
|
+
token: data.access_token,
|
|
176
|
+
expiresAt: Date.now() + (expiresIn - bufferSeconds) * 1e3
|
|
177
|
+
};
|
|
178
|
+
return data.access_token;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
var CustomAuth = class {
|
|
182
|
+
constructor(handler) {
|
|
183
|
+
this.handler = handler;
|
|
184
|
+
}
|
|
185
|
+
async apply(url, init) {
|
|
186
|
+
return this.handler(url.toString(), init);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
var CompositeAuth = class {
|
|
190
|
+
constructor(strategies) {
|
|
191
|
+
this.strategies = strategies;
|
|
192
|
+
}
|
|
193
|
+
async apply(url, init) {
|
|
194
|
+
let result = init;
|
|
195
|
+
for (const strategy of this.strategies) {
|
|
196
|
+
result = await strategy.apply(url, result);
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
function createAuthFromScheme(scheme, credential) {
|
|
202
|
+
switch (scheme.type) {
|
|
203
|
+
case "http": {
|
|
204
|
+
if (scheme.scheme === "bearer") {
|
|
205
|
+
return new BearerAuth(credential);
|
|
206
|
+
}
|
|
207
|
+
if (scheme.scheme === "basic") {
|
|
208
|
+
const colonIndex = credential.indexOf(":");
|
|
209
|
+
if (colonIndex === -1) {
|
|
210
|
+
return new BasicAuth(credential, "");
|
|
211
|
+
}
|
|
212
|
+
return new BasicAuth(credential.slice(0, colonIndex), credential.slice(colonIndex + 1));
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
case "apiKey": {
|
|
217
|
+
const location = scheme.in;
|
|
218
|
+
if (!["header", "query", "cookie"].includes(location)) {
|
|
219
|
+
return new ApiKeyAuth(credential, scheme.name, "header");
|
|
220
|
+
}
|
|
221
|
+
return new ApiKeyAuth(credential, scheme.name, location);
|
|
222
|
+
}
|
|
223
|
+
default:
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/auth/resolver.ts
|
|
229
|
+
function resolveAuth(config, securitySchemes) {
|
|
230
|
+
const strategies = [];
|
|
231
|
+
if (config?.custom) {
|
|
232
|
+
return new CustomAuth(config.custom);
|
|
233
|
+
}
|
|
234
|
+
if (config?.bearerToken) {
|
|
235
|
+
strategies.push(new BearerAuth(config.bearerToken));
|
|
236
|
+
}
|
|
237
|
+
if (config?.apiKey) {
|
|
238
|
+
const apiKeyScheme = Object.values(securitySchemes).find(
|
|
239
|
+
(s) => s.type === "apiKey"
|
|
240
|
+
);
|
|
241
|
+
if (apiKeyScheme) {
|
|
242
|
+
strategies.push(new ApiKeyAuth(config.apiKey, apiKeyScheme.name, apiKeyScheme.in));
|
|
243
|
+
} else {
|
|
244
|
+
strategies.push(new ApiKeyAuth(config.apiKey, "X-API-Key", "header"));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (config?.basicAuth) {
|
|
248
|
+
strategies.push(new BasicAuth(config.basicAuth.username, config.basicAuth.password));
|
|
249
|
+
}
|
|
250
|
+
if (config?.oauth2) {
|
|
251
|
+
strategies.push(
|
|
252
|
+
new OAuth2ClientCredentials(
|
|
253
|
+
config.oauth2.clientId,
|
|
254
|
+
config.oauth2.clientSecret,
|
|
255
|
+
config.oauth2.tokenUrl,
|
|
256
|
+
config.oauth2.scopes
|
|
257
|
+
)
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
if (strategies.length > 0) {
|
|
261
|
+
return strategies.length === 1 ? strategies[0] : new CompositeAuth(strategies);
|
|
262
|
+
}
|
|
263
|
+
return resolveAuthFromEnv(securitySchemes);
|
|
264
|
+
}
|
|
265
|
+
function resolveAuthFromEnv(securitySchemes) {
|
|
266
|
+
for (const [name, scheme] of Object.entries(securitySchemes)) {
|
|
267
|
+
const envName = name.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
268
|
+
const envToken = process.env[`OPENAPI_AUTH_${envName}_TOKEN`] ?? process.env[`OPENAPI_AUTH_${envName}_KEY`];
|
|
269
|
+
if (envToken) {
|
|
270
|
+
const auth = createAuthFromScheme(scheme, envToken);
|
|
271
|
+
if (auth) return auth;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const globalToken = process.env["OPENAPI_AUTH_TOKEN"];
|
|
275
|
+
if (globalToken) {
|
|
276
|
+
return new BearerAuth(globalToken);
|
|
277
|
+
}
|
|
278
|
+
const globalApiKey = process.env["OPENAPI_API_KEY"];
|
|
279
|
+
if (globalApiKey) {
|
|
280
|
+
const apiKeyScheme = Object.values(securitySchemes).find(
|
|
281
|
+
(s) => s.type === "apiKey"
|
|
282
|
+
);
|
|
283
|
+
if (apiKeyScheme) {
|
|
284
|
+
return new ApiKeyAuth(globalApiKey, apiKeyScheme.name, apiKeyScheme.in);
|
|
285
|
+
}
|
|
286
|
+
return new ApiKeyAuth(globalApiKey, "X-API-Key", "header");
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/parser/loader.ts
|
|
292
|
+
import { readFile } from "fs/promises";
|
|
293
|
+
import { parse as parseYaml } from "yaml";
|
|
294
|
+
function resolveSource(source) {
|
|
295
|
+
if (typeof source !== "string") {
|
|
296
|
+
return { type: "inline", value: source };
|
|
297
|
+
}
|
|
298
|
+
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
299
|
+
return { type: "url", value: source };
|
|
300
|
+
}
|
|
301
|
+
if (source.trim().startsWith("{") || source.trim().startsWith("openapi")) {
|
|
302
|
+
return { type: "inline", value: source };
|
|
303
|
+
}
|
|
304
|
+
return { type: "file", value: source };
|
|
305
|
+
}
|
|
306
|
+
async function loadSpec(source) {
|
|
307
|
+
const resolved = resolveSource(source);
|
|
308
|
+
switch (resolved.type) {
|
|
309
|
+
case "url": {
|
|
310
|
+
const res = await fetchWithRetry(resolved.value);
|
|
311
|
+
if (!res.ok) {
|
|
312
|
+
throw new Error(`Failed to fetch spec from ${resolved.value}: ${res.status} ${res.statusText}`);
|
|
313
|
+
}
|
|
314
|
+
const text = await res.text();
|
|
315
|
+
return parseSpecText(text, resolved.value);
|
|
316
|
+
}
|
|
317
|
+
case "file": {
|
|
318
|
+
let text;
|
|
319
|
+
try {
|
|
320
|
+
text = await readFile(resolved.value, "utf-8");
|
|
321
|
+
} catch (err) {
|
|
322
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
323
|
+
throw new Error(`Failed to read spec file "${resolved.value}": ${msg}`);
|
|
324
|
+
}
|
|
325
|
+
return parseSpecText(text, resolved.value);
|
|
326
|
+
}
|
|
327
|
+
case "inline": {
|
|
328
|
+
if (typeof resolved.value === "string") {
|
|
329
|
+
return parseSpecText(resolved.value, "(inline)");
|
|
330
|
+
}
|
|
331
|
+
return resolved.value;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function parseSpecText(text, source) {
|
|
336
|
+
const trimmed = text.trim();
|
|
337
|
+
if (trimmed.startsWith("{")) {
|
|
338
|
+
try {
|
|
339
|
+
return JSON.parse(trimmed);
|
|
340
|
+
} catch (err) {
|
|
341
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
342
|
+
throw new Error(`Failed to parse JSON spec from ${source}: ${msg}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
return parseYaml(trimmed);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
349
|
+
throw new Error(`Failed to parse YAML spec from ${source}: ${msg}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/parser/resolver.ts
|
|
354
|
+
import { validate, dereference } from "@readme/openapi-parser";
|
|
355
|
+
async function resolveSpec(doc) {
|
|
356
|
+
const validation = await validate(structuredClone(doc));
|
|
357
|
+
if (!validation.valid) {
|
|
358
|
+
const validationResult = validation;
|
|
359
|
+
const errors = validationResult.errors ?? [];
|
|
360
|
+
throw new Error(`Invalid OpenAPI spec: ${JSON.stringify(errors)}`);
|
|
361
|
+
}
|
|
362
|
+
const dereferenced = await dereference(structuredClone(doc));
|
|
363
|
+
const operations = extractOperations(dereferenced);
|
|
364
|
+
const schemas = extractSchemas(dereferenced);
|
|
365
|
+
const securitySchemes = extractSecuritySchemes(dereferenced);
|
|
366
|
+
const servers = extractServers(dereferenced);
|
|
367
|
+
const tags = extractTags(dereferenced);
|
|
368
|
+
const externalDocs = extractExternalDocs(dereferenced.externalDocs);
|
|
369
|
+
return {
|
|
370
|
+
title: dereferenced.info.title,
|
|
371
|
+
version: dereferenced.info.version,
|
|
372
|
+
description: dereferenced.info.description,
|
|
373
|
+
servers,
|
|
374
|
+
operations,
|
|
375
|
+
schemas,
|
|
376
|
+
securitySchemes,
|
|
377
|
+
tags,
|
|
378
|
+
externalDocs,
|
|
379
|
+
raw: dereferenced
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
var HTTP_METHODS = ["get", "post", "put", "delete", "patch", "head", "options", "trace"];
|
|
383
|
+
function extractOperations(doc) {
|
|
384
|
+
const operations = [];
|
|
385
|
+
const paths = doc.paths ?? {};
|
|
386
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
387
|
+
if (!pathItem) continue;
|
|
388
|
+
const pathParams = pathItem.parameters ?? [];
|
|
389
|
+
for (const method of HTTP_METHODS) {
|
|
390
|
+
const operation = pathItem[method];
|
|
391
|
+
if (!operation) continue;
|
|
392
|
+
const operationParams = operation.parameters ?? [];
|
|
393
|
+
const allParams = mergeParameters(pathParams, operationParams);
|
|
394
|
+
const operationId = operation.operationId ?? generateOperationId(method, path);
|
|
395
|
+
const parameters = allParams.map((p) => ({
|
|
396
|
+
name: p.name,
|
|
397
|
+
in: p.in,
|
|
398
|
+
required: p.required ?? p.in === "path",
|
|
399
|
+
description: p.description,
|
|
400
|
+
schema: p.schema ?? { type: "string" },
|
|
401
|
+
example: p.example,
|
|
402
|
+
examples: p.examples ? extractExamples(p.examples) : void 0,
|
|
403
|
+
deprecated: p.deprecated
|
|
404
|
+
}));
|
|
405
|
+
let requestBody;
|
|
406
|
+
if (operation.requestBody) {
|
|
407
|
+
const rb = operation.requestBody;
|
|
408
|
+
const content = {};
|
|
409
|
+
for (const [mediaType, mediaObj] of Object.entries(rb.content ?? {})) {
|
|
410
|
+
if (mediaObj.schema) {
|
|
411
|
+
content[mediaType] = {
|
|
412
|
+
schema: mediaObj.schema,
|
|
413
|
+
example: mediaObj.example,
|
|
414
|
+
examples: mediaObj.examples ? extractExamples(mediaObj.examples) : void 0
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
requestBody = {
|
|
419
|
+
required: rb.required ?? false,
|
|
420
|
+
description: rb.description,
|
|
421
|
+
content
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
const responses = {};
|
|
425
|
+
for (const [code, resp] of Object.entries(operation.responses ?? {})) {
|
|
426
|
+
responses[code] = extractResponse(resp);
|
|
427
|
+
}
|
|
428
|
+
operations.push({
|
|
429
|
+
operationId,
|
|
430
|
+
method: method.toUpperCase(),
|
|
431
|
+
path,
|
|
432
|
+
summary: operation.summary,
|
|
433
|
+
description: operation.description,
|
|
434
|
+
deprecated: operation.deprecated,
|
|
435
|
+
parameters,
|
|
436
|
+
requestBody,
|
|
437
|
+
responses,
|
|
438
|
+
security: operation.security ?? doc.security ?? [],
|
|
439
|
+
tags: operation.tags ?? [],
|
|
440
|
+
externalDocs: extractExternalDocs(operation.externalDocs)
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return operations;
|
|
445
|
+
}
|
|
446
|
+
function extractResponse(resp) {
|
|
447
|
+
const parsed = {
|
|
448
|
+
description: resp.description ?? "",
|
|
449
|
+
content: {}
|
|
450
|
+
};
|
|
451
|
+
if (resp.content) {
|
|
452
|
+
for (const [mediaType, mediaObj] of Object.entries(resp.content)) {
|
|
453
|
+
parsed.content[mediaType] = {
|
|
454
|
+
schema: mediaObj.schema
|
|
455
|
+
};
|
|
456
|
+
if (!parsed.schema && mediaObj.schema) {
|
|
457
|
+
parsed.schema = mediaObj.schema;
|
|
458
|
+
parsed.mediaType = mediaType;
|
|
459
|
+
}
|
|
460
|
+
if (parsed.example === void 0 && mediaObj.example !== void 0) {
|
|
461
|
+
parsed.example = mediaObj.example;
|
|
462
|
+
}
|
|
463
|
+
if (!parsed.examples && mediaObj.examples) {
|
|
464
|
+
parsed.examples = extractExamples(mediaObj.examples);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (resp.links) {
|
|
469
|
+
parsed.links = extractLinks(resp.links);
|
|
470
|
+
}
|
|
471
|
+
return parsed;
|
|
472
|
+
}
|
|
473
|
+
function extractExamples(examples) {
|
|
474
|
+
const result = {};
|
|
475
|
+
for (const [name, ex] of Object.entries(examples)) {
|
|
476
|
+
result[name] = {
|
|
477
|
+
summary: ex.summary,
|
|
478
|
+
description: ex.description,
|
|
479
|
+
value: ex.value
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
return result;
|
|
483
|
+
}
|
|
484
|
+
function extractLinks(links) {
|
|
485
|
+
const result = {};
|
|
486
|
+
for (const [name, link] of Object.entries(links)) {
|
|
487
|
+
result[name] = {
|
|
488
|
+
operationId: link.operationId,
|
|
489
|
+
operationRef: link.operationRef,
|
|
490
|
+
parameters: link.parameters,
|
|
491
|
+
description: link.description
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
return result;
|
|
495
|
+
}
|
|
496
|
+
function generateOperationId(method, path) {
|
|
497
|
+
const cleaned = path.replace(/\{([^}]+)\}/g, "by_$1").replace(/[^a-zA-Z0-9]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
|
|
498
|
+
return `${method}_${cleaned}`.toLowerCase();
|
|
499
|
+
}
|
|
500
|
+
function mergeParameters(pathParams, operationParams) {
|
|
501
|
+
const merged = /* @__PURE__ */ new Map();
|
|
502
|
+
for (const p of pathParams) {
|
|
503
|
+
merged.set(`${p.in}:${p.name}`, p);
|
|
504
|
+
}
|
|
505
|
+
for (const p of operationParams) {
|
|
506
|
+
merged.set(`${p.in}:${p.name}`, p);
|
|
507
|
+
}
|
|
508
|
+
return Array.from(merged.values());
|
|
509
|
+
}
|
|
510
|
+
function extractSchemas(doc) {
|
|
511
|
+
const schemas = {};
|
|
512
|
+
const components = doc.components?.schemas ?? {};
|
|
513
|
+
for (const [name, schema] of Object.entries(components)) {
|
|
514
|
+
schemas[name] = schema;
|
|
515
|
+
}
|
|
516
|
+
return schemas;
|
|
517
|
+
}
|
|
518
|
+
function extractSecuritySchemes(doc) {
|
|
519
|
+
const schemes = {};
|
|
520
|
+
const components = doc.components?.securitySchemes ?? {};
|
|
521
|
+
for (const [name, scheme] of Object.entries(components)) {
|
|
522
|
+
schemes[name] = scheme;
|
|
523
|
+
}
|
|
524
|
+
return schemes;
|
|
525
|
+
}
|
|
526
|
+
function extractServers(doc) {
|
|
527
|
+
return (doc.servers ?? []).map((s) => {
|
|
528
|
+
const server = { url: s.url };
|
|
529
|
+
if (s.description) server.description = s.description;
|
|
530
|
+
if (s.variables) {
|
|
531
|
+
server.variables = {};
|
|
532
|
+
for (const [name, v] of Object.entries(s.variables)) {
|
|
533
|
+
server.variables[name] = {
|
|
534
|
+
default: v.default,
|
|
535
|
+
enum: v.enum,
|
|
536
|
+
description: v.description
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return server;
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
function extractTags(doc) {
|
|
544
|
+
return (doc.tags ?? []).map((t) => {
|
|
545
|
+
const tag = { name: t.name };
|
|
546
|
+
if (t.description) tag.description = t.description;
|
|
547
|
+
if (t.externalDocs) {
|
|
548
|
+
tag.externalDocs = {
|
|
549
|
+
url: t.externalDocs.url,
|
|
550
|
+
description: t.externalDocs.description
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return tag;
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
function extractExternalDocs(docs) {
|
|
557
|
+
if (!docs) return void 0;
|
|
558
|
+
return {
|
|
559
|
+
url: docs.url,
|
|
560
|
+
description: docs.description
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/http/response-handler.ts
|
|
565
|
+
async function handleResponse(response) {
|
|
566
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
567
|
+
const status = response.status;
|
|
568
|
+
if (status === 204) {
|
|
569
|
+
return [{ type: "text", text: "No Content (204)" }];
|
|
570
|
+
}
|
|
571
|
+
if (contentType.includes("image/")) {
|
|
572
|
+
const buffer = await response.arrayBuffer();
|
|
573
|
+
const base64 = Buffer.from(buffer).toString("base64");
|
|
574
|
+
return [
|
|
575
|
+
{
|
|
576
|
+
type: "image",
|
|
577
|
+
data: base64,
|
|
578
|
+
mimeType: contentType.split(";")[0].trim()
|
|
579
|
+
}
|
|
580
|
+
];
|
|
581
|
+
}
|
|
582
|
+
const text = await response.text();
|
|
583
|
+
if (contentType.includes("json")) {
|
|
584
|
+
try {
|
|
585
|
+
const parsed = JSON.parse(text);
|
|
586
|
+
return [{ type: "text", text: JSON.stringify(parsed, null, 2) }];
|
|
587
|
+
} catch {
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return [{ type: "text", text: text || `(empty response, status ${status})` }];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/http/client.ts
|
|
594
|
+
function resolveServerUrl(server, variableOverrides) {
|
|
595
|
+
let url = server.url;
|
|
596
|
+
if (server.variables) {
|
|
597
|
+
for (const [name, variable] of Object.entries(server.variables)) {
|
|
598
|
+
const value = variableOverrides?.[name] ?? variable.default;
|
|
599
|
+
if (variable.enum && !variable.enum.includes(value)) {
|
|
600
|
+
throw new Error(`Invalid value "${value}" for server variable "${name}". Allowed: ${variable.enum.join(", ")}`);
|
|
601
|
+
}
|
|
602
|
+
url = url.replaceAll(`{${name}}`, value);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return normalizeUrl(url);
|
|
606
|
+
}
|
|
607
|
+
function normalizeUrl(url) {
|
|
608
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
609
|
+
url = `https://${url}`;
|
|
610
|
+
}
|
|
611
|
+
return url.replace(/\/$/, "");
|
|
612
|
+
}
|
|
613
|
+
function resolveBaseUrl(spec, overrideBaseUrl, serverIndex) {
|
|
614
|
+
if (overrideBaseUrl) return overrideBaseUrl.replace(/\/$/, "");
|
|
615
|
+
const index = serverIndex ?? 0;
|
|
616
|
+
const server = spec.servers[index];
|
|
617
|
+
if (server) {
|
|
618
|
+
return resolveServerUrl(server);
|
|
619
|
+
}
|
|
620
|
+
throw new Error("No server URL found in spec and no baseUrl provided");
|
|
621
|
+
}
|
|
622
|
+
async function executeOperation(operation, args, config) {
|
|
623
|
+
const validationErrors = validateRequiredParams(operation, args);
|
|
624
|
+
if (validationErrors.length > 0) {
|
|
625
|
+
return [{ type: "text", text: `Validation errors:
|
|
626
|
+
${validationErrors.join("\n")}` }];
|
|
627
|
+
}
|
|
628
|
+
let path = operation.path;
|
|
629
|
+
for (const param of operation.parameters) {
|
|
630
|
+
if (param.in === "path" && args[param.name] !== void 0) {
|
|
631
|
+
const value = encodeURIComponent(String(args[param.name]));
|
|
632
|
+
path = path.replaceAll(`{${param.name}}`, value);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const url = new URL(`${config.baseUrl}${path}`);
|
|
636
|
+
for (const param of operation.parameters) {
|
|
637
|
+
if (param.in === "query" && args[param.name] !== void 0) {
|
|
638
|
+
const val = args[param.name];
|
|
639
|
+
if (Array.isArray(val)) {
|
|
640
|
+
for (const item of val) {
|
|
641
|
+
url.searchParams.append(param.name, String(item));
|
|
642
|
+
}
|
|
643
|
+
} else {
|
|
644
|
+
url.searchParams.set(param.name, String(val));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const headers = new Headers(config.defaultHeaders);
|
|
649
|
+
const produces = getResponseMediaTypes(operation);
|
|
650
|
+
headers.set("Accept", produces.length > 0 ? produces.join(", ") : "application/json");
|
|
651
|
+
for (const param of operation.parameters) {
|
|
652
|
+
if (param.in === "header" && args[param.name] !== void 0) {
|
|
653
|
+
headers.set(param.name, String(args[param.name]));
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
let body;
|
|
657
|
+
if (args["body"] !== void 0 && operation.requestBody) {
|
|
658
|
+
const contentType = getRequestContentType(operation);
|
|
659
|
+
headers.set("Content-Type", contentType);
|
|
660
|
+
try {
|
|
661
|
+
body = JSON.stringify(args["body"]);
|
|
662
|
+
} catch {
|
|
663
|
+
return [{ type: "text", text: "Error: request body could not be serialized to JSON" }];
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
let init = {
|
|
667
|
+
method: operation.method,
|
|
668
|
+
headers,
|
|
669
|
+
body
|
|
670
|
+
};
|
|
671
|
+
if (config.auth) {
|
|
672
|
+
try {
|
|
673
|
+
init = await config.auth.apply(url, init);
|
|
674
|
+
} catch (error) {
|
|
675
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
676
|
+
return [{ type: "text", text: `Authentication failed: ${msg}` }];
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
const response = await fetchWithRetry(url.toString(), init, config.fetchOptions);
|
|
681
|
+
const statusPrefix = `HTTP ${response.status} ${response.statusText}
|
|
682
|
+
|
|
683
|
+
`;
|
|
684
|
+
const content = await handleResponse(response);
|
|
685
|
+
if (content.length > 0 && content[0].type === "text") {
|
|
686
|
+
content[0] = { type: "text", text: statusPrefix + content[0].text };
|
|
687
|
+
}
|
|
688
|
+
return content;
|
|
689
|
+
} catch (error) {
|
|
690
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
691
|
+
return [{ type: "text", text: `Request failed: ${msg}` }];
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
function validateRequiredParams(operation, args) {
|
|
695
|
+
const errors = [];
|
|
696
|
+
for (const param of operation.parameters) {
|
|
697
|
+
if (param.required && args[param.name] === void 0) {
|
|
698
|
+
errors.push(`- Missing required ${param.in} parameter: "${param.name}"`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (operation.requestBody?.required && args["body"] === void 0) {
|
|
702
|
+
errors.push("- Missing required request body");
|
|
703
|
+
}
|
|
704
|
+
return errors;
|
|
705
|
+
}
|
|
706
|
+
function getResponseMediaTypes(operation) {
|
|
707
|
+
const types = /* @__PURE__ */ new Set();
|
|
708
|
+
for (const resp of Object.values(operation.responses)) {
|
|
709
|
+
if (resp.content) {
|
|
710
|
+
for (const mediaType of Object.keys(resp.content)) {
|
|
711
|
+
types.add(mediaType);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return Array.from(types);
|
|
716
|
+
}
|
|
717
|
+
function getRequestContentType(operation) {
|
|
718
|
+
if (!operation.requestBody?.content) return "application/json";
|
|
719
|
+
const mediaTypes = Object.keys(operation.requestBody.content);
|
|
720
|
+
if (mediaTypes.includes("application/json")) return "application/json";
|
|
721
|
+
return mediaTypes[0] ?? "application/json";
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// src/mapper/tools.ts
|
|
725
|
+
import { z as z2 } from "zod";
|
|
726
|
+
|
|
727
|
+
// src/mapper/schema-converter.ts
|
|
728
|
+
import { z } from "zod";
|
|
729
|
+
function buildToolInputSchema(operation) {
|
|
730
|
+
const shape = {};
|
|
731
|
+
for (const param of operation.parameters) {
|
|
732
|
+
let zodSchema = convertToZod(param.schema);
|
|
733
|
+
zodSchema = zodSchema.describe(buildParamDescription(param));
|
|
734
|
+
if (!param.required && !hasDefault(zodSchema)) {
|
|
735
|
+
zodSchema = zodSchema.optional();
|
|
736
|
+
}
|
|
737
|
+
shape[param.name] = zodSchema;
|
|
738
|
+
}
|
|
739
|
+
if (operation.requestBody) {
|
|
740
|
+
const jsonContent = operation.requestBody.content["application/json"] ?? operation.requestBody.content[Object.keys(operation.requestBody.content)[0]];
|
|
741
|
+
if (jsonContent?.schema) {
|
|
742
|
+
let bodySchema = convertToZod(jsonContent.schema);
|
|
743
|
+
const bodyParts = [];
|
|
744
|
+
if (operation.requestBody.description) bodyParts.push(operation.requestBody.description);
|
|
745
|
+
if (jsonContent.example !== void 0) {
|
|
746
|
+
bodyParts.push(`Example: ${truncateExample(jsonContent.example)}`);
|
|
747
|
+
}
|
|
748
|
+
if (bodyParts.length > 0) {
|
|
749
|
+
bodySchema = bodySchema.describe(bodyParts.join(" | "));
|
|
750
|
+
}
|
|
751
|
+
if (!operation.requestBody.required) {
|
|
752
|
+
bodySchema = bodySchema.optional();
|
|
753
|
+
}
|
|
754
|
+
shape["body"] = bodySchema;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return shape;
|
|
758
|
+
}
|
|
759
|
+
function buildParamDescription(param) {
|
|
760
|
+
const parts = [];
|
|
761
|
+
if (param.description) parts.push(param.description);
|
|
762
|
+
if (param.deprecated) parts.push("[DEPRECATED]");
|
|
763
|
+
if (param.schema.format) parts.push(`format: ${param.schema.format}`);
|
|
764
|
+
if (param.schema.pattern) parts.push(`pattern: ${param.schema.pattern}`);
|
|
765
|
+
if (param.example !== void 0) parts.push(`Example: ${truncateExample(param.example)}`);
|
|
766
|
+
return parts.join(" | ") || param.schema.type || "any";
|
|
767
|
+
}
|
|
768
|
+
function truncateExample(value, maxLen = 100) {
|
|
769
|
+
const str = typeof value === "string" ? value : JSON.stringify(value);
|
|
770
|
+
if (str.length <= maxLen) return str;
|
|
771
|
+
return str.slice(0, maxLen - 3) + "...";
|
|
772
|
+
}
|
|
773
|
+
function convertToZod(schema) {
|
|
774
|
+
if ("const" in schema && schema.const !== void 0) {
|
|
775
|
+
return z.literal(schema.const);
|
|
776
|
+
}
|
|
777
|
+
if (schema.enum) {
|
|
778
|
+
return applyDefault(convertEnum(schema.enum), schema);
|
|
779
|
+
}
|
|
780
|
+
if (schema.type === "array") {
|
|
781
|
+
const items = schema.items ? convertToZod(schema.items) : z.unknown();
|
|
782
|
+
let arr = z.array(items);
|
|
783
|
+
if (schema.minItems !== void 0) arr = arr.min(schema.minItems);
|
|
784
|
+
if (schema.maxItems !== void 0) arr = arr.max(schema.maxItems);
|
|
785
|
+
return applyDefault(arr, schema);
|
|
786
|
+
}
|
|
787
|
+
if (schema.type === "object" || schema.properties) {
|
|
788
|
+
const objShape = {};
|
|
789
|
+
if (schema.properties) {
|
|
790
|
+
const requiredSet = new Set(schema.required ?? []);
|
|
791
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
792
|
+
const propSchema = value;
|
|
793
|
+
let zodProp = convertToZod(propSchema);
|
|
794
|
+
if (propSchema.description) {
|
|
795
|
+
zodProp = zodProp.describe(propSchema.description);
|
|
796
|
+
}
|
|
797
|
+
if (!requiredSet.has(key) && !hasDefault(zodProp)) {
|
|
798
|
+
zodProp = zodProp.optional();
|
|
799
|
+
}
|
|
800
|
+
objShape[key] = zodProp;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (schema.additionalProperties === true || schema.additionalProperties && typeof schema.additionalProperties !== "boolean") {
|
|
804
|
+
return applyDefault(z.object(objShape).passthrough(), schema);
|
|
805
|
+
}
|
|
806
|
+
return applyDefault(z.object(objShape), schema);
|
|
807
|
+
}
|
|
808
|
+
if (schema.allOf) {
|
|
809
|
+
const schemas = schema.allOf.map(convertToZod);
|
|
810
|
+
if (schemas.length === 1) return schemas[0];
|
|
811
|
+
let result = schemas[0];
|
|
812
|
+
for (let i = 1; i < schemas.length; i++) {
|
|
813
|
+
result = z.intersection(result, schemas[i]);
|
|
814
|
+
}
|
|
815
|
+
return result;
|
|
816
|
+
}
|
|
817
|
+
if (schema.oneOf) {
|
|
818
|
+
const schemas = schema.oneOf.map(convertToZod);
|
|
819
|
+
if (schemas.length === 1) return schemas[0];
|
|
820
|
+
const [a, b, ...rest] = schemas;
|
|
821
|
+
return z.union([a, b, ...rest]);
|
|
822
|
+
}
|
|
823
|
+
if (schema.anyOf) {
|
|
824
|
+
const schemas = schema.anyOf.map(convertToZod);
|
|
825
|
+
if (schemas.length === 1) return schemas[0];
|
|
826
|
+
const [a, b, ...rest] = schemas;
|
|
827
|
+
return z.union([a, b, ...rest]);
|
|
828
|
+
}
|
|
829
|
+
const base = convertBaseType(schema);
|
|
830
|
+
return applyDefault(base, schema);
|
|
831
|
+
}
|
|
832
|
+
function convertBaseType(schema) {
|
|
833
|
+
switch (schema.type) {
|
|
834
|
+
case "string":
|
|
835
|
+
return applyStringConstraints(z.string(), schema);
|
|
836
|
+
case "number":
|
|
837
|
+
case "integer":
|
|
838
|
+
return applyNumberConstraints(z.number(), schema);
|
|
839
|
+
case "boolean":
|
|
840
|
+
return z.boolean();
|
|
841
|
+
default:
|
|
842
|
+
return z.unknown();
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
var NATIVE_STRING_FORMATS = {
|
|
846
|
+
email: (s) => s.email(),
|
|
847
|
+
url: (s) => s.url(),
|
|
848
|
+
uuid: (s) => s.uuid(),
|
|
849
|
+
datetime: (s) => s.datetime(),
|
|
850
|
+
"date-time": (s) => s.datetime(),
|
|
851
|
+
date: (s) => s.date()
|
|
852
|
+
};
|
|
853
|
+
function applyStringConstraints(zStr, schema) {
|
|
854
|
+
if (schema.minLength !== void 0) zStr = zStr.min(schema.minLength);
|
|
855
|
+
if (schema.maxLength !== void 0) zStr = zStr.max(schema.maxLength);
|
|
856
|
+
if (schema.pattern) {
|
|
857
|
+
try {
|
|
858
|
+
const re = new RegExp(schema.pattern);
|
|
859
|
+
zStr = zStr.regex(re);
|
|
860
|
+
} catch {
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (schema.format) {
|
|
864
|
+
const nativeFn = NATIVE_STRING_FORMATS[schema.format];
|
|
865
|
+
if (nativeFn) {
|
|
866
|
+
zStr = nativeFn(zStr);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return zStr;
|
|
870
|
+
}
|
|
871
|
+
function applyNumberConstraints(zNum, schema) {
|
|
872
|
+
if (schema.type === "integer") zNum = zNum.int();
|
|
873
|
+
if (schema.minimum !== void 0) {
|
|
874
|
+
if (typeof schema.exclusiveMinimum === "boolean" && schema.exclusiveMinimum) {
|
|
875
|
+
zNum = zNum.gt(schema.minimum);
|
|
876
|
+
} else {
|
|
877
|
+
zNum = zNum.min(schema.minimum);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (schema.maximum !== void 0) {
|
|
881
|
+
if (typeof schema.exclusiveMaximum === "boolean" && schema.exclusiveMaximum) {
|
|
882
|
+
zNum = zNum.lt(schema.maximum);
|
|
883
|
+
} else {
|
|
884
|
+
zNum = zNum.max(schema.maximum);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (typeof schema.exclusiveMinimum === "number") {
|
|
888
|
+
zNum = zNum.gt(schema.exclusiveMinimum);
|
|
889
|
+
}
|
|
890
|
+
if (typeof schema.exclusiveMaximum === "number") {
|
|
891
|
+
zNum = zNum.lt(schema.exclusiveMaximum);
|
|
892
|
+
}
|
|
893
|
+
return zNum;
|
|
894
|
+
}
|
|
895
|
+
function applyDefault(zSchema, schema) {
|
|
896
|
+
if (schema.default !== void 0) {
|
|
897
|
+
return zSchema.default(schema.default);
|
|
898
|
+
}
|
|
899
|
+
return zSchema;
|
|
900
|
+
}
|
|
901
|
+
function hasDefault(schema) {
|
|
902
|
+
return schema instanceof z.ZodDefault;
|
|
903
|
+
}
|
|
904
|
+
function convertEnum(values) {
|
|
905
|
+
const allStrings = values.every((v) => typeof v === "string");
|
|
906
|
+
if (allStrings && values.length >= 2) {
|
|
907
|
+
const [first, second, ...rest] = values;
|
|
908
|
+
return z.enum([first, second, ...rest]);
|
|
909
|
+
}
|
|
910
|
+
if (allStrings && values.length === 1) {
|
|
911
|
+
return z.literal(values[0]);
|
|
912
|
+
}
|
|
913
|
+
if (values.length === 1) {
|
|
914
|
+
return z.literal(values[0]);
|
|
915
|
+
}
|
|
916
|
+
const schemas = values.map((v) => z.literal(v));
|
|
917
|
+
if (schemas.length >= 2) {
|
|
918
|
+
const [a, b, ...rest] = schemas;
|
|
919
|
+
return z.union([a, b, ...rest]);
|
|
920
|
+
}
|
|
921
|
+
return z.unknown();
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// src/utils/naming.ts
|
|
925
|
+
function sanitizeToolName(name) {
|
|
926
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "").slice(0, 64);
|
|
927
|
+
}
|
|
928
|
+
function truncateDescription(text, maxLength = 200) {
|
|
929
|
+
if (!text) return "";
|
|
930
|
+
if (text.length <= maxLength) return text;
|
|
931
|
+
return text.slice(0, maxLength - 3) + "...";
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// src/mapper/tools.ts
|
|
935
|
+
function registerTools(server, spec, httpConfig) {
|
|
936
|
+
for (const operation of spec.operations) {
|
|
937
|
+
const toolName = sanitizeToolName(operation.operationId);
|
|
938
|
+
const rawDescription = (operation.summary ?? operation.description ?? "").trim().replace(/\s+/g, " ");
|
|
939
|
+
const prefix = operation.deprecated ? "[DEPRECATED] " : "";
|
|
940
|
+
const description = truncateDescription(`${prefix}${rawDescription}`) || `${operation.method} ${operation.path}`;
|
|
941
|
+
const shape = buildToolInputSchema(operation);
|
|
942
|
+
server.tool(
|
|
943
|
+
toolName,
|
|
944
|
+
description,
|
|
945
|
+
shape,
|
|
946
|
+
async (args) => {
|
|
947
|
+
const content = await executeOperation(operation, args, httpConfig);
|
|
948
|
+
return { content };
|
|
949
|
+
}
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
if (spec.servers.length > 0) {
|
|
953
|
+
registerSetEnvironmentTool(server, spec, httpConfig);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
function registerSetEnvironmentTool(server, spec, httpConfig) {
|
|
957
|
+
const shape = {
|
|
958
|
+
server_index: z2.number().int().min(0).optional().describe("Index of the server to use (0-based)"),
|
|
959
|
+
server_url: z2.string().optional().describe("Direct URL override (takes precedence over server_index)"),
|
|
960
|
+
variables: z2.record(z2.string()).optional().describe('Server variable overrides (e.g. {"environment": "staging"})')
|
|
961
|
+
};
|
|
962
|
+
server.tool(
|
|
963
|
+
"set_environment",
|
|
964
|
+
"Switch the active API server/environment. Lists available servers when called without arguments.",
|
|
965
|
+
shape,
|
|
966
|
+
async (args) => {
|
|
967
|
+
const lines = [];
|
|
968
|
+
if (args.server_url) {
|
|
969
|
+
httpConfig.baseUrl = args.server_url.replace(/\/$/, "");
|
|
970
|
+
lines.push(`Active server set to: ${httpConfig.baseUrl}`);
|
|
971
|
+
} else if (args.server_index !== void 0) {
|
|
972
|
+
const server2 = spec.servers[args.server_index];
|
|
973
|
+
if (!server2) {
|
|
974
|
+
return {
|
|
975
|
+
content: [{ type: "text", text: `Invalid server_index ${args.server_index}. Available: 0-${spec.servers.length - 1}` }]
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
httpConfig.baseUrl = resolveServerUrl(server2, args.variables);
|
|
979
|
+
lines.push(`Active server set to: ${httpConfig.baseUrl}`);
|
|
980
|
+
if (server2.description) lines.push(` (${server2.description})`);
|
|
981
|
+
} else if (args.variables) {
|
|
982
|
+
const current = spec.servers.find((s) => {
|
|
983
|
+
try {
|
|
984
|
+
return resolveServerUrl(s) === httpConfig.baseUrl;
|
|
985
|
+
} catch {
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
if (current) {
|
|
990
|
+
httpConfig.baseUrl = resolveServerUrl(current, args.variables);
|
|
991
|
+
lines.push(`Active server re-resolved to: ${httpConfig.baseUrl}`);
|
|
992
|
+
} else {
|
|
993
|
+
return {
|
|
994
|
+
content: [{ type: "text", text: "Cannot apply variables: current baseUrl does not match any known server. Use server_index or server_url instead." }]
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
} else {
|
|
998
|
+
lines.push(`Current active server: ${httpConfig.baseUrl}`);
|
|
999
|
+
}
|
|
1000
|
+
lines.push("", "Available servers:");
|
|
1001
|
+
for (let i = 0; i < spec.servers.length; i++) {
|
|
1002
|
+
const s = spec.servers[i];
|
|
1003
|
+
const resolved = tryResolveUrl(s);
|
|
1004
|
+
const active = resolved === httpConfig.baseUrl ? " [active]" : "";
|
|
1005
|
+
const desc = s.description ? ` \u2014 ${s.description}` : "";
|
|
1006
|
+
lines.push(` [${i}] ${s.url}${desc}${active}`);
|
|
1007
|
+
if (s.variables) {
|
|
1008
|
+
for (const [name, v] of Object.entries(s.variables)) {
|
|
1009
|
+
const enumStr = v.enum ? ` enum=[${v.enum.join(",")}]` : "";
|
|
1010
|
+
lines.push(` {${name}}: default="${v.default}"${enumStr}${v.description ? ` \u2014 ${v.description}` : ""}`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return {
|
|
1015
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
function tryResolveUrl(server) {
|
|
1021
|
+
try {
|
|
1022
|
+
return resolveServerUrl(server);
|
|
1023
|
+
} catch {
|
|
1024
|
+
return null;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// src/mapper/resources.ts
|
|
1029
|
+
function registerResources(server, spec) {
|
|
1030
|
+
server.resource(
|
|
1031
|
+
"openapi-spec",
|
|
1032
|
+
"openapi://spec",
|
|
1033
|
+
{
|
|
1034
|
+
description: `Full OpenAPI spec for ${spec.title} v${spec.version}`,
|
|
1035
|
+
mimeType: "application/json"
|
|
1036
|
+
},
|
|
1037
|
+
async () => ({
|
|
1038
|
+
contents: [
|
|
1039
|
+
{
|
|
1040
|
+
uri: "openapi://spec",
|
|
1041
|
+
mimeType: "application/json",
|
|
1042
|
+
text: JSON.stringify(spec.raw, null, 2)
|
|
1043
|
+
}
|
|
1044
|
+
]
|
|
1045
|
+
})
|
|
1046
|
+
);
|
|
1047
|
+
for (const [name, schema] of Object.entries(spec.schemas)) {
|
|
1048
|
+
const safeName = encodeURIComponent(name);
|
|
1049
|
+
const uri = `openapi://schemas/${safeName}`;
|
|
1050
|
+
server.resource(
|
|
1051
|
+
`schema-${safeName}`,
|
|
1052
|
+
uri,
|
|
1053
|
+
{
|
|
1054
|
+
description: (schema.description ?? `Schema: ${name}`).slice(0, 200),
|
|
1055
|
+
mimeType: "application/json"
|
|
1056
|
+
},
|
|
1057
|
+
async () => ({
|
|
1058
|
+
contents: [
|
|
1059
|
+
{
|
|
1060
|
+
uri,
|
|
1061
|
+
mimeType: "application/json",
|
|
1062
|
+
text: JSON.stringify(schema, null, 2)
|
|
1063
|
+
}
|
|
1064
|
+
]
|
|
1065
|
+
})
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
if (spec.servers.length > 0) {
|
|
1069
|
+
server.resource(
|
|
1070
|
+
"openapi-servers",
|
|
1071
|
+
"openapi://servers",
|
|
1072
|
+
{
|
|
1073
|
+
description: `Available API servers/environments for ${spec.title}`,
|
|
1074
|
+
mimeType: "application/json"
|
|
1075
|
+
},
|
|
1076
|
+
async () => ({
|
|
1077
|
+
contents: [
|
|
1078
|
+
{
|
|
1079
|
+
uri: "openapi://servers",
|
|
1080
|
+
mimeType: "application/json",
|
|
1081
|
+
text: JSON.stringify(spec.servers, null, 2)
|
|
1082
|
+
}
|
|
1083
|
+
]
|
|
1084
|
+
})
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// src/mapper/prompts.ts
|
|
1090
|
+
import { z as z3 } from "zod";
|
|
1091
|
+
function registerPrompts(server, spec, httpConfig) {
|
|
1092
|
+
server.prompt(
|
|
1093
|
+
"describe-api",
|
|
1094
|
+
"Get an overview of this API including endpoints, authentication, and capabilities",
|
|
1095
|
+
{},
|
|
1096
|
+
async () => {
|
|
1097
|
+
const endpoints = spec.operations.map((op) => {
|
|
1098
|
+
const prefix = op.deprecated ? "[DEPRECATED] " : "";
|
|
1099
|
+
return ` ${prefix}${op.method.padEnd(7)} ${op.path} - ${op.summary ?? op.operationId}`;
|
|
1100
|
+
});
|
|
1101
|
+
const authSchemes = Object.entries(spec.securitySchemes).map(([name, scheme]) => ` ${name}: ${scheme.type}${"scheme" in scheme ? ` (${scheme.scheme})` : ""}`);
|
|
1102
|
+
const text = [
|
|
1103
|
+
`# ${spec.title} v${spec.version}`,
|
|
1104
|
+
"",
|
|
1105
|
+
spec.description ?? "",
|
|
1106
|
+
"",
|
|
1107
|
+
formatServersSection(spec, httpConfig),
|
|
1108
|
+
"",
|
|
1109
|
+
formatExternalDocsSection(spec),
|
|
1110
|
+
"",
|
|
1111
|
+
`## Endpoints (${spec.operations.length})`,
|
|
1112
|
+
...endpoints,
|
|
1113
|
+
"",
|
|
1114
|
+
`## Authentication`,
|
|
1115
|
+
authSchemes.length > 0 ? authSchemes.join("\n") : " None required",
|
|
1116
|
+
"",
|
|
1117
|
+
`## Schemas`,
|
|
1118
|
+
Object.keys(spec.schemas).map((name) => ` - ${name}`).join("\n") || " None defined",
|
|
1119
|
+
"",
|
|
1120
|
+
formatTagsSection(spec)
|
|
1121
|
+
].filter((line) => line !== void 0 && line !== "").join("\n");
|
|
1122
|
+
return {
|
|
1123
|
+
messages: [
|
|
1124
|
+
{
|
|
1125
|
+
role: "user",
|
|
1126
|
+
content: { type: "text", text }
|
|
1127
|
+
}
|
|
1128
|
+
]
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
);
|
|
1132
|
+
server.prompt(
|
|
1133
|
+
"explore-endpoint",
|
|
1134
|
+
"Get detailed information about a specific API endpoint",
|
|
1135
|
+
{ operationId: z3.string().describe("The operationId of the endpoint to explore") },
|
|
1136
|
+
async ({ operationId }) => {
|
|
1137
|
+
const operation = spec.operations.find((op) => op.operationId === operationId);
|
|
1138
|
+
if (!operation) {
|
|
1139
|
+
const available = spec.operations.map((op) => op.operationId).join(", ");
|
|
1140
|
+
return {
|
|
1141
|
+
messages: [
|
|
1142
|
+
{
|
|
1143
|
+
role: "user",
|
|
1144
|
+
content: {
|
|
1145
|
+
type: "text",
|
|
1146
|
+
text: `Operation "${operationId}" not found. Available operations: ${available}`
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
]
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
const text = buildEndpointPrompt(operation, spec.tags);
|
|
1153
|
+
return {
|
|
1154
|
+
messages: [
|
|
1155
|
+
{
|
|
1156
|
+
role: "user",
|
|
1157
|
+
content: { type: "text", text }
|
|
1158
|
+
}
|
|
1159
|
+
]
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
function formatServersSection(spec, httpConfig) {
|
|
1165
|
+
if (spec.servers.length === 0) return "## Servers\n (not specified)";
|
|
1166
|
+
const lines = ["## Servers"];
|
|
1167
|
+
for (let i = 0; i < spec.servers.length; i++) {
|
|
1168
|
+
const s = spec.servers[i];
|
|
1169
|
+
let resolved = null;
|
|
1170
|
+
try {
|
|
1171
|
+
resolved = resolveServerUrl(s);
|
|
1172
|
+
} catch {
|
|
1173
|
+
}
|
|
1174
|
+
const active = resolved === httpConfig.baseUrl ? " [active]" : "";
|
|
1175
|
+
const desc = s.description ? ` \u2014 ${s.description}` : "";
|
|
1176
|
+
lines.push(` [${i}]${active} ${s.url}${desc}`);
|
|
1177
|
+
if (s.variables) {
|
|
1178
|
+
for (const [name, v] of Object.entries(s.variables)) {
|
|
1179
|
+
const enumStr = v.enum ? `=[${v.enum.join(",")}]` : "";
|
|
1180
|
+
lines.push(` {${name}}: default="${v.default}"${enumStr}${v.description ? ` \u2014 ${v.description}` : ""}`);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
if (spec.servers.length > 1) {
|
|
1185
|
+
lines.push("", " Use the `set_environment` tool to switch servers.");
|
|
1186
|
+
}
|
|
1187
|
+
return lines.join("\n");
|
|
1188
|
+
}
|
|
1189
|
+
function formatTagsSection(spec) {
|
|
1190
|
+
if (spec.tags.length === 0) return "";
|
|
1191
|
+
const lines = ["## Tags"];
|
|
1192
|
+
for (const tag of spec.tags) {
|
|
1193
|
+
let line = ` - **${tag.name}**`;
|
|
1194
|
+
if (tag.description) line += `: ${tag.description}`;
|
|
1195
|
+
if (tag.externalDocs) line += ` (docs: ${tag.externalDocs.url})`;
|
|
1196
|
+
lines.push(line);
|
|
1197
|
+
}
|
|
1198
|
+
return lines.join("\n");
|
|
1199
|
+
}
|
|
1200
|
+
function formatExternalDocsSection(spec) {
|
|
1201
|
+
if (!spec.externalDocs) return "";
|
|
1202
|
+
const desc = spec.externalDocs.description ? `${spec.externalDocs.description}: ` : "";
|
|
1203
|
+
return `## External Documentation
|
|
1204
|
+
${desc}${spec.externalDocs.url}`;
|
|
1205
|
+
}
|
|
1206
|
+
function buildEndpointPrompt(operation, globalTags) {
|
|
1207
|
+
const deprecatedTag = operation.deprecated ? " [DEPRECATED]" : "";
|
|
1208
|
+
const params = operation.parameters.map((p) => {
|
|
1209
|
+
const parts = [
|
|
1210
|
+
` - ${p.name} (${p.in}, ${p.required ? "required" : "optional"})`
|
|
1211
|
+
];
|
|
1212
|
+
if (p.deprecated) parts[0] += " [DEPRECATED]";
|
|
1213
|
+
parts[0] += `: ${p.description ?? p.schema.type ?? "any"}`;
|
|
1214
|
+
if (p.schema.format) parts[0] += ` (format: ${p.schema.format})`;
|
|
1215
|
+
if (p.example !== void 0) parts[0] += ` \u2014 example: ${formatExample(p.example)}`;
|
|
1216
|
+
return parts[0];
|
|
1217
|
+
});
|
|
1218
|
+
const responses = formatResponses(operation.responses);
|
|
1219
|
+
const links = formatLinks(operation.responses);
|
|
1220
|
+
const sections = [
|
|
1221
|
+
`# ${operation.method} ${operation.path}${deprecatedTag}`,
|
|
1222
|
+
`operationId: ${operation.operationId}`,
|
|
1223
|
+
"",
|
|
1224
|
+
operation.summary ? `**Summary**: ${operation.summary}` : "",
|
|
1225
|
+
operation.description ? `**Description**: ${operation.description}` : "",
|
|
1226
|
+
"",
|
|
1227
|
+
`## Parameters`,
|
|
1228
|
+
params.length > 0 ? params.join("\n") : " None",
|
|
1229
|
+
"",
|
|
1230
|
+
formatRequestBody(operation),
|
|
1231
|
+
"",
|
|
1232
|
+
`## Responses`,
|
|
1233
|
+
...responses,
|
|
1234
|
+
""
|
|
1235
|
+
];
|
|
1236
|
+
if (links.length > 0) {
|
|
1237
|
+
sections.push(`## Links`, ...links, "");
|
|
1238
|
+
}
|
|
1239
|
+
sections.push(
|
|
1240
|
+
`## Security`,
|
|
1241
|
+
operation.security.length > 0 ? operation.security.map((s) => ` ${Object.keys(s).join(", ")}`).join("\n") : " None",
|
|
1242
|
+
""
|
|
1243
|
+
);
|
|
1244
|
+
if (operation.tags.length > 0) {
|
|
1245
|
+
const tagLines = ["## Tags"];
|
|
1246
|
+
for (const tagName of operation.tags) {
|
|
1247
|
+
const globalTag = globalTags.find((t) => t.name === tagName);
|
|
1248
|
+
let line = ` - **${tagName}**`;
|
|
1249
|
+
if (globalTag?.description) line += `: ${globalTag.description}`;
|
|
1250
|
+
if (globalTag?.externalDocs) line += ` (docs: ${globalTag.externalDocs.url})`;
|
|
1251
|
+
tagLines.push(line);
|
|
1252
|
+
}
|
|
1253
|
+
sections.push(tagLines.join("\n"));
|
|
1254
|
+
}
|
|
1255
|
+
if (operation.externalDocs) {
|
|
1256
|
+
const desc = operation.externalDocs.description ? `${operation.externalDocs.description}: ` : "";
|
|
1257
|
+
sections.push("", `## External Documentation`, ` ${desc}${operation.externalDocs.url}`);
|
|
1258
|
+
}
|
|
1259
|
+
return sections.filter((line) => line !== void 0).join("\n");
|
|
1260
|
+
}
|
|
1261
|
+
function formatRequestBody(operation) {
|
|
1262
|
+
if (!operation.requestBody) return "";
|
|
1263
|
+
const lines = [
|
|
1264
|
+
`## Request Body ${operation.requestBody.required ? "(required)" : "(optional)"}`,
|
|
1265
|
+
operation.requestBody.description ?? "",
|
|
1266
|
+
"```json",
|
|
1267
|
+
safeStringify(
|
|
1268
|
+
Object.values(operation.requestBody.content)[0]?.schema ?? {},
|
|
1269
|
+
null,
|
|
1270
|
+
2
|
|
1271
|
+
),
|
|
1272
|
+
"```"
|
|
1273
|
+
];
|
|
1274
|
+
const jsonContent = operation.requestBody.content["application/json"] ?? Object.values(operation.requestBody.content)[0];
|
|
1275
|
+
if (jsonContent?.example !== void 0) {
|
|
1276
|
+
lines.push("", "**Example:**", "```json", safeStringify(jsonContent.example, null, 2), "```");
|
|
1277
|
+
}
|
|
1278
|
+
return lines.join("\n");
|
|
1279
|
+
}
|
|
1280
|
+
function formatResponses(responses) {
|
|
1281
|
+
const lines = [];
|
|
1282
|
+
for (const [code, resp] of Object.entries(responses)) {
|
|
1283
|
+
lines.push(` ${code}: ${resp.description || "(no description)"}`);
|
|
1284
|
+
if (resp.schema) {
|
|
1285
|
+
lines.push(" ```json", ` ${safeStringify(resp.schema, null, 2).split("\n").join("\n ")}`, " ```");
|
|
1286
|
+
}
|
|
1287
|
+
if (resp.example !== void 0) {
|
|
1288
|
+
lines.push(` Example:`);
|
|
1289
|
+
lines.push(" ```json", ` ${safeStringify(resp.example, null, 2).split("\n").join("\n ")}`, " ```");
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return lines;
|
|
1293
|
+
}
|
|
1294
|
+
function formatLinks(responses) {
|
|
1295
|
+
const lines = [];
|
|
1296
|
+
for (const [code, resp] of Object.entries(responses)) {
|
|
1297
|
+
if (!resp.links) continue;
|
|
1298
|
+
for (const [, link] of Object.entries(resp.links)) {
|
|
1299
|
+
const target = link.operationId ?? link.operationRef ?? "(unknown)";
|
|
1300
|
+
const params = link.parameters ? Object.entries(link.parameters).map(([k, v]) => `${k}=${v}`).join(", ") : "";
|
|
1301
|
+
const desc = link.description ? ` \u2014 ${link.description}` : "";
|
|
1302
|
+
lines.push(` After ${code} response, call \`${target}\`${params ? ` with {${params}}` : ""}${desc}`);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return lines;
|
|
1306
|
+
}
|
|
1307
|
+
function formatExample(value) {
|
|
1308
|
+
if (typeof value === "string") return value;
|
|
1309
|
+
return safeStringify(value);
|
|
1310
|
+
}
|
|
1311
|
+
function safeStringify(value, _replacer, indent) {
|
|
1312
|
+
try {
|
|
1313
|
+
return JSON.stringify(value, null, indent);
|
|
1314
|
+
} catch {
|
|
1315
|
+
return "(circular or non-serializable)";
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// src/server.ts
|
|
1320
|
+
async function createOpenApiMcp(options) {
|
|
1321
|
+
const doc = await loadSpec(options.source);
|
|
1322
|
+
const spec = await resolveSpec(doc);
|
|
1323
|
+
const serverName = options.name ?? spec.title ?? "dynamic-openapi-mcp";
|
|
1324
|
+
const serverVersion = options.version ?? spec.version ?? "1.0.0";
|
|
1325
|
+
const server = new McpServer({
|
|
1326
|
+
name: serverName,
|
|
1327
|
+
version: serverVersion
|
|
1328
|
+
});
|
|
1329
|
+
const auth = resolveAuth(options.auth, spec.securitySchemes);
|
|
1330
|
+
const baseUrl = resolveBaseUrl(spec, options.baseUrl, options.serverIndex);
|
|
1331
|
+
const httpConfig = {
|
|
1332
|
+
baseUrl,
|
|
1333
|
+
auth,
|
|
1334
|
+
defaultHeaders: options.headers,
|
|
1335
|
+
fetchOptions: options.fetchOptions
|
|
1336
|
+
};
|
|
1337
|
+
registerTools(server, spec, httpConfig);
|
|
1338
|
+
registerResources(server, spec);
|
|
1339
|
+
registerPrompts(server, spec, httpConfig);
|
|
1340
|
+
return {
|
|
1341
|
+
server,
|
|
1342
|
+
spec,
|
|
1343
|
+
async serve() {
|
|
1344
|
+
const transport = new StdioServerTransport();
|
|
1345
|
+
await server.connect(transport);
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// src/cli.ts
|
|
1351
|
+
function parseArgs(argv) {
|
|
1352
|
+
const args = {};
|
|
1353
|
+
for (let i = 2; i < argv.length; i++) {
|
|
1354
|
+
const arg = argv[i];
|
|
1355
|
+
const next = argv[i + 1];
|
|
1356
|
+
if ((arg === "-s" || arg === "--source") && next) {
|
|
1357
|
+
args.source = next;
|
|
1358
|
+
i++;
|
|
1359
|
+
} else if ((arg === "-b" || arg === "--base-url") && next) {
|
|
1360
|
+
args.baseUrl = next;
|
|
1361
|
+
i++;
|
|
1362
|
+
} else if (arg === "--server-index" && next) {
|
|
1363
|
+
const parsed = parseInt(next, 10);
|
|
1364
|
+
if (isNaN(parsed) || parsed < 0) {
|
|
1365
|
+
console.error(`Error: --server-index must be a non-negative integer, got "${next}"`);
|
|
1366
|
+
process.exit(1);
|
|
1367
|
+
}
|
|
1368
|
+
args.serverIndex = parsed;
|
|
1369
|
+
i++;
|
|
1370
|
+
} else if (arg === "-h" || arg === "--help") {
|
|
1371
|
+
printHelp();
|
|
1372
|
+
process.exit(0);
|
|
1373
|
+
} else if (!arg.startsWith("-") && !args.source) {
|
|
1374
|
+
args.source = arg;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
return args;
|
|
1378
|
+
}
|
|
1379
|
+
function printHelp() {
|
|
1380
|
+
console.log(`
|
|
1381
|
+
dynamic-openapi-mcp - Transform OpenAPI specs into MCP servers
|
|
1382
|
+
|
|
1383
|
+
Usage:
|
|
1384
|
+
dynamic-openapi-mcp [options] [source]
|
|
1385
|
+
|
|
1386
|
+
Options:
|
|
1387
|
+
-s, --source <url|file> OpenAPI spec URL or file path
|
|
1388
|
+
-b, --base-url <url> Override the base URL from the spec
|
|
1389
|
+
--server-index <n> Use the Nth server from the spec (0-based, default: 0)
|
|
1390
|
+
-h, --help Show this help message
|
|
1391
|
+
|
|
1392
|
+
Environment Variables:
|
|
1393
|
+
OPENAPI_SOURCE OpenAPI spec URL or file path
|
|
1394
|
+
OPENAPI_BASE_URL Override base URL
|
|
1395
|
+
OPENAPI_SERVER_INDEX Server index (0-based)
|
|
1396
|
+
OPENAPI_AUTH_TOKEN Bearer token for API authentication
|
|
1397
|
+
OPENAPI_API_KEY API key for authentication
|
|
1398
|
+
|
|
1399
|
+
Examples:
|
|
1400
|
+
dynamic-openapi-mcp -s https://petstore3.swagger.io/api/v3/openapi.json
|
|
1401
|
+
dynamic-openapi-mcp ./spec.yaml
|
|
1402
|
+
dynamic-openapi-mcp --server-index 1 ./spec.yaml
|
|
1403
|
+
OPENAPI_SOURCE=./spec.yaml OPENAPI_AUTH_TOKEN=sk-123 dynamic-openapi-mcp
|
|
1404
|
+
`);
|
|
1405
|
+
}
|
|
1406
|
+
async function main() {
|
|
1407
|
+
const args = parseArgs(process.argv);
|
|
1408
|
+
const source = args.source ?? process.env["OPENAPI_SOURCE"] ?? process.env["OPENAPI_SOURCE_FILE"];
|
|
1409
|
+
if (!source) {
|
|
1410
|
+
console.error("Error: No OpenAPI source specified.");
|
|
1411
|
+
console.error("Use -s <url|file> or set OPENAPI_SOURCE environment variable.");
|
|
1412
|
+
console.error("Run dynamic-openapi-mcp --help for usage information.");
|
|
1413
|
+
process.exit(1);
|
|
1414
|
+
}
|
|
1415
|
+
const baseUrl = args.baseUrl ?? process.env["OPENAPI_BASE_URL"];
|
|
1416
|
+
let serverIndex = args.serverIndex;
|
|
1417
|
+
if (serverIndex === void 0 && process.env["OPENAPI_SERVER_INDEX"]) {
|
|
1418
|
+
const parsed = parseInt(process.env["OPENAPI_SERVER_INDEX"], 10);
|
|
1419
|
+
if (!isNaN(parsed) && parsed >= 0) {
|
|
1420
|
+
serverIndex = parsed;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
try {
|
|
1424
|
+
const mcp = await createOpenApiMcp({
|
|
1425
|
+
source,
|
|
1426
|
+
baseUrl,
|
|
1427
|
+
serverIndex
|
|
1428
|
+
});
|
|
1429
|
+
const opCount = mcp.spec.operations.length;
|
|
1430
|
+
const schemaCount = Object.keys(mcp.spec.schemas).length;
|
|
1431
|
+
process.stderr.write(
|
|
1432
|
+
`dynamic-openapi-mcp: loaded "${mcp.spec.title}" v${mcp.spec.version} \u2014 ${opCount} tools, ${schemaCount} schemas
|
|
1433
|
+
`
|
|
1434
|
+
);
|
|
1435
|
+
await mcp.serve();
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
if (error instanceof Error) {
|
|
1438
|
+
process.stderr.write(`dynamic-openapi-mcp: ${error.message}
|
|
1439
|
+
`);
|
|
1440
|
+
if (error.stack) {
|
|
1441
|
+
process.stderr.write(`${error.stack}
|
|
1442
|
+
`);
|
|
1443
|
+
}
|
|
1444
|
+
} else {
|
|
1445
|
+
process.stderr.write(`dynamic-openapi-mcp: ${String(error)}
|
|
1446
|
+
`);
|
|
1447
|
+
}
|
|
1448
|
+
process.exit(1);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
main();
|
|
1452
|
+
//# sourceMappingURL=cli.js.map
|