@frontmcp/adapters 1.8.3 → 1.8.5
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 +256 -68
- package/esm/openapi/index.mjs +256 -68
- package/esm/package.json +6 -6
- package/index.js +257 -72
- package/openapi/index.js +257 -72
- package/openapi/openapi.adapter.d.ts +3 -2
- package/openapi/openapi.adapter.d.ts.map +1 -1
- package/openapi/openapi.security.d.ts +49 -4
- package/openapi/openapi.security.d.ts.map +1 -1
- package/openapi/openapi.tool.d.ts.map +1 -1
- package/openapi/openapi.types.d.ts +4 -2
- package/openapi/openapi.types.d.ts.map +1 -1
- package/package.json +6 -6
- package/skills/bundle/bundle.schema.d.ts +2 -2
package/esm/index.mjs
CHANGED
|
@@ -183,7 +183,11 @@ var OpenApiSpecPoller = class {
|
|
|
183
183
|
};
|
|
184
184
|
|
|
185
185
|
// libs/adapters/src/openapi/openapi.security.ts
|
|
186
|
-
import {
|
|
186
|
+
import {
|
|
187
|
+
createSecurityContext,
|
|
188
|
+
SecurityResolver
|
|
189
|
+
} from "mcp-from-openapi";
|
|
190
|
+
import { PublicMcpError } from "@frontmcp/sdk";
|
|
187
191
|
async function createSecurityContextFromAuth(tool2, ctx, options) {
|
|
188
192
|
if (options.securityResolver) {
|
|
189
193
|
return await options.securityResolver(tool2, ctx);
|
|
@@ -238,7 +242,7 @@ async function createSecurityContextFromAuth(tool2, ctx, options) {
|
|
|
238
242
|
throw err;
|
|
239
243
|
}
|
|
240
244
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
241
|
-
throw new Error(`authProviderMapper['${scheme}'] threw an error: ${errorMessage}
|
|
245
|
+
throw new Error(`authProviderMapper['${scheme}'] threw an error: ${errorMessage}`, { cause: err });
|
|
242
246
|
}
|
|
243
247
|
}
|
|
244
248
|
}
|
|
@@ -286,19 +290,163 @@ function extractSecuritySchemes(tools) {
|
|
|
286
290
|
}
|
|
287
291
|
return schemes;
|
|
288
292
|
}
|
|
293
|
+
function describeSecuritySchemes(tools) {
|
|
294
|
+
const schemes = /* @__PURE__ */ new Map();
|
|
295
|
+
for (const tool2 of tools) {
|
|
296
|
+
for (const mapper of tool2.mapper) {
|
|
297
|
+
if (mapper.security?.scheme && !schemes.has(mapper.security.scheme)) {
|
|
298
|
+
schemes.set(mapper.security.scheme, mapper.security);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return schemes;
|
|
303
|
+
}
|
|
304
|
+
function schemeParametersOf(tools) {
|
|
305
|
+
const parameters = /* @__PURE__ */ new Map();
|
|
306
|
+
for (const tool2 of tools) {
|
|
307
|
+
for (const mapper of tool2.mapper) {
|
|
308
|
+
const scheme = mapper.security?.scheme;
|
|
309
|
+
if (scheme && !parameters.has(scheme)) parameters.set(scheme, mapper);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return parameters;
|
|
313
|
+
}
|
|
314
|
+
function headerCredentialSource(parameter, options) {
|
|
315
|
+
if (!parameter) return void 0;
|
|
316
|
+
const staticHeaders = new Headers(options.additionalHeaders ?? {});
|
|
317
|
+
if (carriesSchemeParameter(parameter, new URL("http://startup.invalid/"), staticHeaders)) return "static";
|
|
318
|
+
if (options.headersMapper && (parameter.type === "header" || parameter.type === "cookie")) return "per-request";
|
|
319
|
+
return void 0;
|
|
320
|
+
}
|
|
321
|
+
function isBearerScheme(security) {
|
|
322
|
+
if (!security || security.type !== "http") return false;
|
|
323
|
+
return (security.httpScheme ?? "bearer").toLowerCase() === "bearer";
|
|
324
|
+
}
|
|
325
|
+
function credentialFieldOf(security) {
|
|
326
|
+
if (security?.type === "apiKey") return "apiKey";
|
|
327
|
+
if (security?.type === "oauth2" || security?.type === "openIdConnect") return "oauth2Token";
|
|
328
|
+
if (security?.type === "http" && security.httpScheme?.toLowerCase() === "basic") return "basic";
|
|
329
|
+
return "jwt";
|
|
330
|
+
}
|
|
331
|
+
function formatMissingSecurityMappingsError(adapterName, tools, missingMappings) {
|
|
332
|
+
const schemes = describeSecuritySchemes(tools);
|
|
333
|
+
const fieldOf = (scheme) => credentialFieldOf(schemes.get(scheme));
|
|
334
|
+
const firstField = fieldOf(missingMappings[0] ?? "");
|
|
335
|
+
const bearerSchemes = missingMappings.filter((scheme) => isBearerScheme(schemes.get(scheme)));
|
|
336
|
+
const lines = [
|
|
337
|
+
`[OpenAPI Adapter: ${adapterName}] Invalid security configuration.`,
|
|
338
|
+
`Missing auth provider mappings for security schemes: ${missingMappings.join(", ")}`,
|
|
339
|
+
"",
|
|
340
|
+
"Your OpenAPI spec requires these security schemes, but no credential option covers them.",
|
|
341
|
+
"",
|
|
342
|
+
"Add one of the following to your adapter configuration:",
|
|
343
|
+
"",
|
|
344
|
+
"1. authProviderMapper (recommended): a function per scheme that returns the credential issued for the API:",
|
|
345
|
+
" authProviderMapper: {",
|
|
346
|
+
...missingMappings.map((scheme) => ` '${scheme}': (ctx) => getApiCredential(ctx), // the ${fieldOf(scheme)}`),
|
|
347
|
+
" }",
|
|
348
|
+
"",
|
|
349
|
+
"2. securityResolver:",
|
|
350
|
+
` securityResolver: async (tool, ctx) => ({ ${firstField}: await getApiCredential(ctx) })`,
|
|
351
|
+
"",
|
|
352
|
+
"3. staticAuth (one credential for every caller):",
|
|
353
|
+
` staticAuth: { ${firstField}: process.env.API_CREDENTIAL }`
|
|
354
|
+
];
|
|
355
|
+
if (bearerSchemes.length > 0) {
|
|
356
|
+
lines.push(
|
|
357
|
+
"",
|
|
358
|
+
`4. passthroughCallerToken: true, for the HTTP bearer schemes (${bearerSchemes.join(", ")}) only, and only if the API`,
|
|
359
|
+
" accepts the MCP client's own token (same issuer and audience). It sends nothing for other schemes."
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
return lines.join("\n");
|
|
363
|
+
}
|
|
364
|
+
function authenticationRequiredError(tool2) {
|
|
365
|
+
const required = requiredSecurityOf(tool2);
|
|
366
|
+
const schemes = [...new Map(required.map((security) => [security.scheme, security])).values()];
|
|
367
|
+
const schemesStr = schemes.map((security) => `${security.scheme} (${security.type})`).join(", ") || "unknown";
|
|
368
|
+
const first = schemes[0];
|
|
369
|
+
const firstScheme = first?.scheme ?? "BearerAuth";
|
|
370
|
+
const field = credentialFieldOf(first);
|
|
371
|
+
const bearerSchemes = schemes.filter(isBearerScheme).map((security) => security.scheme);
|
|
372
|
+
const solutions = [
|
|
373
|
+
` 1. Add authProviderMapper: { '${firstScheme}': (ctx) => getApiCredential(ctx) }, returning the ${field} issued for the API`,
|
|
374
|
+
` 2. Add securityResolver: async (tool, ctx) => ({ ${field}: await getApiCredential(ctx) })`,
|
|
375
|
+
` 3. Add staticAuth: { ${field}: process.env.API_CREDENTIAL }`
|
|
376
|
+
];
|
|
377
|
+
if (bearerSchemes.length > 0) {
|
|
378
|
+
solutions.push(
|
|
379
|
+
` 4. Set passthroughCallerToken: true, only if the API accepts the MCP client's own token (it fills ${bearerSchemes.join(", ")} only)`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return new Error(
|
|
383
|
+
`Authentication required for tool '${tool2.name}': no credential for its security schemes.
|
|
384
|
+
Required security schemes: ${schemesStr}
|
|
385
|
+
Solutions:
|
|
386
|
+
` + solutions.join("\n")
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
function requiredSecurityOf(tool2) {
|
|
390
|
+
return tool2.mapper.flatMap((mapper) => mapper.security && mapper.required === true ? [mapper.security] : []);
|
|
391
|
+
}
|
|
392
|
+
function authorizationSchemeOf(security) {
|
|
393
|
+
if (security?.type === "http") return (security.httpScheme ?? "bearer").toLowerCase();
|
|
394
|
+
if (security?.type === "oauth2" || security?.type === "openIdConnect") return "bearer";
|
|
395
|
+
return void 0;
|
|
396
|
+
}
|
|
397
|
+
function carriesSchemeParameter(mapper, url, headers) {
|
|
398
|
+
if (mapper.type === "query") return !!url.searchParams.get(mapper.key);
|
|
399
|
+
if (mapper.type === "cookie") {
|
|
400
|
+
return (headers.get("cookie") ?? "").split(";").some((pair) => {
|
|
401
|
+
const [name, ...value2] = pair.trim().split("=");
|
|
402
|
+
return name === mapper.key && value2.join("=") !== "";
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
if (mapper.type !== "header") return false;
|
|
406
|
+
const value = headers.get(mapper.key)?.trim();
|
|
407
|
+
if (!value) return false;
|
|
408
|
+
const scheme = authorizationSchemeOf(mapper.security);
|
|
409
|
+
if (!scheme) return true;
|
|
410
|
+
const space = value.indexOf(" ");
|
|
411
|
+
return space > 0 && value.slice(0, space).toLowerCase() === scheme && value.slice(space + 1).trim() !== "";
|
|
412
|
+
}
|
|
413
|
+
function assertRequestHasCredential(tool2, url, headers) {
|
|
414
|
+
const securityMappers = tool2.mapper.filter((mapper) => mapper.security && mapper.required === true);
|
|
415
|
+
if (securityMappers.length === 0) return;
|
|
416
|
+
const parsedUrl = new URL(url);
|
|
417
|
+
if (!securityMappers.some((mapper) => carriesSchemeParameter(mapper, parsedUrl, headers))) {
|
|
418
|
+
throw authenticationRequiredError(tool2);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
289
421
|
function validateSecurityConfiguration(tools, options) {
|
|
422
|
+
const result = validateCredentialSources(tools, options);
|
|
423
|
+
const schemesInInput = inputSecuritySchemes(options);
|
|
424
|
+
if (schemesInInput !== "all" && schemesInInput.size > 0) {
|
|
425
|
+
const include = options.generateOptions?.includeSecurityInInput;
|
|
426
|
+
const sources = [
|
|
427
|
+
(options.securitySchemesInInput?.length ?? 0) > 0 ? "securitySchemesInInput" : void 0,
|
|
428
|
+
Array.isArray(include) && include.length > 0 ? "an includeSecurityInInput list" : void 0
|
|
429
|
+
].filter((source) => source !== void 0);
|
|
430
|
+
result.securityRiskScore = "high";
|
|
431
|
+
result.warnings.push(
|
|
432
|
+
`SECURITY WARNING: ${sources.join(" and ")} ${sources.length > 1 ? "are" : "is"} enabled. The model provides the credential for security schemes ${[...schemesInInput].join(", ")} in tool inputs, used when the server supplies none. Credentials may be logged or exposed, and the model chooses whose account a call uses.`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
return result;
|
|
436
|
+
}
|
|
437
|
+
function validateCredentialSources(tools, options) {
|
|
290
438
|
const result = {
|
|
291
439
|
valid: true,
|
|
292
440
|
missingMappings: [],
|
|
293
441
|
warnings: [],
|
|
294
442
|
securityRiskScore: "low"
|
|
295
443
|
};
|
|
296
|
-
const securitySchemes =
|
|
297
|
-
const
|
|
298
|
-
if (
|
|
444
|
+
const securitySchemes = describeSecuritySchemes(tools);
|
|
445
|
+
const inputSchemes = inputSecuritySchemes(options);
|
|
446
|
+
if (inputSchemes === "all") {
|
|
299
447
|
result.securityRiskScore = "high";
|
|
300
448
|
result.warnings.push(
|
|
301
|
-
"SECURITY WARNING: includeSecurityInInput is enabled. Users will provide authentication directly in tool inputs. This increases security risk as credentials may be logged or exposed."
|
|
449
|
+
"SECURITY WARNING: includeSecurityInInput is enabled. Users will provide authentication directly in tool inputs (used for a scheme when the server supplies none). This increases security risk as credentials may be logged or exposed."
|
|
302
450
|
);
|
|
303
451
|
return result;
|
|
304
452
|
}
|
|
@@ -316,21 +464,23 @@ function validateSecurityConfiguration(tools, options) {
|
|
|
316
464
|
);
|
|
317
465
|
return withPassthroughRisk(result, securitySchemes, options);
|
|
318
466
|
}
|
|
319
|
-
const schemesInInput =
|
|
467
|
+
const schemesInInput = inputSchemes;
|
|
320
468
|
if (options.authProviderMapper || schemesInInput.size > 0) {
|
|
321
|
-
result.securityRiskScore =
|
|
322
|
-
if (schemesInInput.size > 0) {
|
|
323
|
-
result.warnings.push(
|
|
324
|
-
`INFO: Per-scheme security control enabled. Schemes in input: ${Array.from(schemesInInput).join(", ")}`
|
|
325
|
-
);
|
|
326
|
-
}
|
|
469
|
+
result.securityRiskScore = "low";
|
|
327
470
|
const passthroughFallback = [];
|
|
328
|
-
|
|
471
|
+
const perRequestHeaders = [];
|
|
472
|
+
const schemeParameters = schemeParametersOf(tools);
|
|
473
|
+
for (const [scheme, security] of securitySchemes) {
|
|
329
474
|
if (schemesInInput.has(scheme)) {
|
|
330
475
|
continue;
|
|
331
476
|
}
|
|
332
477
|
if (!options.authProviderMapper?.[scheme]) {
|
|
333
|
-
|
|
478
|
+
const headerSource = headerCredentialSource(schemeParameters.get(scheme), options);
|
|
479
|
+
if (headerSource === "static") {
|
|
480
|
+
continue;
|
|
481
|
+
} else if (headerSource === "per-request") {
|
|
482
|
+
perRequestHeaders.push(scheme);
|
|
483
|
+
} else if (options.passthroughCallerToken === true && isBearerScheme(security)) {
|
|
334
484
|
passthroughFallback.push(scheme);
|
|
335
485
|
} else {
|
|
336
486
|
result.valid = false;
|
|
@@ -343,6 +493,11 @@ function validateSecurityConfiguration(tools, options) {
|
|
|
343
493
|
`ERROR: Missing auth provider mappings for security schemes: ${result.missingMappings.join(", ")}`
|
|
344
494
|
);
|
|
345
495
|
}
|
|
496
|
+
if (perRequestHeaders.length > 0) {
|
|
497
|
+
result.warnings.push(
|
|
498
|
+
`INFO: Security schemes with no authProviderMapper entry (${perRequestHeaders.join(", ")}) take their credential from headersMapper. An operation that requires only them is refused when headersMapper sets none.`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
346
501
|
withPassthroughRisk(result, securitySchemes, options);
|
|
347
502
|
if (passthroughFallback.length > 0) {
|
|
348
503
|
result.warnings.push(
|
|
@@ -352,10 +507,17 @@ function validateSecurityConfiguration(tools, options) {
|
|
|
352
507
|
return result;
|
|
353
508
|
}
|
|
354
509
|
if (securitySchemes.size > 0 && options.passthroughCallerToken !== true) {
|
|
355
|
-
const schemesStr = Array.from(securitySchemes).join(", ");
|
|
510
|
+
const schemesStr = Array.from(securitySchemes.keys()).join(", ");
|
|
356
511
|
result.securityRiskScore = "medium";
|
|
357
512
|
result.warnings.push(
|
|
358
|
-
`SECURITY WARNING: No auth configuration provided, so the adapter has no credentials for the API (security schemes: ${schemesStr}). Operations that require authentication
|
|
513
|
+
`SECURITY WARNING: No auth configuration provided, so the adapter has no credentials for the API (security schemes: ${schemesStr}). Operations that require authentication fail unless additionalHeaders or headersMapper sets their credential. The MCP client's own token is not forwarded: configure authProviderMapper, securityResolver or staticAuth, or set passthroughCallerToken: true if the API is meant to receive the caller's MCP token.`
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
const unfilled = [...securitySchemes].filter(([, security]) => !isBearerScheme(security)).map(([scheme]) => scheme);
|
|
517
|
+
if (options.passthroughCallerToken === true && unfilled.length > 0) {
|
|
518
|
+
result.securityRiskScore = "medium";
|
|
519
|
+
result.warnings.push(
|
|
520
|
+
`SECURITY WARNING: The adapter has no credentials for the API for security schemes: ${unfilled.join(", ")}. passthroughCallerToken sends only a bearer token, which these schemes don't use, so operations that require only them fail unless additionalHeaders or headersMapper sets their credential. Configure authProviderMapper, securityResolver or staticAuth for them.`
|
|
359
521
|
);
|
|
360
522
|
}
|
|
361
523
|
return withPassthroughRisk(result, securitySchemes, options);
|
|
@@ -364,10 +526,11 @@ function canPassThroughCallerToken(options) {
|
|
|
364
526
|
return options.passthroughCallerToken === true && !options.securityResolver && !hasStaticAuth(options);
|
|
365
527
|
}
|
|
366
528
|
function withPassthroughRisk(result, securitySchemes, options) {
|
|
367
|
-
|
|
529
|
+
const bearerSchemes = [...securitySchemes].filter(([, security]) => isBearerScheme(security));
|
|
530
|
+
if (bearerSchemes.length === 0 || !canPassThroughCallerToken(options)) {
|
|
368
531
|
return result;
|
|
369
532
|
}
|
|
370
|
-
const schemesStr =
|
|
533
|
+
const schemesStr = bearerSchemes.map(([scheme]) => scheme).join(", ");
|
|
371
534
|
const when = options.authProviderMapper ? " whenever no authProviderMapper function returns a credential" : "";
|
|
372
535
|
result.securityRiskScore = "high";
|
|
373
536
|
result.warnings.push(
|
|
@@ -375,28 +538,69 @@ function withPassthroughRisk(result, securitySchemes, options) {
|
|
|
375
538
|
);
|
|
376
539
|
return result;
|
|
377
540
|
}
|
|
378
|
-
async function resolveToolSecurity(tool2, ctx, options) {
|
|
541
|
+
async function resolveToolSecurity(tool2, ctx, options, request = {}) {
|
|
379
542
|
const securityResolver = new SecurityResolver();
|
|
380
543
|
const securityContext = await createSecurityContextFromAuth(tool2, ctx, options);
|
|
381
544
|
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;
|
|
382
545
|
const requiresSecurity = tool2.mapper.some((m) => m.security && m.required === true);
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const firstScheme = uniqueSchemes[0] || "BearerAuth";
|
|
388
|
-
throw new Error(
|
|
389
|
-
`Authentication required for tool '${tool2.name}' but no auth configuration found.
|
|
390
|
-
Required security schemes: ${schemesStr}
|
|
391
|
-
Solutions:
|
|
392
|
-
1. Add authProviderMapper: { '${firstScheme}': (ctx) => ctx.authInfo.user?.token }
|
|
393
|
-
2. Add securityResolver: async (tool, ctx) => ({ jwt: await getApiToken(ctx) })
|
|
394
|
-
3. Add staticAuth: { jwt: process.env.API_TOKEN }
|
|
395
|
-
4. Set passthroughCallerToken: true, only if the API accepts the MCP client's own token
|
|
396
|
-
5. Set generateOptions.includeSecurityInInput: true (not recommended for production)`
|
|
397
|
-
);
|
|
546
|
+
const fromInput = request.input ? await resolveInputSecurity(tool2, request.input, options, securityResolver) : void 0;
|
|
547
|
+
const hasInputCredential = !!fromInput && Object.keys(fromInput.headers).length + Object.keys(fromInput.query).length + Object.keys(fromInput.cookies).length > 0;
|
|
548
|
+
if (requiresSecurity && !hasAuth && !hasInputCredential && !request.deferCredentialCheck) {
|
|
549
|
+
throw authenticationRequiredError(tool2);
|
|
398
550
|
}
|
|
399
|
-
|
|
551
|
+
const resolved = await securityResolver.resolve(tool2.mapper, securityContext);
|
|
552
|
+
if (!fromInput) return resolved;
|
|
553
|
+
return {
|
|
554
|
+
...resolved,
|
|
555
|
+
headers: { ...fromInput.headers, ...resolved.headers },
|
|
556
|
+
query: { ...fromInput.query, ...resolved.query },
|
|
557
|
+
cookies: { ...fromInput.cookies, ...resolved.cookies }
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function inputSecuritySchemes(options) {
|
|
561
|
+
const include = options.generateOptions?.includeSecurityInInput;
|
|
562
|
+
if (include === true) return "all";
|
|
563
|
+
return /* @__PURE__ */ new Set([...Array.isArray(include) ? include : [], ...options.securitySchemesInInput ?? []]);
|
|
564
|
+
}
|
|
565
|
+
function isInputScheme(scheme, options) {
|
|
566
|
+
const schemes = inputSecuritySchemes(options);
|
|
567
|
+
return schemes === "all" || schemes.has(scheme);
|
|
568
|
+
}
|
|
569
|
+
function inputCredentialContext(security, value) {
|
|
570
|
+
const bare = value.replace(/^(?:bearer|basic)\s+/i, "");
|
|
571
|
+
if (security.type === "apiKey") {
|
|
572
|
+
return security.apiKeyName ? { apiKeys: { [security.apiKeyName]: value } } : { apiKey: value };
|
|
573
|
+
}
|
|
574
|
+
if (security.type === "oauth2" || security.type === "openIdConnect") return { oauth2Token: bare };
|
|
575
|
+
if (security.type === "http") {
|
|
576
|
+
const httpScheme = (security.httpScheme ?? "bearer").toLowerCase();
|
|
577
|
+
if (httpScheme === "bearer") return { jwt: bare };
|
|
578
|
+
if (httpScheme === "basic") return { basic: bare };
|
|
579
|
+
}
|
|
580
|
+
return void 0;
|
|
581
|
+
}
|
|
582
|
+
async function resolveInputSecurity(tool2, input, options, securityResolver) {
|
|
583
|
+
const resolved = { headers: {}, query: {}, cookies: {} };
|
|
584
|
+
for (const mapper of tool2.mapper) {
|
|
585
|
+
const security = mapper.security;
|
|
586
|
+
if (!security || !isInputScheme(security.scheme, options)) continue;
|
|
587
|
+
const value = input[mapper.inputKey];
|
|
588
|
+
if (typeof value !== "string" || value === "") continue;
|
|
589
|
+
if (/[\r\n\x00\f\v]/.test(value)) {
|
|
590
|
+
throw new PublicMcpError(
|
|
591
|
+
`Invalid value for '${mapper.inputKey}': contains control characters (possible header injection attack)`,
|
|
592
|
+
"INVALID_HEADER_VALUE",
|
|
593
|
+
400
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
const context = inputCredentialContext(security, value);
|
|
597
|
+
if (!context) continue;
|
|
598
|
+
const one = await securityResolver.resolve([mapper], createSecurityContext(context));
|
|
599
|
+
Object.assign(resolved.headers, one.headers);
|
|
600
|
+
Object.assign(resolved.query, one.query);
|
|
601
|
+
Object.assign(resolved.cookies, one.cookies);
|
|
602
|
+
}
|
|
603
|
+
return resolved;
|
|
400
604
|
}
|
|
401
605
|
|
|
402
606
|
// libs/adapters/src/openapi/openapi.tool.ts
|
|
@@ -645,7 +849,7 @@ function formatZodIssues(issues, prefix) {
|
|
|
645
849
|
}
|
|
646
850
|
|
|
647
851
|
// libs/adapters/src/openapi/openapi.utils.ts
|
|
648
|
-
import { PublicMcpError } from "@frontmcp/sdk";
|
|
852
|
+
import { PublicMcpError as PublicMcpError2 } from "@frontmcp/sdk";
|
|
649
853
|
import { trimTrailing, validateBaseUrl } from "@frontmcp/utils";
|
|
650
854
|
function coerceToString(value, paramName, location) {
|
|
651
855
|
if (value === null || value === void 0) {
|
|
@@ -703,7 +907,7 @@ function buildRequest(tool2, input, security, baseUrl) {
|
|
|
703
907
|
case "header": {
|
|
704
908
|
const headerValue = coerceToString(value, mapper.key, "header");
|
|
705
909
|
if (/[\r\n\x00\f\v]/.test(headerValue)) {
|
|
706
|
-
throw new
|
|
910
|
+
throw new PublicMcpError2(
|
|
707
911
|
`Invalid header value for '${mapper.key}': contains control characters (possible header injection attack)`,
|
|
708
912
|
"INVALID_HEADER_VALUE",
|
|
709
913
|
400
|
|
@@ -880,7 +1084,10 @@ function createOpenApiTool(openapiTool, options, logger) {
|
|
|
880
1084
|
inputTransforms,
|
|
881
1085
|
transformContext
|
|
882
1086
|
);
|
|
883
|
-
const security = await resolveToolSecurity(openapiTool, ctx, options
|
|
1087
|
+
const security = await resolveToolSecurity(openapiTool, ctx, options, {
|
|
1088
|
+
input: injectedInput,
|
|
1089
|
+
deferCredentialCheck: true
|
|
1090
|
+
});
|
|
884
1091
|
const { url, headers, body: requestBody } = buildRequest(openapiTool, injectedInput, security, options.baseUrl);
|
|
885
1092
|
applyAdditionalHeaders(headers, options.additionalHeaders);
|
|
886
1093
|
if (options.headersMapper) {
|
|
@@ -896,6 +1103,7 @@ function createOpenApiTool(openapiTool, options, logger) {
|
|
|
896
1103
|
throw new Error(`headersMapper failed for tool '${openapiTool.name}': ${errorMessage}`, { cause: err });
|
|
897
1104
|
}
|
|
898
1105
|
}
|
|
1106
|
+
assertRequestHasCredential(openapiTool, url, headers);
|
|
899
1107
|
let finalBody = requestBody;
|
|
900
1108
|
if (options.bodyMapper && requestBody) {
|
|
901
1109
|
try {
|
|
@@ -1144,8 +1352,9 @@ var OpenapiAdapter = class extends DynamicAdapter {
|
|
|
1144
1352
|
if (!this.generator) {
|
|
1145
1353
|
this.generator = await this.initializeGenerator();
|
|
1146
1354
|
}
|
|
1147
|
-
const
|
|
1148
|
-
const
|
|
1355
|
+
const inputSchemes = inputSecuritySchemes(this.options);
|
|
1356
|
+
const hasPerSchemeControl = inputSchemes !== "all" && inputSchemes.size > 0;
|
|
1357
|
+
const includeSecurityInInput = inputSchemes === "all" || hasPerSchemeControl;
|
|
1149
1358
|
let openapiTools = await this.generator.generateTools({
|
|
1150
1359
|
includeOperations: this.options.generateOptions?.includeOperations,
|
|
1151
1360
|
excludeOperations: this.options.generateOptions?.excludeOperations,
|
|
@@ -1161,7 +1370,7 @@ var OpenapiAdapter = class extends DynamicAdapter {
|
|
|
1161
1370
|
formatResolvers: this.options.generateOptions?.formatResolvers
|
|
1162
1371
|
});
|
|
1163
1372
|
if (hasPerSchemeControl) {
|
|
1164
|
-
openapiTools = openapiTools.map((tool2) => this.filterSecuritySchemes(tool2));
|
|
1373
|
+
openapiTools = openapiTools.map((tool2) => this.filterSecuritySchemes(tool2, inputSchemes));
|
|
1165
1374
|
}
|
|
1166
1375
|
const validation = validateSecurityConfiguration(openapiTools, this.options);
|
|
1167
1376
|
this.logger.info("Security Analysis:");
|
|
@@ -1180,28 +1389,7 @@ var OpenapiAdapter = class extends DynamicAdapter {
|
|
|
1180
1389
|
});
|
|
1181
1390
|
}
|
|
1182
1391
|
if (!validation.valid) {
|
|
1183
|
-
throw new Error(
|
|
1184
|
-
`[OpenAPI Adapter: ${this.options.name}] Invalid security configuration.
|
|
1185
|
-
Missing auth provider mappings for security schemes: ${validation.missingMappings.join(", ")}
|
|
1186
|
-
|
|
1187
|
-
Your OpenAPI spec requires these security schemes, but no auth configuration was provided.
|
|
1188
|
-
|
|
1189
|
-
Add one of the following to your adapter configuration:
|
|
1190
|
-
|
|
1191
|
-
1. authProviderMapper (recommended):
|
|
1192
|
-
authProviderMapper: {
|
|
1193
|
-
` + validation.missingMappings.map((s) => ` '${s}': (authInfo) => authInfo.user?.${s.toLowerCase()}Token,`).join("\n") + `
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
2. securityResolver:
|
|
1197
|
-
securityResolver: async (tool, ctx) => ({ jwt: await getApiToken(ctx) })
|
|
1198
|
-
|
|
1199
|
-
3. staticAuth:
|
|
1200
|
-
staticAuth: { jwt: process.env.API_TOKEN }
|
|
1201
|
-
|
|
1202
|
-
4. Include security in input (NOT recommended for production):
|
|
1203
|
-
generateOptions: { includeSecurityInInput: true }`
|
|
1204
|
-
);
|
|
1392
|
+
throw new Error(formatMissingSecurityMappingsError(this.options.name, openapiTools, validation.missingMappings));
|
|
1205
1393
|
}
|
|
1206
1394
|
let transformedTools = openapiTools;
|
|
1207
1395
|
if (this.options.descriptionMode && this.options.descriptionMode !== "summaryOnly") {
|
|
@@ -1493,12 +1681,12 @@ ${opDescription}`;
|
|
|
1493
1681
|
};
|
|
1494
1682
|
}
|
|
1495
1683
|
/**
|
|
1496
|
-
* Filter security schemes in tool input
|
|
1497
|
-
* Removes security inputs that should be resolved from context
|
|
1684
|
+
* Filter security schemes in tool input to the ones named for it (`securitySchemesInInput`, or an
|
|
1685
|
+
* `includeSecurityInInput` list). Removes security inputs that should be resolved from context
|
|
1686
|
+
* instead of user input.
|
|
1498
1687
|
* @private
|
|
1499
1688
|
*/
|
|
1500
|
-
filterSecuritySchemes(tool2) {
|
|
1501
|
-
const allowedSchemes = new Set(this.options.securitySchemesInInput || []);
|
|
1689
|
+
filterSecuritySchemes(tool2, allowedSchemes) {
|
|
1502
1690
|
if (allowedSchemes.size === 0) return tool2;
|
|
1503
1691
|
const schemesToRemove = /* @__PURE__ */ new Set();
|
|
1504
1692
|
const inputKeysToRemove = /* @__PURE__ */ new Set();
|