@frontmcp/adapters 1.8.3 → 1.8.4

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
@@ -183,7 +183,11 @@ var OpenApiSpecPoller = class {
183
183
  };
184
184
 
185
185
  // libs/adapters/src/openapi/openapi.security.ts
186
- import { createSecurityContext, SecurityResolver } from "mcp-from-openapi";
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,158 @@ 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 = options.securitySchemesInInput ?? [];
424
+ if (options.generateOptions?.includeSecurityInInput !== true && schemesInInput.length > 0) {
425
+ result.securityRiskScore = "high";
426
+ result.warnings.push(
427
+ `SECURITY WARNING: securitySchemesInInput 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.`
428
+ );
429
+ }
430
+ return result;
431
+ }
432
+ function validateCredentialSources(tools, options) {
290
433
  const result = {
291
434
  valid: true,
292
435
  missingMappings: [],
293
436
  warnings: [],
294
437
  securityRiskScore: "low"
295
438
  };
296
- const securitySchemes = extractSecuritySchemes(tools);
439
+ const securitySchemes = describeSecuritySchemes(tools);
297
440
  const includeSecurityInInput = options.generateOptions?.includeSecurityInInput ?? false;
298
441
  if (includeSecurityInInput) {
299
442
  result.securityRiskScore = "high";
300
443
  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."
444
+ "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
445
  );
303
446
  return result;
304
447
  }
@@ -318,19 +461,21 @@ function validateSecurityConfiguration(tools, options) {
318
461
  }
319
462
  const schemesInInput = new Set(options.securitySchemesInInput || []);
320
463
  if (options.authProviderMapper || schemesInInput.size > 0) {
321
- result.securityRiskScore = schemesInInput.size > 0 ? "medium" : "low";
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
- }
464
+ result.securityRiskScore = "low";
327
465
  const passthroughFallback = [];
328
- for (const scheme of securitySchemes) {
466
+ const perRequestHeaders = [];
467
+ const schemeParameters = schemeParametersOf(tools);
468
+ for (const [scheme, security] of securitySchemes) {
329
469
  if (schemesInInput.has(scheme)) {
330
470
  continue;
331
471
  }
332
472
  if (!options.authProviderMapper?.[scheme]) {
333
- if (options.passthroughCallerToken === true) {
473
+ const headerSource = headerCredentialSource(schemeParameters.get(scheme), options);
474
+ if (headerSource === "static") {
475
+ continue;
476
+ } else if (headerSource === "per-request") {
477
+ perRequestHeaders.push(scheme);
478
+ } else if (options.passthroughCallerToken === true && isBearerScheme(security)) {
334
479
  passthroughFallback.push(scheme);
335
480
  } else {
336
481
  result.valid = false;
@@ -343,6 +488,11 @@ function validateSecurityConfiguration(tools, options) {
343
488
  `ERROR: Missing auth provider mappings for security schemes: ${result.missingMappings.join(", ")}`
344
489
  );
345
490
  }
491
+ if (perRequestHeaders.length > 0) {
492
+ result.warnings.push(
493
+ `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.`
494
+ );
495
+ }
346
496
  withPassthroughRisk(result, securitySchemes, options);
347
497
  if (passthroughFallback.length > 0) {
348
498
  result.warnings.push(
@@ -352,10 +502,17 @@ function validateSecurityConfiguration(tools, options) {
352
502
  return result;
353
503
  }
354
504
  if (securitySchemes.size > 0 && options.passthroughCallerToken !== true) {
355
- const schemesStr = Array.from(securitySchemes).join(", ");
505
+ const schemesStr = Array.from(securitySchemes.keys()).join(", ");
356
506
  result.securityRiskScore = "medium";
357
507
  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 will fail. 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.`
508
+ `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.`
509
+ );
510
+ }
511
+ const unfilled = [...securitySchemes].filter(([, security]) => !isBearerScheme(security)).map(([scheme]) => scheme);
512
+ if (options.passthroughCallerToken === true && unfilled.length > 0) {
513
+ result.securityRiskScore = "medium";
514
+ result.warnings.push(
515
+ `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
516
  );
360
517
  }
361
518
  return withPassthroughRisk(result, securitySchemes, options);
@@ -364,10 +521,11 @@ function canPassThroughCallerToken(options) {
364
521
  return options.passthroughCallerToken === true && !options.securityResolver && !hasStaticAuth(options);
365
522
  }
366
523
  function withPassthroughRisk(result, securitySchemes, options) {
367
- if (securitySchemes.size === 0 || !canPassThroughCallerToken(options)) {
524
+ const bearerSchemes = [...securitySchemes].filter(([, security]) => isBearerScheme(security));
525
+ if (bearerSchemes.length === 0 || !canPassThroughCallerToken(options)) {
368
526
  return result;
369
527
  }
370
- const schemesStr = Array.from(securitySchemes).join(", ");
528
+ const schemesStr = bearerSchemes.map(([scheme]) => scheme).join(", ");
371
529
  const when = options.authProviderMapper ? " whenever no authProviderMapper function returns a credential" : "";
372
530
  result.securityRiskScore = "high";
373
531
  result.warnings.push(
@@ -375,28 +533,63 @@ function withPassthroughRisk(result, securitySchemes, options) {
375
533
  );
376
534
  return result;
377
535
  }
378
- async function resolveToolSecurity(tool2, ctx, options) {
536
+ async function resolveToolSecurity(tool2, ctx, options, request = {}) {
379
537
  const securityResolver = new SecurityResolver();
380
538
  const securityContext = await createSecurityContextFromAuth(tool2, ctx, options);
381
539
  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
540
  const requiresSecurity = tool2.mapper.some((m) => m.security && m.required === true);
383
- if (requiresSecurity && !hasAuth) {
384
- const requiredSchemes = tool2.mapper.filter((m) => m.security && m.required === true).map((m) => m.security?.scheme ?? "unknown");
385
- const uniqueSchemes = [...new Set(requiredSchemes)];
386
- const schemesStr = uniqueSchemes.join(", ") || "unknown";
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
- );
541
+ const fromInput = request.input ? await resolveInputSecurity(tool2, request.input, options, securityResolver) : void 0;
542
+ const hasInputCredential = !!fromInput && Object.keys(fromInput.headers).length + Object.keys(fromInput.query).length + Object.keys(fromInput.cookies).length > 0;
543
+ if (requiresSecurity && !hasAuth && !hasInputCredential && !request.deferCredentialCheck) {
544
+ throw authenticationRequiredError(tool2);
398
545
  }
399
- return await securityResolver.resolve(tool2.mapper, securityContext);
546
+ const resolved = await securityResolver.resolve(tool2.mapper, securityContext);
547
+ if (!fromInput) return resolved;
548
+ return {
549
+ ...resolved,
550
+ headers: { ...fromInput.headers, ...resolved.headers },
551
+ query: { ...fromInput.query, ...resolved.query },
552
+ cookies: { ...fromInput.cookies, ...resolved.cookies }
553
+ };
554
+ }
555
+ function isInputScheme(scheme, options) {
556
+ return options.generateOptions?.includeSecurityInInput === true || (options.securitySchemesInInput ?? []).includes(scheme);
557
+ }
558
+ function inputCredentialContext(security, value) {
559
+ const bare = value.replace(/^(?:bearer|basic)\s+/i, "");
560
+ if (security.type === "apiKey") {
561
+ return security.apiKeyName ? { apiKeys: { [security.apiKeyName]: value } } : { apiKey: value };
562
+ }
563
+ if (security.type === "oauth2" || security.type === "openIdConnect") return { oauth2Token: bare };
564
+ if (security.type === "http") {
565
+ const httpScheme = (security.httpScheme ?? "bearer").toLowerCase();
566
+ if (httpScheme === "bearer") return { jwt: bare };
567
+ if (httpScheme === "basic") return { basic: bare };
568
+ }
569
+ return void 0;
570
+ }
571
+ async function resolveInputSecurity(tool2, input, options, securityResolver) {
572
+ const resolved = { headers: {}, query: {}, cookies: {} };
573
+ for (const mapper of tool2.mapper) {
574
+ const security = mapper.security;
575
+ if (!security || !isInputScheme(security.scheme, options)) continue;
576
+ const value = input[mapper.inputKey];
577
+ if (typeof value !== "string" || value === "") continue;
578
+ if (/[\r\n\x00\f\v]/.test(value)) {
579
+ throw new PublicMcpError(
580
+ `Invalid value for '${mapper.inputKey}': contains control characters (possible header injection attack)`,
581
+ "INVALID_HEADER_VALUE",
582
+ 400
583
+ );
584
+ }
585
+ const context = inputCredentialContext(security, value);
586
+ if (!context) continue;
587
+ const one = await securityResolver.resolve([mapper], createSecurityContext(context));
588
+ Object.assign(resolved.headers, one.headers);
589
+ Object.assign(resolved.query, one.query);
590
+ Object.assign(resolved.cookies, one.cookies);
591
+ }
592
+ return resolved;
400
593
  }
401
594
 
402
595
  // libs/adapters/src/openapi/openapi.tool.ts
@@ -645,7 +838,7 @@ function formatZodIssues(issues, prefix) {
645
838
  }
646
839
 
647
840
  // libs/adapters/src/openapi/openapi.utils.ts
648
- import { PublicMcpError } from "@frontmcp/sdk";
841
+ import { PublicMcpError as PublicMcpError2 } from "@frontmcp/sdk";
649
842
  import { trimTrailing, validateBaseUrl } from "@frontmcp/utils";
650
843
  function coerceToString(value, paramName, location) {
651
844
  if (value === null || value === void 0) {
@@ -703,7 +896,7 @@ function buildRequest(tool2, input, security, baseUrl) {
703
896
  case "header": {
704
897
  const headerValue = coerceToString(value, mapper.key, "header");
705
898
  if (/[\r\n\x00\f\v]/.test(headerValue)) {
706
- throw new PublicMcpError(
899
+ throw new PublicMcpError2(
707
900
  `Invalid header value for '${mapper.key}': contains control characters (possible header injection attack)`,
708
901
  "INVALID_HEADER_VALUE",
709
902
  400
@@ -880,7 +1073,10 @@ function createOpenApiTool(openapiTool, options, logger) {
880
1073
  inputTransforms,
881
1074
  transformContext
882
1075
  );
883
- const security = await resolveToolSecurity(openapiTool, ctx, options);
1076
+ const security = await resolveToolSecurity(openapiTool, ctx, options, {
1077
+ input: injectedInput,
1078
+ deferCredentialCheck: true
1079
+ });
884
1080
  const { url, headers, body: requestBody } = buildRequest(openapiTool, injectedInput, security, options.baseUrl);
885
1081
  applyAdditionalHeaders(headers, options.additionalHeaders);
886
1082
  if (options.headersMapper) {
@@ -896,6 +1092,7 @@ function createOpenApiTool(openapiTool, options, logger) {
896
1092
  throw new Error(`headersMapper failed for tool '${openapiTool.name}': ${errorMessage}`, { cause: err });
897
1093
  }
898
1094
  }
1095
+ assertRequestHasCredential(openapiTool, url, headers);
899
1096
  let finalBody = requestBody;
900
1097
  if (options.bodyMapper && requestBody) {
901
1098
  try {
@@ -1180,28 +1377,7 @@ var OpenapiAdapter = class extends DynamicAdapter {
1180
1377
  });
1181
1378
  }
1182
1379
  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
- );
1380
+ throw new Error(formatMissingSecurityMappingsError(this.options.name, openapiTools, validation.missingMappings));
1205
1381
  }
1206
1382
  let transformedTools = openapiTools;
1207
1383
  if (this.options.descriptionMode && this.options.descriptionMode !== "summaryOnly") {