@frontmcp/adapters 1.8.2 → 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 +287 -72
- package/esm/openapi/index.mjs +287 -72
- package/esm/package.json +6 -6
- package/esm/skills/index.mjs +94 -12
- package/index.js +288 -76
- package/openapi/index.js +288 -76
- package/openapi/openapi.adapter.d.ts.map +1 -1
- package/openapi/openapi.security.d.ts +44 -5
- package/openapi/openapi.security.d.ts.map +1 -1
- package/openapi/openapi.tool.d.ts.map +1 -1
- package/openapi/openapi.types.d.ts +19 -0
- package/openapi/openapi.types.d.ts.map +1 -1
- package/package.json +6 -6
- package/skills/bundle/bundle.schema.d.ts +2 -2
- package/skills/index.js +94 -12
- package/skills/source-options.d.ts.map +1 -1
- package/skills/sources/npm.source.d.ts +9 -4
- package/skills/sources/npm.source.d.ts.map +1 -1
- package/skills/sources/saas-pull.source.d.ts +25 -2
- package/skills/sources/saas-pull.source.d.ts.map +1 -1
package/index.js
CHANGED
|
@@ -46,7 +46,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
46
46
|
|
|
47
47
|
// libs/adapters/src/openapi/openapi.adapter.ts
|
|
48
48
|
var import_mcp_from_openapi3 = require("mcp-from-openapi");
|
|
49
|
-
var
|
|
49
|
+
var import_sdk5 = require("@frontmcp/sdk");
|
|
50
50
|
|
|
51
51
|
// libs/adapters/src/openapi/openapi-spec-poller.ts
|
|
52
52
|
var import_mcp_from_openapi = require("mcp-from-openapi");
|
|
@@ -219,12 +219,13 @@ var OpenApiSpecPoller = class {
|
|
|
219
219
|
|
|
220
220
|
// libs/adapters/src/openapi/openapi.security.ts
|
|
221
221
|
var import_mcp_from_openapi2 = require("mcp-from-openapi");
|
|
222
|
+
var import_sdk2 = require("@frontmcp/sdk");
|
|
222
223
|
async function createSecurityContextFromAuth(tool2, ctx, options) {
|
|
223
224
|
if (options.securityResolver) {
|
|
224
225
|
return await options.securityResolver(tool2, ctx);
|
|
225
226
|
}
|
|
226
227
|
if (options.authProviderMapper) {
|
|
227
|
-
const context =
|
|
228
|
+
const context = {};
|
|
228
229
|
const securitySchemes = /* @__PURE__ */ new Set();
|
|
229
230
|
for (const mapper of tool2.mapper) {
|
|
230
231
|
if (mapper.security?.scheme) {
|
|
@@ -273,26 +274,42 @@ async function createSecurityContextFromAuth(tool2, ctx, options) {
|
|
|
273
274
|
throw err;
|
|
274
275
|
}
|
|
275
276
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
276
|
-
throw new Error(`authProviderMapper['${scheme}'] threw an error: ${errorMessage}
|
|
277
|
+
throw new Error(`authProviderMapper['${scheme}'] threw an error: ${errorMessage}`, { cause: err });
|
|
277
278
|
}
|
|
278
279
|
}
|
|
279
280
|
}
|
|
281
|
+
if (hasStaticAuth(options)) {
|
|
282
|
+
return (0, import_mcp_from_openapi2.createSecurityContext)({ ...options.staticAuth, ...context });
|
|
283
|
+
}
|
|
280
284
|
const hasAnyAuth = context.jwt || context.apiKey || context.basic || context.oauth2Token;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
+
if (!hasAnyAuth && options.passthroughCallerToken === true) {
|
|
286
|
+
const callerToken = getCallerToken(ctx);
|
|
287
|
+
if (callerToken) {
|
|
288
|
+
context.jwt = callerToken;
|
|
285
289
|
}
|
|
286
|
-
context.jwt = authToken;
|
|
287
290
|
}
|
|
288
|
-
return context;
|
|
291
|
+
return (0, import_mcp_from_openapi2.createSecurityContext)(context);
|
|
289
292
|
}
|
|
290
|
-
if (options
|
|
293
|
+
if (hasStaticAuth(options)) {
|
|
291
294
|
return (0, import_mcp_from_openapi2.createSecurityContext)(options.staticAuth);
|
|
292
295
|
}
|
|
293
|
-
|
|
294
|
-
jwt: ctx
|
|
295
|
-
}
|
|
296
|
+
if (options.passthroughCallerToken === true) {
|
|
297
|
+
return (0, import_mcp_from_openapi2.createSecurityContext)({ jwt: getCallerToken(ctx) });
|
|
298
|
+
}
|
|
299
|
+
return (0, import_mcp_from_openapi2.createSecurityContext)({});
|
|
300
|
+
}
|
|
301
|
+
function hasStaticAuth(options) {
|
|
302
|
+
return !!options.staticAuth && Object.keys(options.staticAuth).length > 0;
|
|
303
|
+
}
|
|
304
|
+
function getCallerToken(ctx) {
|
|
305
|
+
const authToken = ctx.authInfo?.token;
|
|
306
|
+
if (authToken === void 0 || authToken === null || authToken === "") {
|
|
307
|
+
return void 0;
|
|
308
|
+
}
|
|
309
|
+
if (typeof authToken !== "string") {
|
|
310
|
+
throw new Error(`authInfo.token must be a string, but got: ${typeof authToken}`);
|
|
311
|
+
}
|
|
312
|
+
return authToken;
|
|
296
313
|
}
|
|
297
314
|
function extractSecuritySchemes(tools) {
|
|
298
315
|
const schemes = /* @__PURE__ */ new Set();
|
|
@@ -305,19 +322,158 @@ function extractSecuritySchemes(tools) {
|
|
|
305
322
|
}
|
|
306
323
|
return schemes;
|
|
307
324
|
}
|
|
325
|
+
function describeSecuritySchemes(tools) {
|
|
326
|
+
const schemes = /* @__PURE__ */ new Map();
|
|
327
|
+
for (const tool2 of tools) {
|
|
328
|
+
for (const mapper of tool2.mapper) {
|
|
329
|
+
if (mapper.security?.scheme && !schemes.has(mapper.security.scheme)) {
|
|
330
|
+
schemes.set(mapper.security.scheme, mapper.security);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return schemes;
|
|
335
|
+
}
|
|
336
|
+
function schemeParametersOf(tools) {
|
|
337
|
+
const parameters = /* @__PURE__ */ new Map();
|
|
338
|
+
for (const tool2 of tools) {
|
|
339
|
+
for (const mapper of tool2.mapper) {
|
|
340
|
+
const scheme = mapper.security?.scheme;
|
|
341
|
+
if (scheme && !parameters.has(scheme)) parameters.set(scheme, mapper);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return parameters;
|
|
345
|
+
}
|
|
346
|
+
function headerCredentialSource(parameter, options) {
|
|
347
|
+
if (!parameter) return void 0;
|
|
348
|
+
const staticHeaders = new Headers(options.additionalHeaders ?? {});
|
|
349
|
+
if (carriesSchemeParameter(parameter, new URL("http://startup.invalid/"), staticHeaders)) return "static";
|
|
350
|
+
if (options.headersMapper && (parameter.type === "header" || parameter.type === "cookie")) return "per-request";
|
|
351
|
+
return void 0;
|
|
352
|
+
}
|
|
353
|
+
function isBearerScheme(security) {
|
|
354
|
+
if (!security || security.type !== "http") return false;
|
|
355
|
+
return (security.httpScheme ?? "bearer").toLowerCase() === "bearer";
|
|
356
|
+
}
|
|
357
|
+
function credentialFieldOf(security) {
|
|
358
|
+
if (security?.type === "apiKey") return "apiKey";
|
|
359
|
+
if (security?.type === "oauth2" || security?.type === "openIdConnect") return "oauth2Token";
|
|
360
|
+
if (security?.type === "http" && security.httpScheme?.toLowerCase() === "basic") return "basic";
|
|
361
|
+
return "jwt";
|
|
362
|
+
}
|
|
363
|
+
function formatMissingSecurityMappingsError(adapterName, tools, missingMappings) {
|
|
364
|
+
const schemes = describeSecuritySchemes(tools);
|
|
365
|
+
const fieldOf = (scheme) => credentialFieldOf(schemes.get(scheme));
|
|
366
|
+
const firstField = fieldOf(missingMappings[0] ?? "");
|
|
367
|
+
const bearerSchemes = missingMappings.filter((scheme) => isBearerScheme(schemes.get(scheme)));
|
|
368
|
+
const lines = [
|
|
369
|
+
`[OpenAPI Adapter: ${adapterName}] Invalid security configuration.`,
|
|
370
|
+
`Missing auth provider mappings for security schemes: ${missingMappings.join(", ")}`,
|
|
371
|
+
"",
|
|
372
|
+
"Your OpenAPI spec requires these security schemes, but no credential option covers them.",
|
|
373
|
+
"",
|
|
374
|
+
"Add one of the following to your adapter configuration:",
|
|
375
|
+
"",
|
|
376
|
+
"1. authProviderMapper (recommended): a function per scheme that returns the credential issued for the API:",
|
|
377
|
+
" authProviderMapper: {",
|
|
378
|
+
...missingMappings.map((scheme) => ` '${scheme}': (ctx) => getApiCredential(ctx), // the ${fieldOf(scheme)}`),
|
|
379
|
+
" }",
|
|
380
|
+
"",
|
|
381
|
+
"2. securityResolver:",
|
|
382
|
+
` securityResolver: async (tool, ctx) => ({ ${firstField}: await getApiCredential(ctx) })`,
|
|
383
|
+
"",
|
|
384
|
+
"3. staticAuth (one credential for every caller):",
|
|
385
|
+
` staticAuth: { ${firstField}: process.env.API_CREDENTIAL }`
|
|
386
|
+
];
|
|
387
|
+
if (bearerSchemes.length > 0) {
|
|
388
|
+
lines.push(
|
|
389
|
+
"",
|
|
390
|
+
`4. passthroughCallerToken: true, for the HTTP bearer schemes (${bearerSchemes.join(", ")}) only, and only if the API`,
|
|
391
|
+
" accepts the MCP client's own token (same issuer and audience). It sends nothing for other schemes."
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
return lines.join("\n");
|
|
395
|
+
}
|
|
396
|
+
function authenticationRequiredError(tool2) {
|
|
397
|
+
const required = requiredSecurityOf(tool2);
|
|
398
|
+
const schemes = [...new Map(required.map((security) => [security.scheme, security])).values()];
|
|
399
|
+
const schemesStr = schemes.map((security) => `${security.scheme} (${security.type})`).join(", ") || "unknown";
|
|
400
|
+
const first = schemes[0];
|
|
401
|
+
const firstScheme = first?.scheme ?? "BearerAuth";
|
|
402
|
+
const field = credentialFieldOf(first);
|
|
403
|
+
const bearerSchemes = schemes.filter(isBearerScheme).map((security) => security.scheme);
|
|
404
|
+
const solutions = [
|
|
405
|
+
` 1. Add authProviderMapper: { '${firstScheme}': (ctx) => getApiCredential(ctx) }, returning the ${field} issued for the API`,
|
|
406
|
+
` 2. Add securityResolver: async (tool, ctx) => ({ ${field}: await getApiCredential(ctx) })`,
|
|
407
|
+
` 3. Add staticAuth: { ${field}: process.env.API_CREDENTIAL }`
|
|
408
|
+
];
|
|
409
|
+
if (bearerSchemes.length > 0) {
|
|
410
|
+
solutions.push(
|
|
411
|
+
` 4. Set passthroughCallerToken: true, only if the API accepts the MCP client's own token (it fills ${bearerSchemes.join(", ")} only)`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
return new Error(
|
|
415
|
+
`Authentication required for tool '${tool2.name}': no credential for its security schemes.
|
|
416
|
+
Required security schemes: ${schemesStr}
|
|
417
|
+
Solutions:
|
|
418
|
+
` + solutions.join("\n")
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
function requiredSecurityOf(tool2) {
|
|
422
|
+
return tool2.mapper.flatMap((mapper) => mapper.security && mapper.required === true ? [mapper.security] : []);
|
|
423
|
+
}
|
|
424
|
+
function authorizationSchemeOf(security) {
|
|
425
|
+
if (security?.type === "http") return (security.httpScheme ?? "bearer").toLowerCase();
|
|
426
|
+
if (security?.type === "oauth2" || security?.type === "openIdConnect") return "bearer";
|
|
427
|
+
return void 0;
|
|
428
|
+
}
|
|
429
|
+
function carriesSchemeParameter(mapper, url, headers) {
|
|
430
|
+
if (mapper.type === "query") return !!url.searchParams.get(mapper.key);
|
|
431
|
+
if (mapper.type === "cookie") {
|
|
432
|
+
return (headers.get("cookie") ?? "").split(";").some((pair) => {
|
|
433
|
+
const [name, ...value2] = pair.trim().split("=");
|
|
434
|
+
return name === mapper.key && value2.join("=") !== "";
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
if (mapper.type !== "header") return false;
|
|
438
|
+
const value = headers.get(mapper.key)?.trim();
|
|
439
|
+
if (!value) return false;
|
|
440
|
+
const scheme = authorizationSchemeOf(mapper.security);
|
|
441
|
+
if (!scheme) return true;
|
|
442
|
+
const space = value.indexOf(" ");
|
|
443
|
+
return space > 0 && value.slice(0, space).toLowerCase() === scheme && value.slice(space + 1).trim() !== "";
|
|
444
|
+
}
|
|
445
|
+
function assertRequestHasCredential(tool2, url, headers) {
|
|
446
|
+
const securityMappers = tool2.mapper.filter((mapper) => mapper.security && mapper.required === true);
|
|
447
|
+
if (securityMappers.length === 0) return;
|
|
448
|
+
const parsedUrl = new URL(url);
|
|
449
|
+
if (!securityMappers.some((mapper) => carriesSchemeParameter(mapper, parsedUrl, headers))) {
|
|
450
|
+
throw authenticationRequiredError(tool2);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
308
453
|
function validateSecurityConfiguration(tools, options) {
|
|
454
|
+
const result = validateCredentialSources(tools, options);
|
|
455
|
+
const schemesInInput = options.securitySchemesInInput ?? [];
|
|
456
|
+
if (options.generateOptions?.includeSecurityInInput !== true && schemesInInput.length > 0) {
|
|
457
|
+
result.securityRiskScore = "high";
|
|
458
|
+
result.warnings.push(
|
|
459
|
+
`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.`
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
return result;
|
|
463
|
+
}
|
|
464
|
+
function validateCredentialSources(tools, options) {
|
|
309
465
|
const result = {
|
|
310
466
|
valid: true,
|
|
311
467
|
missingMappings: [],
|
|
312
468
|
warnings: [],
|
|
313
469
|
securityRiskScore: "low"
|
|
314
470
|
};
|
|
315
|
-
const securitySchemes =
|
|
471
|
+
const securitySchemes = describeSecuritySchemes(tools);
|
|
316
472
|
const includeSecurityInInput = options.generateOptions?.includeSecurityInInput ?? false;
|
|
317
473
|
if (includeSecurityInInput) {
|
|
318
474
|
result.securityRiskScore = "high";
|
|
319
475
|
result.warnings.push(
|
|
320
|
-
"SECURITY WARNING: includeSecurityInInput is enabled. Users will provide authentication directly in tool inputs. This increases security risk as credentials may be logged or exposed."
|
|
476
|
+
"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."
|
|
321
477
|
);
|
|
322
478
|
return result;
|
|
323
479
|
}
|
|
@@ -328,28 +484,35 @@ function validateSecurityConfiguration(tools, options) {
|
|
|
328
484
|
);
|
|
329
485
|
return result;
|
|
330
486
|
}
|
|
331
|
-
if (
|
|
487
|
+
if (hasStaticAuth(options)) {
|
|
332
488
|
result.securityRiskScore = "medium";
|
|
333
489
|
result.warnings.push(
|
|
334
490
|
"SECURITY INFO: Using staticAuth with hardcoded credentials. Ensure credentials are stored securely (environment variables, secrets manager)."
|
|
335
491
|
);
|
|
336
|
-
return result;
|
|
492
|
+
return withPassthroughRisk(result, securitySchemes, options);
|
|
337
493
|
}
|
|
338
494
|
const schemesInInput = new Set(options.securitySchemesInInput || []);
|
|
339
495
|
if (options.authProviderMapper || schemesInInput.size > 0) {
|
|
340
|
-
result.securityRiskScore =
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
for (const scheme of securitySchemes) {
|
|
496
|
+
result.securityRiskScore = "low";
|
|
497
|
+
const passthroughFallback = [];
|
|
498
|
+
const perRequestHeaders = [];
|
|
499
|
+
const schemeParameters = schemeParametersOf(tools);
|
|
500
|
+
for (const [scheme, security] of securitySchemes) {
|
|
347
501
|
if (schemesInInput.has(scheme)) {
|
|
348
502
|
continue;
|
|
349
503
|
}
|
|
350
504
|
if (!options.authProviderMapper?.[scheme]) {
|
|
351
|
-
|
|
352
|
-
|
|
505
|
+
const headerSource = headerCredentialSource(schemeParameters.get(scheme), options);
|
|
506
|
+
if (headerSource === "static") {
|
|
507
|
+
continue;
|
|
508
|
+
} else if (headerSource === "per-request") {
|
|
509
|
+
perRequestHeaders.push(scheme);
|
|
510
|
+
} else if (options.passthroughCallerToken === true && isBearerScheme(security)) {
|
|
511
|
+
passthroughFallback.push(scheme);
|
|
512
|
+
} else {
|
|
513
|
+
result.valid = false;
|
|
514
|
+
result.missingMappings.push(scheme);
|
|
515
|
+
}
|
|
353
516
|
}
|
|
354
517
|
}
|
|
355
518
|
if (!result.valid) {
|
|
@@ -357,47 +520,113 @@ function validateSecurityConfiguration(tools, options) {
|
|
|
357
520
|
`ERROR: Missing auth provider mappings for security schemes: ${result.missingMappings.join(", ")}`
|
|
358
521
|
);
|
|
359
522
|
}
|
|
523
|
+
if (perRequestHeaders.length > 0) {
|
|
524
|
+
result.warnings.push(
|
|
525
|
+
`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.`
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
withPassthroughRisk(result, securitySchemes, options);
|
|
529
|
+
if (passthroughFallback.length > 0) {
|
|
530
|
+
result.warnings.push(
|
|
531
|
+
`SECURITY WARNING: Security schemes with no authProviderMapper entry (${passthroughFallback.join(", ")}) get the MCP client's own token, because passthroughCallerToken is enabled.`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
360
534
|
return result;
|
|
361
535
|
}
|
|
362
|
-
if (securitySchemes.size > 0) {
|
|
536
|
+
if (securitySchemes.size > 0 && options.passthroughCallerToken !== true) {
|
|
537
|
+
const schemesStr = Array.from(securitySchemes.keys()).join(", ");
|
|
363
538
|
result.securityRiskScore = "medium";
|
|
364
539
|
result.warnings.push(
|
|
365
|
-
`
|
|
366
|
-
securitySchemes
|
|
367
|
-
).join(", ")}`
|
|
540
|
+
`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.`
|
|
368
541
|
);
|
|
542
|
+
}
|
|
543
|
+
const unfilled = [...securitySchemes].filter(([, security]) => !isBearerScheme(security)).map(([scheme]) => scheme);
|
|
544
|
+
if (options.passthroughCallerToken === true && unfilled.length > 0) {
|
|
545
|
+
result.securityRiskScore = "medium";
|
|
369
546
|
result.warnings.push(
|
|
370
|
-
|
|
547
|
+
`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.`
|
|
371
548
|
);
|
|
372
549
|
}
|
|
550
|
+
return withPassthroughRisk(result, securitySchemes, options);
|
|
551
|
+
}
|
|
552
|
+
function canPassThroughCallerToken(options) {
|
|
553
|
+
return options.passthroughCallerToken === true && !options.securityResolver && !hasStaticAuth(options);
|
|
554
|
+
}
|
|
555
|
+
function withPassthroughRisk(result, securitySchemes, options) {
|
|
556
|
+
const bearerSchemes = [...securitySchemes].filter(([, security]) => isBearerScheme(security));
|
|
557
|
+
if (bearerSchemes.length === 0 || !canPassThroughCallerToken(options)) {
|
|
558
|
+
return result;
|
|
559
|
+
}
|
|
560
|
+
const schemesStr = bearerSchemes.map(([scheme]) => scheme).join(", ");
|
|
561
|
+
const when = options.authProviderMapper ? " whenever no authProviderMapper function returns a credential" : "";
|
|
562
|
+
result.securityRiskScore = "high";
|
|
563
|
+
result.warnings.push(
|
|
564
|
+
`SECURITY WARNING: passthroughCallerToken is enabled. The MCP client's own token (ctx.authInfo.token) is sent to the API${when} for security schemes: ${schemesStr}. Only use this when the API accepts tokens issued for this MCP server.`
|
|
565
|
+
);
|
|
373
566
|
return result;
|
|
374
567
|
}
|
|
375
|
-
async function resolveToolSecurity(tool2, ctx, options) {
|
|
568
|
+
async function resolveToolSecurity(tool2, ctx, options, request = {}) {
|
|
376
569
|
const securityResolver = new import_mcp_from_openapi2.SecurityResolver();
|
|
377
570
|
const securityContext = await createSecurityContextFromAuth(tool2, ctx, options);
|
|
378
571
|
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;
|
|
379
572
|
const requiresSecurity = tool2.mapper.some((m) => m.security && m.required === true);
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const firstScheme = uniqueSchemes[0] || "BearerAuth";
|
|
385
|
-
throw new Error(
|
|
386
|
-
`Authentication required for tool '${tool2.name}' but no auth configuration found.
|
|
387
|
-
Required security schemes: ${schemesStr}
|
|
388
|
-
Solutions:
|
|
389
|
-
1. Add authProviderMapper: { '${firstScheme}': (ctx) => ctx.authInfo.user?.token }
|
|
390
|
-
2. Add securityResolver: (tool, ctx) => ({ jwt: ctx.authInfo.token })
|
|
391
|
-
3. Add staticAuth: { jwt: process.env.API_TOKEN }
|
|
392
|
-
4. Set generateOptions.includeSecurityInInput: true (not recommended for production)`
|
|
393
|
-
);
|
|
573
|
+
const fromInput = request.input ? await resolveInputSecurity(tool2, request.input, options, securityResolver) : void 0;
|
|
574
|
+
const hasInputCredential = !!fromInput && Object.keys(fromInput.headers).length + Object.keys(fromInput.query).length + Object.keys(fromInput.cookies).length > 0;
|
|
575
|
+
if (requiresSecurity && !hasAuth && !hasInputCredential && !request.deferCredentialCheck) {
|
|
576
|
+
throw authenticationRequiredError(tool2);
|
|
394
577
|
}
|
|
395
|
-
|
|
578
|
+
const resolved = await securityResolver.resolve(tool2.mapper, securityContext);
|
|
579
|
+
if (!fromInput) return resolved;
|
|
580
|
+
return {
|
|
581
|
+
...resolved,
|
|
582
|
+
headers: { ...fromInput.headers, ...resolved.headers },
|
|
583
|
+
query: { ...fromInput.query, ...resolved.query },
|
|
584
|
+
cookies: { ...fromInput.cookies, ...resolved.cookies }
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function isInputScheme(scheme, options) {
|
|
588
|
+
return options.generateOptions?.includeSecurityInInput === true || (options.securitySchemesInInput ?? []).includes(scheme);
|
|
589
|
+
}
|
|
590
|
+
function inputCredentialContext(security, value) {
|
|
591
|
+
const bare = value.replace(/^(?:bearer|basic)\s+/i, "");
|
|
592
|
+
if (security.type === "apiKey") {
|
|
593
|
+
return security.apiKeyName ? { apiKeys: { [security.apiKeyName]: value } } : { apiKey: value };
|
|
594
|
+
}
|
|
595
|
+
if (security.type === "oauth2" || security.type === "openIdConnect") return { oauth2Token: bare };
|
|
596
|
+
if (security.type === "http") {
|
|
597
|
+
const httpScheme = (security.httpScheme ?? "bearer").toLowerCase();
|
|
598
|
+
if (httpScheme === "bearer") return { jwt: bare };
|
|
599
|
+
if (httpScheme === "basic") return { basic: bare };
|
|
600
|
+
}
|
|
601
|
+
return void 0;
|
|
602
|
+
}
|
|
603
|
+
async function resolveInputSecurity(tool2, input, options, securityResolver) {
|
|
604
|
+
const resolved = { headers: {}, query: {}, cookies: {} };
|
|
605
|
+
for (const mapper of tool2.mapper) {
|
|
606
|
+
const security = mapper.security;
|
|
607
|
+
if (!security || !isInputScheme(security.scheme, options)) continue;
|
|
608
|
+
const value = input[mapper.inputKey];
|
|
609
|
+
if (typeof value !== "string" || value === "") continue;
|
|
610
|
+
if (/[\r\n\x00\f\v]/.test(value)) {
|
|
611
|
+
throw new import_sdk2.PublicMcpError(
|
|
612
|
+
`Invalid value for '${mapper.inputKey}': contains control characters (possible header injection attack)`,
|
|
613
|
+
"INVALID_HEADER_VALUE",
|
|
614
|
+
400
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
const context = inputCredentialContext(security, value);
|
|
618
|
+
if (!context) continue;
|
|
619
|
+
const one = await securityResolver.resolve([mapper], (0, import_mcp_from_openapi2.createSecurityContext)(context));
|
|
620
|
+
Object.assign(resolved.headers, one.headers);
|
|
621
|
+
Object.assign(resolved.query, one.query);
|
|
622
|
+
Object.assign(resolved.cookies, one.cookies);
|
|
623
|
+
}
|
|
624
|
+
return resolved;
|
|
396
625
|
}
|
|
397
626
|
|
|
398
627
|
// libs/adapters/src/openapi/openapi.tool.ts
|
|
399
628
|
var import_lazy_zod2 = require("@frontmcp/lazy-zod");
|
|
400
|
-
var
|
|
629
|
+
var import_sdk4 = require("@frontmcp/sdk");
|
|
401
630
|
var import_utils3 = require("@frontmcp/utils");
|
|
402
631
|
|
|
403
632
|
// libs/adapters/src/openapi/openapi.frontmcp-schema.ts
|
|
@@ -641,7 +870,7 @@ function formatZodIssues(issues, prefix) {
|
|
|
641
870
|
}
|
|
642
871
|
|
|
643
872
|
// libs/adapters/src/openapi/openapi.utils.ts
|
|
644
|
-
var
|
|
873
|
+
var import_sdk3 = require("@frontmcp/sdk");
|
|
645
874
|
var import_utils2 = require("@frontmcp/utils");
|
|
646
875
|
function coerceToString(value, paramName, location) {
|
|
647
876
|
if (value === null || value === void 0) {
|
|
@@ -699,7 +928,7 @@ function buildRequest(tool2, input, security, baseUrl) {
|
|
|
699
928
|
case "header": {
|
|
700
929
|
const headerValue = coerceToString(value, mapper.key, "header");
|
|
701
930
|
if (/[\r\n\x00\f\v]/.test(headerValue)) {
|
|
702
|
-
throw new
|
|
931
|
+
throw new import_sdk3.PublicMcpError(
|
|
703
932
|
`Invalid header value for '${mapper.key}': contains control characters (possible header injection attack)`,
|
|
704
933
|
"INVALID_HEADER_VALUE",
|
|
705
934
|
400
|
|
@@ -864,7 +1093,7 @@ function createOpenApiTool(openapiTool, options, logger) {
|
|
|
864
1093
|
if (toolTransform.ui) {
|
|
865
1094
|
toolMetadata["ui"] = toolTransform.ui;
|
|
866
1095
|
}
|
|
867
|
-
return (0,
|
|
1096
|
+
return (0, import_sdk4.tool)(toolMetadata)(async (input, toolCtx) => {
|
|
868
1097
|
const ctx = toolCtx.context;
|
|
869
1098
|
const transformContext = {
|
|
870
1099
|
ctx,
|
|
@@ -876,7 +1105,10 @@ function createOpenApiTool(openapiTool, options, logger) {
|
|
|
876
1105
|
inputTransforms,
|
|
877
1106
|
transformContext
|
|
878
1107
|
);
|
|
879
|
-
const security = await resolveToolSecurity(openapiTool, ctx, options
|
|
1108
|
+
const security = await resolveToolSecurity(openapiTool, ctx, options, {
|
|
1109
|
+
input: injectedInput,
|
|
1110
|
+
deferCredentialCheck: true
|
|
1111
|
+
});
|
|
880
1112
|
const { url, headers, body: requestBody } = buildRequest(openapiTool, injectedInput, security, options.baseUrl);
|
|
881
1113
|
applyAdditionalHeaders(headers, options.additionalHeaders);
|
|
882
1114
|
if (options.headersMapper) {
|
|
@@ -892,6 +1124,7 @@ function createOpenApiTool(openapiTool, options, logger) {
|
|
|
892
1124
|
throw new Error(`headersMapper failed for tool '${openapiTool.name}': ${errorMessage}`, { cause: err });
|
|
893
1125
|
}
|
|
894
1126
|
}
|
|
1127
|
+
assertRequestHasCredential(openapiTool, url, headers);
|
|
895
1128
|
let finalBody = requestBody;
|
|
896
1129
|
if (options.bodyMapper && requestBody) {
|
|
897
1130
|
try {
|
|
@@ -1113,7 +1346,7 @@ function createConsoleLogger(prefix) {
|
|
|
1113
1346
|
child: (childPrefix) => createConsoleLogger(`${prefix}:${childPrefix}`)
|
|
1114
1347
|
};
|
|
1115
1348
|
}
|
|
1116
|
-
var OpenapiAdapter = class extends
|
|
1349
|
+
var OpenapiAdapter = class extends import_sdk5.DynamicAdapter {
|
|
1117
1350
|
generator;
|
|
1118
1351
|
logger;
|
|
1119
1352
|
options;
|
|
@@ -1176,28 +1409,7 @@ var OpenapiAdapter = class extends import_sdk4.DynamicAdapter {
|
|
|
1176
1409
|
});
|
|
1177
1410
|
}
|
|
1178
1411
|
if (!validation.valid) {
|
|
1179
|
-
throw new Error(
|
|
1180
|
-
`[OpenAPI Adapter: ${this.options.name}] Invalid security configuration.
|
|
1181
|
-
Missing auth provider mappings for security schemes: ${validation.missingMappings.join(", ")}
|
|
1182
|
-
|
|
1183
|
-
Your OpenAPI spec requires these security schemes, but no auth configuration was provided.
|
|
1184
|
-
|
|
1185
|
-
Add one of the following to your adapter configuration:
|
|
1186
|
-
|
|
1187
|
-
1. authProviderMapper (recommended):
|
|
1188
|
-
authProviderMapper: {
|
|
1189
|
-
` + validation.missingMappings.map((s) => ` '${s}': (authInfo) => authInfo.user?.${s.toLowerCase()}Token,`).join("\n") + `
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
2. securityResolver:
|
|
1193
|
-
securityResolver: (tool, authInfo) => ({ jwt: authInfo.token })
|
|
1194
|
-
|
|
1195
|
-
3. staticAuth:
|
|
1196
|
-
staticAuth: { jwt: process.env.API_TOKEN }
|
|
1197
|
-
|
|
1198
|
-
4. Include security in input (NOT recommended for production):
|
|
1199
|
-
generateOptions: { includeSecurityInInput: true }`
|
|
1200
|
-
);
|
|
1412
|
+
throw new Error(formatMissingSecurityMappingsError(this.options.name, openapiTools, validation.missingMappings));
|
|
1201
1413
|
}
|
|
1202
1414
|
let transformedTools = openapiTools;
|
|
1203
1415
|
if (this.options.descriptionMode && this.options.descriptionMode !== "summaryOnly") {
|
|
@@ -1851,7 +2063,7 @@ ${this.formatSchemaAsSummary(schema)}`;
|
|
|
1851
2063
|
}
|
|
1852
2064
|
};
|
|
1853
2065
|
OpenapiAdapter = __decorateClass([
|
|
1854
|
-
(0,
|
|
2066
|
+
(0, import_sdk5.Adapter)({
|
|
1855
2067
|
name: "openapi",
|
|
1856
2068
|
description: "OpenAPI adapter for FrontMCP - Automatically generates MCP tools from OpenAPI specifications"
|
|
1857
2069
|
})
|