@frontmcp/adapters 1.0.4 → 1.1.0-beta.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/esm/index.mjs +403 -403
- package/esm/openapi/index.mjs +403 -403
- package/esm/package.json +6 -4
- package/index.js +407 -407
- package/openapi/index.js +407 -407
- package/openapi/openapi.frontmcp-schema.d.ts +21 -21
- package/openapi/openapi.frontmcp-schema.d.ts.map +1 -1
- package/openapi/openapi.tool.d.ts +2 -2
- package/openapi/openapi.tool.d.ts.map +1 -1
- package/package.json +6 -4
package/esm/index.mjs
CHANGED
|
@@ -14,338 +14,12 @@ import { Adapter, DynamicAdapter } from "@frontmcp/sdk";
|
|
|
14
14
|
import { OpenAPIToolGenerator } from "mcp-from-openapi";
|
|
15
15
|
|
|
16
16
|
// libs/adapters/src/openapi/openapi.tool.ts
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
// libs/adapters/src/openapi/openapi.utils.ts
|
|
22
|
-
import { validateBaseUrl } from "@frontmcp/utils";
|
|
23
|
-
import { PublicMcpError } from "@frontmcp/sdk";
|
|
24
|
-
function coerceToString(value, paramName, location) {
|
|
25
|
-
if (value === null || value === void 0) {
|
|
26
|
-
return "";
|
|
27
|
-
}
|
|
28
|
-
if (typeof value === "object") {
|
|
29
|
-
if (Array.isArray(value)) {
|
|
30
|
-
if (location === "query") {
|
|
31
|
-
return value.map(String).join(",");
|
|
32
|
-
}
|
|
33
|
-
throw new Error(`${location} parameter '${paramName}' cannot be an array. Received: ${JSON.stringify(value)}`);
|
|
34
|
-
}
|
|
35
|
-
throw new Error(`${location} parameter '${paramName}' cannot be an object. Received: ${JSON.stringify(value)}`);
|
|
36
|
-
}
|
|
37
|
-
return String(value);
|
|
38
|
-
}
|
|
39
|
-
function appendCookie(headers, name, value) {
|
|
40
|
-
if (!/^[\w!#$%&'*+\-.^`|~]+$/.test(name)) {
|
|
41
|
-
throw new Error(`Invalid cookie name: '${name}'. Cookie names must be valid tokens.`);
|
|
42
|
-
}
|
|
43
|
-
const existing = headers.get("Cookie") || "";
|
|
44
|
-
const cookiePair = `${name}=${encodeURIComponent(coerceToString(value, name, "cookie"))}`;
|
|
45
|
-
const combined = existing ? `${existing}; ${cookiePair}` : cookiePair;
|
|
46
|
-
headers.set("Cookie", combined);
|
|
47
|
-
}
|
|
48
|
-
function buildRequest(tool2, input, security, baseUrl) {
|
|
49
|
-
const rawBaseUrl = tool2.metadata.servers?.[0]?.url || baseUrl;
|
|
50
|
-
validateBaseUrl(rawBaseUrl);
|
|
51
|
-
const apiBaseUrl = rawBaseUrl.replace(/\/+$/, "");
|
|
52
|
-
let path = tool2.metadata.path;
|
|
53
|
-
const queryParams = new URLSearchParams();
|
|
54
|
-
const headers = new Headers({
|
|
55
|
-
accept: "application/json",
|
|
56
|
-
...security.headers
|
|
57
|
-
});
|
|
58
|
-
let body;
|
|
59
|
-
for (const mapper of tool2.mapper) {
|
|
60
|
-
if (mapper.security) continue;
|
|
61
|
-
const value = input[mapper.inputKey];
|
|
62
|
-
if (value === void 0 || value === null) {
|
|
63
|
-
if (mapper.required) {
|
|
64
|
-
throw new Error(
|
|
65
|
-
`Required ${mapper.type} parameter '${mapper.key}' (input: '${mapper.inputKey}') is missing for operation '${tool2.name}'`
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
switch (mapper.type) {
|
|
71
|
-
case "path":
|
|
72
|
-
path = path.replaceAll(`{${mapper.key}}`, encodeURIComponent(coerceToString(value, mapper.key, "path")));
|
|
73
|
-
break;
|
|
74
|
-
case "query":
|
|
75
|
-
queryParams.set(mapper.key, coerceToString(value, mapper.key, "query"));
|
|
76
|
-
break;
|
|
77
|
-
case "header": {
|
|
78
|
-
const headerValue = coerceToString(value, mapper.key, "header");
|
|
79
|
-
if (/[\r\n\x00\f\v]/.test(headerValue)) {
|
|
80
|
-
throw new PublicMcpError(
|
|
81
|
-
`Invalid header value for '${mapper.key}': contains control characters (possible header injection attack)`,
|
|
82
|
-
"INVALID_HEADER_VALUE",
|
|
83
|
-
400
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
headers.set(mapper.key, headerValue);
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
case "cookie":
|
|
90
|
-
appendCookie(headers, mapper.key, value);
|
|
91
|
-
break;
|
|
92
|
-
case "body":
|
|
93
|
-
if (!body) body = {};
|
|
94
|
-
body[mapper.key] = value;
|
|
95
|
-
break;
|
|
96
|
-
default:
|
|
97
|
-
throw new Error(
|
|
98
|
-
`Unknown mapper type '${mapper.type}' for parameter '${mapper.key}' in operation '${tool2.name}'`
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
Object.entries(security.query).forEach(([key, value]) => {
|
|
103
|
-
if (queryParams.has(key)) {
|
|
104
|
-
throw new Error(
|
|
105
|
-
`Query parameter collision: '${key}' is provided both as user input and security parameter. This could indicate a security misconfiguration in operation '${tool2.name}'.`
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
queryParams.set(key, coerceToString(value, key, "security query"));
|
|
109
|
-
});
|
|
110
|
-
if (security.cookies && Object.keys(security.cookies).length > 0) {
|
|
111
|
-
Object.entries(security.cookies).forEach(([key, value]) => {
|
|
112
|
-
appendCookie(headers, key, value);
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
if (path.includes("{")) {
|
|
116
|
-
throw new Error(`Failed to resolve all path parameters in ${path} for operation ${tool2.name}`);
|
|
117
|
-
}
|
|
118
|
-
const queryString = queryParams.toString();
|
|
119
|
-
const url = `${apiBaseUrl}${path}${queryString ? `?${queryString}` : ""}`;
|
|
120
|
-
return { url, headers, body };
|
|
121
|
-
}
|
|
122
|
-
function applyAdditionalHeaders(headers, additionalHeaders) {
|
|
123
|
-
if (!additionalHeaders) return;
|
|
124
|
-
Object.entries(additionalHeaders).forEach(([key, value]) => {
|
|
125
|
-
headers.set(key, value);
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
var DEFAULT_MAX_RESPONSE_SIZE = 10 * 1024 * 1024;
|
|
129
|
-
async function parseResponse(response, options) {
|
|
130
|
-
const maxSize = options?.maxResponseSize ?? DEFAULT_MAX_RESPONSE_SIZE;
|
|
131
|
-
const contentLength = response.headers.get("content-length");
|
|
132
|
-
if (contentLength) {
|
|
133
|
-
const length = parseInt(contentLength, 10);
|
|
134
|
-
if (!isNaN(length) && isFinite(length) && length > maxSize) {
|
|
135
|
-
throw new Error(`Response size (${length} bytes) exceeds maximum allowed (${maxSize} bytes)`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
const text = await response.text();
|
|
139
|
-
const byteSize = new TextEncoder().encode(text).length;
|
|
140
|
-
if (byteSize > maxSize) {
|
|
141
|
-
throw new Error(`Response size (${byteSize} bytes) exceeds maximum allowed (${maxSize} bytes)`);
|
|
142
|
-
}
|
|
143
|
-
const contentType = response.headers.get("content-type");
|
|
144
|
-
let data;
|
|
145
|
-
if (contentType?.toLowerCase().includes("application/json")) {
|
|
146
|
-
try {
|
|
147
|
-
data = JSON.parse(text);
|
|
148
|
-
} catch {
|
|
149
|
-
data = text;
|
|
150
|
-
}
|
|
151
|
-
} else {
|
|
152
|
-
data = text;
|
|
153
|
-
}
|
|
154
|
-
if (!response.ok) {
|
|
155
|
-
return {
|
|
156
|
-
status: response.status,
|
|
157
|
-
ok: false,
|
|
158
|
-
data,
|
|
159
|
-
error: typeof data === "object" && data !== null && "message" in data ? String(data.message) : typeof data === "string" ? data : `HTTP ${response.status} error`
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
return {
|
|
163
|
-
status: response.status,
|
|
164
|
-
ok: true,
|
|
165
|
-
data
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// libs/adapters/src/openapi/openapi.security.ts
|
|
170
|
-
import { SecurityResolver, createSecurityContext } from "mcp-from-openapi";
|
|
171
|
-
async function createSecurityContextFromAuth(tool2, ctx, options) {
|
|
172
|
-
if (options.securityResolver) {
|
|
173
|
-
return await options.securityResolver(tool2, ctx);
|
|
174
|
-
}
|
|
175
|
-
if (options.authProviderMapper) {
|
|
176
|
-
const context = createSecurityContext({});
|
|
177
|
-
const securitySchemes = /* @__PURE__ */ new Set();
|
|
178
|
-
for (const mapper of tool2.mapper) {
|
|
179
|
-
if (mapper.security?.scheme) {
|
|
180
|
-
securitySchemes.add(mapper.security.scheme);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
for (const scheme of securitySchemes) {
|
|
184
|
-
const authExtractor = options.authProviderMapper[scheme];
|
|
185
|
-
if (authExtractor) {
|
|
186
|
-
try {
|
|
187
|
-
const token = authExtractor(ctx);
|
|
188
|
-
if (token !== void 0 && token !== null && typeof token !== "string") {
|
|
189
|
-
throw new Error(
|
|
190
|
-
`authProviderMapper['${scheme}'] must return a string or undefined, but returned: ${typeof token}`
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
if (token === "") {
|
|
194
|
-
throw new Error(
|
|
195
|
-
`authProviderMapper['${scheme}'] returned empty string. Return undefined/null if no token is available, or provide a valid token.`
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
if (token) {
|
|
199
|
-
const schemeMapper = tool2.mapper.find((m) => m.security?.scheme === scheme);
|
|
200
|
-
const schemeType = schemeMapper?.security?.type?.toLowerCase();
|
|
201
|
-
const httpScheme = schemeMapper?.security?.httpScheme?.toLowerCase();
|
|
202
|
-
if (schemeType === "apikey") {
|
|
203
|
-
if (!context.apiKey) {
|
|
204
|
-
context.apiKey = token;
|
|
205
|
-
}
|
|
206
|
-
} else if (schemeType === "http" && httpScheme === "basic") {
|
|
207
|
-
if (!context.basic) {
|
|
208
|
-
context.basic = token;
|
|
209
|
-
}
|
|
210
|
-
} else if (schemeType === "oauth2") {
|
|
211
|
-
if (!context.oauth2Token) {
|
|
212
|
-
context.oauth2Token = token;
|
|
213
|
-
}
|
|
214
|
-
} else {
|
|
215
|
-
if (!context.jwt) {
|
|
216
|
-
context.jwt = token;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
} catch (err) {
|
|
221
|
-
if (err instanceof Error && err.message.includes("authProviderMapper")) {
|
|
222
|
-
throw err;
|
|
223
|
-
}
|
|
224
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
225
|
-
throw new Error(`authProviderMapper['${scheme}'] threw an error: ${errorMessage}`);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
const hasAnyAuth = context.jwt || context.apiKey || context.basic || context.oauth2Token;
|
|
230
|
-
const authToken = ctx.authInfo?.token;
|
|
231
|
-
if (!hasAnyAuth && authToken) {
|
|
232
|
-
if (typeof authToken !== "string") {
|
|
233
|
-
throw new Error(`authInfo.token must be a string, but got: ${typeof authToken}`);
|
|
234
|
-
}
|
|
235
|
-
context.jwt = authToken;
|
|
236
|
-
}
|
|
237
|
-
return context;
|
|
238
|
-
}
|
|
239
|
-
if (options.staticAuth) {
|
|
240
|
-
return createSecurityContext(options.staticAuth);
|
|
241
|
-
}
|
|
242
|
-
return createSecurityContext({
|
|
243
|
-
jwt: ctx.authInfo?.token
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
function extractSecuritySchemes(tools) {
|
|
247
|
-
const schemes = /* @__PURE__ */ new Set();
|
|
248
|
-
for (const tool2 of tools) {
|
|
249
|
-
for (const mapper of tool2.mapper) {
|
|
250
|
-
if (mapper.security?.scheme) {
|
|
251
|
-
schemes.add(mapper.security.scheme);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
return schemes;
|
|
256
|
-
}
|
|
257
|
-
function validateSecurityConfiguration(tools, options) {
|
|
258
|
-
const result = {
|
|
259
|
-
valid: true,
|
|
260
|
-
missingMappings: [],
|
|
261
|
-
warnings: [],
|
|
262
|
-
securityRiskScore: "low"
|
|
263
|
-
};
|
|
264
|
-
const securitySchemes = extractSecuritySchemes(tools);
|
|
265
|
-
const includeSecurityInInput = options.generateOptions?.includeSecurityInInput ?? false;
|
|
266
|
-
if (includeSecurityInInput) {
|
|
267
|
-
result.securityRiskScore = "high";
|
|
268
|
-
result.warnings.push(
|
|
269
|
-
"SECURITY WARNING: includeSecurityInInput is enabled. Users will provide authentication directly in tool inputs. This increases security risk as credentials may be logged or exposed."
|
|
270
|
-
);
|
|
271
|
-
return result;
|
|
272
|
-
}
|
|
273
|
-
if (options.securityResolver) {
|
|
274
|
-
result.securityRiskScore = "low";
|
|
275
|
-
result.warnings.push(
|
|
276
|
-
"INFO: Using custom securityResolver. Ensure your resolver properly validates and secures credentials from context."
|
|
277
|
-
);
|
|
278
|
-
return result;
|
|
279
|
-
}
|
|
280
|
-
if (options.staticAuth && Object.keys(options.staticAuth).length > 0) {
|
|
281
|
-
result.securityRiskScore = "medium";
|
|
282
|
-
result.warnings.push(
|
|
283
|
-
"SECURITY INFO: Using staticAuth with hardcoded credentials. Ensure credentials are stored securely (environment variables, secrets manager)."
|
|
284
|
-
);
|
|
285
|
-
return result;
|
|
286
|
-
}
|
|
287
|
-
const schemesInInput = new Set(options.securitySchemesInInput || []);
|
|
288
|
-
if (options.authProviderMapper || schemesInInput.size > 0) {
|
|
289
|
-
result.securityRiskScore = schemesInInput.size > 0 ? "medium" : "low";
|
|
290
|
-
if (schemesInInput.size > 0) {
|
|
291
|
-
result.warnings.push(
|
|
292
|
-
`INFO: Per-scheme security control enabled. Schemes in input: ${Array.from(schemesInInput).join(", ")}`
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
for (const scheme of securitySchemes) {
|
|
296
|
-
if (schemesInInput.has(scheme)) {
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
if (!options.authProviderMapper?.[scheme]) {
|
|
300
|
-
result.valid = false;
|
|
301
|
-
result.missingMappings.push(scheme);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
if (!result.valid) {
|
|
305
|
-
result.warnings.push(
|
|
306
|
-
`ERROR: Missing auth provider mappings for security schemes: ${result.missingMappings.join(", ")}`
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
return result;
|
|
310
|
-
}
|
|
311
|
-
if (securitySchemes.size > 0) {
|
|
312
|
-
result.securityRiskScore = "medium";
|
|
313
|
-
result.warnings.push(
|
|
314
|
-
`INFO: No auth configuration provided. Using default ctx.authInfo.token for all security schemes: ${Array.from(
|
|
315
|
-
securitySchemes
|
|
316
|
-
).join(", ")}`
|
|
317
|
-
);
|
|
318
|
-
result.warnings.push(
|
|
319
|
-
"RECOMMENDATION: For multiple auth providers, use authProviderMapper or securityResolver to map each security scheme to the correct auth provider."
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
return result;
|
|
323
|
-
}
|
|
324
|
-
async function resolveToolSecurity(tool2, ctx, options) {
|
|
325
|
-
const securityResolver = new SecurityResolver();
|
|
326
|
-
const securityContext = await createSecurityContextFromAuth(tool2, ctx, options);
|
|
327
|
-
const hasAuth = securityContext.jwt || securityContext.apiKey || securityContext.basic || securityContext.oauth2Token || securityContext.apiKeys && Object.keys(securityContext.apiKeys).length > 0 || securityContext.customHeaders && Object.keys(securityContext.customHeaders).length > 0;
|
|
328
|
-
const requiresSecurity = tool2.mapper.some((m) => m.security && m.required === true);
|
|
329
|
-
if (requiresSecurity && !hasAuth) {
|
|
330
|
-
const requiredSchemes = tool2.mapper.filter((m) => m.security && m.required === true).map((m) => m.security?.scheme ?? "unknown");
|
|
331
|
-
const uniqueSchemes = [...new Set(requiredSchemes)];
|
|
332
|
-
const schemesStr = uniqueSchemes.join(", ") || "unknown";
|
|
333
|
-
const firstScheme = uniqueSchemes[0] || "BearerAuth";
|
|
334
|
-
throw new Error(
|
|
335
|
-
`Authentication required for tool '${tool2.name}' but no auth configuration found.
|
|
336
|
-
Required security schemes: ${schemesStr}
|
|
337
|
-
Solutions:
|
|
338
|
-
1. Add authProviderMapper: { '${firstScheme}': (ctx) => ctx.authInfo.user?.token }
|
|
339
|
-
2. Add securityResolver: (tool, ctx) => ({ jwt: ctx.authInfo.token })
|
|
340
|
-
3. Add staticAuth: { jwt: process.env.API_TOKEN }
|
|
341
|
-
4. Set generateOptions.includeSecurityInInput: true (not recommended for production)`
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
return await securityResolver.resolve(tool2.mapper, securityContext);
|
|
345
|
-
}
|
|
17
|
+
import { convertJsonSchemaToZod } from "zod-from-json-schema";
|
|
18
|
+
import { z as z2 } from "@frontmcp/lazy-zod";
|
|
19
|
+
import { tool } from "@frontmcp/sdk";
|
|
346
20
|
|
|
347
21
|
// libs/adapters/src/openapi/openapi.frontmcp-schema.ts
|
|
348
|
-
import { z } from "zod";
|
|
22
|
+
import { z } from "@frontmcp/lazy-zod";
|
|
349
23
|
var FRONTMCP_SCHEMA_VERSION = "1.0";
|
|
350
24
|
var SUPPORTED_VERSIONS = ["1.0"];
|
|
351
25
|
var FrontMcpAnnotationsSchema = z.object({
|
|
@@ -491,98 +165,424 @@ function extractValidFields(data, version, warnings) {
|
|
|
491
165
|
warnings.push(`Invalid 'examples': some examples have invalid format`);
|
|
492
166
|
result.examples = extractValidExamples(data["examples"], warnings);
|
|
493
167
|
}
|
|
494
|
-
}
|
|
495
|
-
return result;
|
|
496
|
-
}
|
|
497
|
-
function extractValidAnnotations(data, warnings) {
|
|
498
|
-
if (typeof data !== "object" || data === null) return void 0;
|
|
499
|
-
const obj = data;
|
|
500
|
-
const result = {};
|
|
501
|
-
for (const field of KNOWN_ANNOTATION_FIELDS) {
|
|
502
|
-
if (obj[field] !== void 0) {
|
|
503
|
-
if (field === "title" && typeof obj[field] === "string") {
|
|
504
|
-
result.title = obj[field];
|
|
505
|
-
} else if (field !== "title" && typeof obj[field] === "boolean") {
|
|
506
|
-
result[field] = obj[field];
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
function extractValidAnnotations(data, warnings) {
|
|
172
|
+
if (typeof data !== "object" || data === null) return void 0;
|
|
173
|
+
const obj = data;
|
|
174
|
+
const result = {};
|
|
175
|
+
for (const field of KNOWN_ANNOTATION_FIELDS) {
|
|
176
|
+
if (obj[field] !== void 0) {
|
|
177
|
+
if (field === "title" && typeof obj[field] === "string") {
|
|
178
|
+
result.title = obj[field];
|
|
179
|
+
} else if (field !== "title" && typeof obj[field] === "boolean") {
|
|
180
|
+
result[field] = obj[field];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
for (const key of Object.keys(obj)) {
|
|
185
|
+
if (!KNOWN_ANNOTATION_FIELDS.has(key)) {
|
|
186
|
+
warnings.push(`Unknown field 'annotations.${key}' (will be ignored)`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
190
|
+
}
|
|
191
|
+
function extractValidCache(data, warnings) {
|
|
192
|
+
if (typeof data !== "object" || data === null) return void 0;
|
|
193
|
+
const obj = data;
|
|
194
|
+
const result = {};
|
|
195
|
+
if (typeof obj["ttl"] === "number" && Number.isInteger(obj["ttl"]) && obj["ttl"] > 0) {
|
|
196
|
+
result.ttl = obj["ttl"];
|
|
197
|
+
} else if (obj["ttl"] !== void 0) {
|
|
198
|
+
warnings.push(`Invalid 'cache.ttl': expected positive integer`);
|
|
199
|
+
}
|
|
200
|
+
if (typeof obj["slideWindow"] === "boolean") {
|
|
201
|
+
result.slideWindow = obj["slideWindow"];
|
|
202
|
+
} else if (obj["slideWindow"] !== void 0) {
|
|
203
|
+
warnings.push(`Invalid 'cache.slideWindow': expected boolean`);
|
|
204
|
+
}
|
|
205
|
+
for (const key of Object.keys(obj)) {
|
|
206
|
+
if (!KNOWN_CACHE_FIELDS.has(key)) {
|
|
207
|
+
warnings.push(`Unknown field 'cache.${key}' (will be ignored)`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
211
|
+
}
|
|
212
|
+
function extractValidCodeCall(data, warnings) {
|
|
213
|
+
if (typeof data !== "object" || data === null) return void 0;
|
|
214
|
+
const obj = data;
|
|
215
|
+
const result = {};
|
|
216
|
+
if (typeof obj["enabledInCodeCall"] === "boolean") {
|
|
217
|
+
result.enabledInCodeCall = obj["enabledInCodeCall"];
|
|
218
|
+
} else if (obj["enabledInCodeCall"] !== void 0) {
|
|
219
|
+
warnings.push(`Invalid 'codecall.enabledInCodeCall': expected boolean`);
|
|
220
|
+
}
|
|
221
|
+
if (typeof obj["visibleInListTools"] === "boolean") {
|
|
222
|
+
result.visibleInListTools = obj["visibleInListTools"];
|
|
223
|
+
} else if (obj["visibleInListTools"] !== void 0) {
|
|
224
|
+
warnings.push(`Invalid 'codecall.visibleInListTools': expected boolean`);
|
|
225
|
+
}
|
|
226
|
+
for (const key of Object.keys(obj)) {
|
|
227
|
+
if (!KNOWN_CODECALL_FIELDS.has(key)) {
|
|
228
|
+
warnings.push(`Unknown field 'codecall.${key}' (will be ignored)`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
232
|
+
}
|
|
233
|
+
function extractValidTags(data) {
|
|
234
|
+
if (!Array.isArray(data)) return void 0;
|
|
235
|
+
const validTags = data.filter((item) => typeof item === "string");
|
|
236
|
+
return validTags.length > 0 ? validTags : void 0;
|
|
237
|
+
}
|
|
238
|
+
function extractValidExamples(data, warnings) {
|
|
239
|
+
if (!Array.isArray(data)) return void 0;
|
|
240
|
+
const validExamples = [];
|
|
241
|
+
for (let i = 0; i < data.length; i++) {
|
|
242
|
+
const item = data[i];
|
|
243
|
+
const result = FrontMcpExampleSchema.safeParse(item);
|
|
244
|
+
if (result.success) {
|
|
245
|
+
validExamples.push(result.data);
|
|
246
|
+
} else {
|
|
247
|
+
warnings.push(
|
|
248
|
+
`Invalid example at index ${i}: ${formatZodIssues(result.error.issues, `examples[${i}]`).join(", ")}`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return validExamples.length > 0 ? validExamples : void 0;
|
|
253
|
+
}
|
|
254
|
+
function formatZodIssues(issues, prefix) {
|
|
255
|
+
return issues.map((issue) => {
|
|
256
|
+
const path = issue.path.length > 0 ? `${prefix}.${issue.path.join(".")}` : prefix;
|
|
257
|
+
return `Invalid '${path}': ${issue.message}`;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// libs/adapters/src/openapi/openapi.security.ts
|
|
262
|
+
import { SecurityResolver, createSecurityContext } from "mcp-from-openapi";
|
|
263
|
+
async function createSecurityContextFromAuth(tool2, ctx, options) {
|
|
264
|
+
if (options.securityResolver) {
|
|
265
|
+
return await options.securityResolver(tool2, ctx);
|
|
266
|
+
}
|
|
267
|
+
if (options.authProviderMapper) {
|
|
268
|
+
const context = createSecurityContext({});
|
|
269
|
+
const securitySchemes = /* @__PURE__ */ new Set();
|
|
270
|
+
for (const mapper of tool2.mapper) {
|
|
271
|
+
if (mapper.security?.scheme) {
|
|
272
|
+
securitySchemes.add(mapper.security.scheme);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (const scheme of securitySchemes) {
|
|
276
|
+
const authExtractor = options.authProviderMapper[scheme];
|
|
277
|
+
if (authExtractor) {
|
|
278
|
+
try {
|
|
279
|
+
const token = authExtractor(ctx);
|
|
280
|
+
if (token !== void 0 && token !== null && typeof token !== "string") {
|
|
281
|
+
throw new Error(
|
|
282
|
+
`authProviderMapper['${scheme}'] must return a string or undefined, but returned: ${typeof token}`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
if (token === "") {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`authProviderMapper['${scheme}'] returned empty string. Return undefined/null if no token is available, or provide a valid token.`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (token) {
|
|
291
|
+
const schemeMapper = tool2.mapper.find((m) => m.security?.scheme === scheme);
|
|
292
|
+
const schemeType = schemeMapper?.security?.type?.toLowerCase();
|
|
293
|
+
const httpScheme = schemeMapper?.security?.httpScheme?.toLowerCase();
|
|
294
|
+
if (schemeType === "apikey") {
|
|
295
|
+
if (!context.apiKey) {
|
|
296
|
+
context.apiKey = token;
|
|
297
|
+
}
|
|
298
|
+
} else if (schemeType === "http" && httpScheme === "basic") {
|
|
299
|
+
if (!context.basic) {
|
|
300
|
+
context.basic = token;
|
|
301
|
+
}
|
|
302
|
+
} else if (schemeType === "oauth2") {
|
|
303
|
+
if (!context.oauth2Token) {
|
|
304
|
+
context.oauth2Token = token;
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
if (!context.jwt) {
|
|
308
|
+
context.jwt = token;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} catch (err) {
|
|
313
|
+
if (err instanceof Error && err.message.includes("authProviderMapper")) {
|
|
314
|
+
throw err;
|
|
315
|
+
}
|
|
316
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
317
|
+
throw new Error(`authProviderMapper['${scheme}'] threw an error: ${errorMessage}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const hasAnyAuth = context.jwt || context.apiKey || context.basic || context.oauth2Token;
|
|
322
|
+
const authToken = ctx.authInfo?.token;
|
|
323
|
+
if (!hasAnyAuth && authToken) {
|
|
324
|
+
if (typeof authToken !== "string") {
|
|
325
|
+
throw new Error(`authInfo.token must be a string, but got: ${typeof authToken}`);
|
|
326
|
+
}
|
|
327
|
+
context.jwt = authToken;
|
|
328
|
+
}
|
|
329
|
+
return context;
|
|
330
|
+
}
|
|
331
|
+
if (options.staticAuth) {
|
|
332
|
+
return createSecurityContext(options.staticAuth);
|
|
333
|
+
}
|
|
334
|
+
return createSecurityContext({
|
|
335
|
+
jwt: ctx.authInfo?.token
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
function extractSecuritySchemes(tools) {
|
|
339
|
+
const schemes = /* @__PURE__ */ new Set();
|
|
340
|
+
for (const tool2 of tools) {
|
|
341
|
+
for (const mapper of tool2.mapper) {
|
|
342
|
+
if (mapper.security?.scheme) {
|
|
343
|
+
schemes.add(mapper.security.scheme);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return schemes;
|
|
348
|
+
}
|
|
349
|
+
function validateSecurityConfiguration(tools, options) {
|
|
350
|
+
const result = {
|
|
351
|
+
valid: true,
|
|
352
|
+
missingMappings: [],
|
|
353
|
+
warnings: [],
|
|
354
|
+
securityRiskScore: "low"
|
|
355
|
+
};
|
|
356
|
+
const securitySchemes = extractSecuritySchemes(tools);
|
|
357
|
+
const includeSecurityInInput = options.generateOptions?.includeSecurityInInput ?? false;
|
|
358
|
+
if (includeSecurityInInput) {
|
|
359
|
+
result.securityRiskScore = "high";
|
|
360
|
+
result.warnings.push(
|
|
361
|
+
"SECURITY WARNING: includeSecurityInInput is enabled. Users will provide authentication directly in tool inputs. This increases security risk as credentials may be logged or exposed."
|
|
362
|
+
);
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
if (options.securityResolver) {
|
|
366
|
+
result.securityRiskScore = "low";
|
|
367
|
+
result.warnings.push(
|
|
368
|
+
"INFO: Using custom securityResolver. Ensure your resolver properly validates and secures credentials from context."
|
|
369
|
+
);
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
if (options.staticAuth && Object.keys(options.staticAuth).length > 0) {
|
|
373
|
+
result.securityRiskScore = "medium";
|
|
374
|
+
result.warnings.push(
|
|
375
|
+
"SECURITY INFO: Using staticAuth with hardcoded credentials. Ensure credentials are stored securely (environment variables, secrets manager)."
|
|
376
|
+
);
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
const schemesInInput = new Set(options.securitySchemesInInput || []);
|
|
380
|
+
if (options.authProviderMapper || schemesInInput.size > 0) {
|
|
381
|
+
result.securityRiskScore = schemesInInput.size > 0 ? "medium" : "low";
|
|
382
|
+
if (schemesInInput.size > 0) {
|
|
383
|
+
result.warnings.push(
|
|
384
|
+
`INFO: Per-scheme security control enabled. Schemes in input: ${Array.from(schemesInInput).join(", ")}`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
for (const scheme of securitySchemes) {
|
|
388
|
+
if (schemesInInput.has(scheme)) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (!options.authProviderMapper?.[scheme]) {
|
|
392
|
+
result.valid = false;
|
|
393
|
+
result.missingMappings.push(scheme);
|
|
507
394
|
}
|
|
508
395
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
396
|
+
if (!result.valid) {
|
|
397
|
+
result.warnings.push(
|
|
398
|
+
`ERROR: Missing auth provider mappings for security schemes: ${result.missingMappings.join(", ")}`
|
|
399
|
+
);
|
|
513
400
|
}
|
|
401
|
+
return result;
|
|
514
402
|
}
|
|
515
|
-
|
|
403
|
+
if (securitySchemes.size > 0) {
|
|
404
|
+
result.securityRiskScore = "medium";
|
|
405
|
+
result.warnings.push(
|
|
406
|
+
`INFO: No auth configuration provided. Using default ctx.authInfo.token for all security schemes: ${Array.from(
|
|
407
|
+
securitySchemes
|
|
408
|
+
).join(", ")}`
|
|
409
|
+
);
|
|
410
|
+
result.warnings.push(
|
|
411
|
+
"RECOMMENDATION: For multiple auth providers, use authProviderMapper or securityResolver to map each security scheme to the correct auth provider."
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
return result;
|
|
516
415
|
}
|
|
517
|
-
function
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
416
|
+
async function resolveToolSecurity(tool2, ctx, options) {
|
|
417
|
+
const securityResolver = new SecurityResolver();
|
|
418
|
+
const securityContext = await createSecurityContextFromAuth(tool2, ctx, options);
|
|
419
|
+
const hasAuth = securityContext.jwt || securityContext.apiKey || securityContext.basic || securityContext.oauth2Token || securityContext.apiKeys && Object.keys(securityContext.apiKeys).length > 0 || securityContext.customHeaders && Object.keys(securityContext.customHeaders).length > 0;
|
|
420
|
+
const requiresSecurity = tool2.mapper.some((m) => m.security && m.required === true);
|
|
421
|
+
if (requiresSecurity && !hasAuth) {
|
|
422
|
+
const requiredSchemes = tool2.mapper.filter((m) => m.security && m.required === true).map((m) => m.security?.scheme ?? "unknown");
|
|
423
|
+
const uniqueSchemes = [...new Set(requiredSchemes)];
|
|
424
|
+
const schemesStr = uniqueSchemes.join(", ") || "unknown";
|
|
425
|
+
const firstScheme = uniqueSchemes[0] || "BearerAuth";
|
|
426
|
+
throw new Error(
|
|
427
|
+
`Authentication required for tool '${tool2.name}' but no auth configuration found.
|
|
428
|
+
Required security schemes: ${schemesStr}
|
|
429
|
+
Solutions:
|
|
430
|
+
1. Add authProviderMapper: { '${firstScheme}': (ctx) => ctx.authInfo.user?.token }
|
|
431
|
+
2. Add securityResolver: (tool, ctx) => ({ jwt: ctx.authInfo.token })
|
|
432
|
+
3. Add staticAuth: { jwt: process.env.API_TOKEN }
|
|
433
|
+
4. Set generateOptions.includeSecurityInInput: true (not recommended for production)`
|
|
434
|
+
);
|
|
525
435
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
436
|
+
return await securityResolver.resolve(tool2.mapper, securityContext);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// libs/adapters/src/openapi/openapi.utils.ts
|
|
440
|
+
import { validateBaseUrl } from "@frontmcp/utils";
|
|
441
|
+
import { PublicMcpError } from "@frontmcp/sdk";
|
|
442
|
+
function coerceToString(value, paramName, location) {
|
|
443
|
+
if (value === null || value === void 0) {
|
|
444
|
+
return "";
|
|
530
445
|
}
|
|
531
|
-
|
|
532
|
-
if (
|
|
533
|
-
|
|
446
|
+
if (typeof value === "object") {
|
|
447
|
+
if (Array.isArray(value)) {
|
|
448
|
+
if (location === "query") {
|
|
449
|
+
return value.map(String).join(",");
|
|
450
|
+
}
|
|
451
|
+
throw new Error(`${location} parameter '${paramName}' cannot be an array. Received: ${JSON.stringify(value)}`);
|
|
534
452
|
}
|
|
453
|
+
throw new Error(`${location} parameter '${paramName}' cannot be an object. Received: ${JSON.stringify(value)}`);
|
|
535
454
|
}
|
|
536
|
-
return
|
|
455
|
+
return String(value);
|
|
537
456
|
}
|
|
538
|
-
function
|
|
539
|
-
if (
|
|
540
|
-
|
|
541
|
-
const result = {};
|
|
542
|
-
if (typeof obj["enabledInCodeCall"] === "boolean") {
|
|
543
|
-
result.enabledInCodeCall = obj["enabledInCodeCall"];
|
|
544
|
-
} else if (obj["enabledInCodeCall"] !== void 0) {
|
|
545
|
-
warnings.push(`Invalid 'codecall.enabledInCodeCall': expected boolean`);
|
|
546
|
-
}
|
|
547
|
-
if (typeof obj["visibleInListTools"] === "boolean") {
|
|
548
|
-
result.visibleInListTools = obj["visibleInListTools"];
|
|
549
|
-
} else if (obj["visibleInListTools"] !== void 0) {
|
|
550
|
-
warnings.push(`Invalid 'codecall.visibleInListTools': expected boolean`);
|
|
457
|
+
function appendCookie(headers, name, value) {
|
|
458
|
+
if (!/^[\w!#$%&'*+\-.^`|~]+$/.test(name)) {
|
|
459
|
+
throw new Error(`Invalid cookie name: '${name}'. Cookie names must be valid tokens.`);
|
|
551
460
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
461
|
+
const existing = headers.get("Cookie") || "";
|
|
462
|
+
const cookiePair = `${name}=${encodeURIComponent(coerceToString(value, name, "cookie"))}`;
|
|
463
|
+
const combined = existing ? `${existing}; ${cookiePair}` : cookiePair;
|
|
464
|
+
headers.set("Cookie", combined);
|
|
465
|
+
}
|
|
466
|
+
function buildRequest(tool2, input, security, baseUrl) {
|
|
467
|
+
const rawBaseUrl = tool2.metadata.servers?.[0]?.url || baseUrl;
|
|
468
|
+
validateBaseUrl(rawBaseUrl);
|
|
469
|
+
const apiBaseUrl = rawBaseUrl.replace(/\/+$/, "");
|
|
470
|
+
let path = tool2.metadata.path;
|
|
471
|
+
const queryParams = new URLSearchParams();
|
|
472
|
+
const headers = new Headers({
|
|
473
|
+
accept: "application/json",
|
|
474
|
+
...security.headers
|
|
475
|
+
});
|
|
476
|
+
let body;
|
|
477
|
+
for (const mapper of tool2.mapper) {
|
|
478
|
+
if (mapper.security) continue;
|
|
479
|
+
const value = input[mapper.inputKey];
|
|
480
|
+
if (value === void 0 || value === null) {
|
|
481
|
+
if (mapper.required) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
`Required ${mapper.type} parameter '${mapper.key}' (input: '${mapper.inputKey}') is missing for operation '${tool2.name}'`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
switch (mapper.type) {
|
|
489
|
+
case "path":
|
|
490
|
+
path = path.replaceAll(`{${mapper.key}}`, encodeURIComponent(coerceToString(value, mapper.key, "path")));
|
|
491
|
+
break;
|
|
492
|
+
case "query":
|
|
493
|
+
queryParams.set(mapper.key, coerceToString(value, mapper.key, "query"));
|
|
494
|
+
break;
|
|
495
|
+
case "header": {
|
|
496
|
+
const headerValue = coerceToString(value, mapper.key, "header");
|
|
497
|
+
if (/[\r\n\x00\f\v]/.test(headerValue)) {
|
|
498
|
+
throw new PublicMcpError(
|
|
499
|
+
`Invalid header value for '${mapper.key}': contains control characters (possible header injection attack)`,
|
|
500
|
+
"INVALID_HEADER_VALUE",
|
|
501
|
+
400
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
headers.set(mapper.key, headerValue);
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
case "cookie":
|
|
508
|
+
appendCookie(headers, mapper.key, value);
|
|
509
|
+
break;
|
|
510
|
+
case "body":
|
|
511
|
+
if (!body) body = {};
|
|
512
|
+
body[mapper.key] = value;
|
|
513
|
+
break;
|
|
514
|
+
default:
|
|
515
|
+
throw new Error(
|
|
516
|
+
`Unknown mapper type '${mapper.type}' for parameter '${mapper.key}' in operation '${tool2.name}'`
|
|
517
|
+
);
|
|
555
518
|
}
|
|
556
519
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
const validTags = data.filter((item) => typeof item === "string");
|
|
562
|
-
return validTags.length > 0 ? validTags : void 0;
|
|
563
|
-
}
|
|
564
|
-
function extractValidExamples(data, warnings) {
|
|
565
|
-
if (!Array.isArray(data)) return void 0;
|
|
566
|
-
const validExamples = [];
|
|
567
|
-
for (let i = 0; i < data.length; i++) {
|
|
568
|
-
const item = data[i];
|
|
569
|
-
const result = FrontMcpExampleSchema.safeParse(item);
|
|
570
|
-
if (result.success) {
|
|
571
|
-
validExamples.push(result.data);
|
|
572
|
-
} else {
|
|
573
|
-
warnings.push(
|
|
574
|
-
`Invalid example at index ${i}: ${formatZodIssues(result.error.issues, `examples[${i}]`).join(", ")}`
|
|
520
|
+
Object.entries(security.query).forEach(([key, value]) => {
|
|
521
|
+
if (queryParams.has(key)) {
|
|
522
|
+
throw new Error(
|
|
523
|
+
`Query parameter collision: '${key}' is provided both as user input and security parameter. This could indicate a security misconfiguration in operation '${tool2.name}'.`
|
|
575
524
|
);
|
|
576
525
|
}
|
|
526
|
+
queryParams.set(key, coerceToString(value, key, "security query"));
|
|
527
|
+
});
|
|
528
|
+
if (security.cookies && Object.keys(security.cookies).length > 0) {
|
|
529
|
+
Object.entries(security.cookies).forEach(([key, value]) => {
|
|
530
|
+
appendCookie(headers, key, value);
|
|
531
|
+
});
|
|
577
532
|
}
|
|
578
|
-
|
|
533
|
+
if (path.includes("{")) {
|
|
534
|
+
throw new Error(`Failed to resolve all path parameters in ${path} for operation ${tool2.name}`);
|
|
535
|
+
}
|
|
536
|
+
const queryString = queryParams.toString();
|
|
537
|
+
const url = `${apiBaseUrl}${path}${queryString ? `?${queryString}` : ""}`;
|
|
538
|
+
return { url, headers, body };
|
|
579
539
|
}
|
|
580
|
-
function
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
540
|
+
function applyAdditionalHeaders(headers, additionalHeaders) {
|
|
541
|
+
if (!additionalHeaders) return;
|
|
542
|
+
Object.entries(additionalHeaders).forEach(([key, value]) => {
|
|
543
|
+
headers.set(key, value);
|
|
584
544
|
});
|
|
585
545
|
}
|
|
546
|
+
var DEFAULT_MAX_RESPONSE_SIZE = 10 * 1024 * 1024;
|
|
547
|
+
async function parseResponse(response, options) {
|
|
548
|
+
const maxSize = options?.maxResponseSize ?? DEFAULT_MAX_RESPONSE_SIZE;
|
|
549
|
+
const contentLength = response.headers.get("content-length");
|
|
550
|
+
if (contentLength) {
|
|
551
|
+
const length = parseInt(contentLength, 10);
|
|
552
|
+
if (!isNaN(length) && isFinite(length) && length > maxSize) {
|
|
553
|
+
throw new Error(`Response size (${length} bytes) exceeds maximum allowed (${maxSize} bytes)`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const text = await response.text();
|
|
557
|
+
const byteSize = new TextEncoder().encode(text).length;
|
|
558
|
+
if (byteSize > maxSize) {
|
|
559
|
+
throw new Error(`Response size (${byteSize} bytes) exceeds maximum allowed (${maxSize} bytes)`);
|
|
560
|
+
}
|
|
561
|
+
const contentType = response.headers.get("content-type");
|
|
562
|
+
let data;
|
|
563
|
+
if (contentType?.toLowerCase().includes("application/json")) {
|
|
564
|
+
try {
|
|
565
|
+
data = JSON.parse(text);
|
|
566
|
+
} catch {
|
|
567
|
+
data = text;
|
|
568
|
+
}
|
|
569
|
+
} else {
|
|
570
|
+
data = text;
|
|
571
|
+
}
|
|
572
|
+
if (!response.ok) {
|
|
573
|
+
return {
|
|
574
|
+
status: response.status,
|
|
575
|
+
ok: false,
|
|
576
|
+
data,
|
|
577
|
+
error: typeof data === "object" && data !== null && "message" in data ? String(data.message) : typeof data === "string" ? data : `HTTP ${response.status} error`
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
status: response.status,
|
|
582
|
+
ok: true,
|
|
583
|
+
data
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
586
|
|
|
587
587
|
// libs/adapters/src/openapi/openapi.tool.ts
|
|
588
588
|
function createOpenApiTool(openapiTool, options, logger) {
|