@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 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 { z as z2 } from "zod";
18
- import { tool } from "@frontmcp/sdk";
19
- import { convertJsonSchemaToZod } from "zod-from-json-schema";
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
- for (const key of Object.keys(obj)) {
511
- if (!KNOWN_ANNOTATION_FIELDS.has(key)) {
512
- warnings.push(`Unknown field 'annotations.${key}' (will be ignored)`);
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
- return Object.keys(result).length > 0 ? result : void 0;
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 extractValidCache(data, warnings) {
518
- if (typeof data !== "object" || data === null) return void 0;
519
- const obj = data;
520
- const result = {};
521
- if (typeof obj["ttl"] === "number" && Number.isInteger(obj["ttl"]) && obj["ttl"] > 0) {
522
- result.ttl = obj["ttl"];
523
- } else if (obj["ttl"] !== void 0) {
524
- warnings.push(`Invalid 'cache.ttl': expected positive integer`);
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
- if (typeof obj["slideWindow"] === "boolean") {
527
- result.slideWindow = obj["slideWindow"];
528
- } else if (obj["slideWindow"] !== void 0) {
529
- warnings.push(`Invalid 'cache.slideWindow': expected boolean`);
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
- for (const key of Object.keys(obj)) {
532
- if (!KNOWN_CACHE_FIELDS.has(key)) {
533
- warnings.push(`Unknown field 'cache.${key}' (will be ignored)`);
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 Object.keys(result).length > 0 ? result : void 0;
455
+ return String(value);
537
456
  }
538
- function extractValidCodeCall(data, warnings) {
539
- if (typeof data !== "object" || data === null) return void 0;
540
- const obj = data;
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
- for (const key of Object.keys(obj)) {
553
- if (!KNOWN_CODECALL_FIELDS.has(key)) {
554
- warnings.push(`Unknown field 'codecall.${key}' (will be ignored)`);
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
- return Object.keys(result).length > 0 ? result : void 0;
558
- }
559
- function extractValidTags(data) {
560
- if (!Array.isArray(data)) return void 0;
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
- return validExamples.length > 0 ? validExamples : void 0;
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 formatZodIssues(issues, prefix) {
581
- return issues.map((issue) => {
582
- const path = issue.path.length > 0 ? `${prefix}.${issue.path.join(".")}` : prefix;
583
- return `Invalid '${path}': ${issue.message}`;
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) {