@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/esm/index.mjs
CHANGED
|
@@ -183,13 +183,17 @@ 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);
|
|
190
194
|
}
|
|
191
195
|
if (options.authProviderMapper) {
|
|
192
|
-
const context =
|
|
196
|
+
const context = {};
|
|
193
197
|
const securitySchemes = /* @__PURE__ */ new Set();
|
|
194
198
|
for (const mapper of tool2.mapper) {
|
|
195
199
|
if (mapper.security?.scheme) {
|
|
@@ -238,26 +242,42 @@ 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
|
}
|
|
249
|
+
if (hasStaticAuth(options)) {
|
|
250
|
+
return createSecurityContext({ ...options.staticAuth, ...context });
|
|
251
|
+
}
|
|
245
252
|
const hasAnyAuth = context.jwt || context.apiKey || context.basic || context.oauth2Token;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (
|
|
249
|
-
|
|
253
|
+
if (!hasAnyAuth && options.passthroughCallerToken === true) {
|
|
254
|
+
const callerToken = getCallerToken(ctx);
|
|
255
|
+
if (callerToken) {
|
|
256
|
+
context.jwt = callerToken;
|
|
250
257
|
}
|
|
251
|
-
context.jwt = authToken;
|
|
252
258
|
}
|
|
253
|
-
return context;
|
|
259
|
+
return createSecurityContext(context);
|
|
254
260
|
}
|
|
255
|
-
if (options
|
|
261
|
+
if (hasStaticAuth(options)) {
|
|
256
262
|
return createSecurityContext(options.staticAuth);
|
|
257
263
|
}
|
|
258
|
-
|
|
259
|
-
jwt: ctx
|
|
260
|
-
}
|
|
264
|
+
if (options.passthroughCallerToken === true) {
|
|
265
|
+
return createSecurityContext({ jwt: getCallerToken(ctx) });
|
|
266
|
+
}
|
|
267
|
+
return createSecurityContext({});
|
|
268
|
+
}
|
|
269
|
+
function hasStaticAuth(options) {
|
|
270
|
+
return !!options.staticAuth && Object.keys(options.staticAuth).length > 0;
|
|
271
|
+
}
|
|
272
|
+
function getCallerToken(ctx) {
|
|
273
|
+
const authToken = ctx.authInfo?.token;
|
|
274
|
+
if (authToken === void 0 || authToken === null || authToken === "") {
|
|
275
|
+
return void 0;
|
|
276
|
+
}
|
|
277
|
+
if (typeof authToken !== "string") {
|
|
278
|
+
throw new Error(`authInfo.token must be a string, but got: ${typeof authToken}`);
|
|
279
|
+
}
|
|
280
|
+
return authToken;
|
|
261
281
|
}
|
|
262
282
|
function extractSecuritySchemes(tools) {
|
|
263
283
|
const schemes = /* @__PURE__ */ new Set();
|
|
@@ -270,19 +290,158 @@ function extractSecuritySchemes(tools) {
|
|
|
270
290
|
}
|
|
271
291
|
return schemes;
|
|
272
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
|
+
}
|
|
273
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) {
|
|
274
433
|
const result = {
|
|
275
434
|
valid: true,
|
|
276
435
|
missingMappings: [],
|
|
277
436
|
warnings: [],
|
|
278
437
|
securityRiskScore: "low"
|
|
279
438
|
};
|
|
280
|
-
const securitySchemes =
|
|
439
|
+
const securitySchemes = describeSecuritySchemes(tools);
|
|
281
440
|
const includeSecurityInInput = options.generateOptions?.includeSecurityInInput ?? false;
|
|
282
441
|
if (includeSecurityInInput) {
|
|
283
442
|
result.securityRiskScore = "high";
|
|
284
443
|
result.warnings.push(
|
|
285
|
-
"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."
|
|
286
445
|
);
|
|
287
446
|
return result;
|
|
288
447
|
}
|
|
@@ -293,28 +452,35 @@ function validateSecurityConfiguration(tools, options) {
|
|
|
293
452
|
);
|
|
294
453
|
return result;
|
|
295
454
|
}
|
|
296
|
-
if (
|
|
455
|
+
if (hasStaticAuth(options)) {
|
|
297
456
|
result.securityRiskScore = "medium";
|
|
298
457
|
result.warnings.push(
|
|
299
458
|
"SECURITY INFO: Using staticAuth with hardcoded credentials. Ensure credentials are stored securely (environment variables, secrets manager)."
|
|
300
459
|
);
|
|
301
|
-
return result;
|
|
460
|
+
return withPassthroughRisk(result, securitySchemes, options);
|
|
302
461
|
}
|
|
303
462
|
const schemesInInput = new Set(options.securitySchemesInInput || []);
|
|
304
463
|
if (options.authProviderMapper || schemesInInput.size > 0) {
|
|
305
|
-
result.securityRiskScore =
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
for (const scheme of securitySchemes) {
|
|
464
|
+
result.securityRiskScore = "low";
|
|
465
|
+
const passthroughFallback = [];
|
|
466
|
+
const perRequestHeaders = [];
|
|
467
|
+
const schemeParameters = schemeParametersOf(tools);
|
|
468
|
+
for (const [scheme, security] of securitySchemes) {
|
|
312
469
|
if (schemesInInput.has(scheme)) {
|
|
313
470
|
continue;
|
|
314
471
|
}
|
|
315
472
|
if (!options.authProviderMapper?.[scheme]) {
|
|
316
|
-
|
|
317
|
-
|
|
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)) {
|
|
479
|
+
passthroughFallback.push(scheme);
|
|
480
|
+
} else {
|
|
481
|
+
result.valid = false;
|
|
482
|
+
result.missingMappings.push(scheme);
|
|
483
|
+
}
|
|
318
484
|
}
|
|
319
485
|
}
|
|
320
486
|
if (!result.valid) {
|
|
@@ -322,42 +488,108 @@ function validateSecurityConfiguration(tools, options) {
|
|
|
322
488
|
`ERROR: Missing auth provider mappings for security schemes: ${result.missingMappings.join(", ")}`
|
|
323
489
|
);
|
|
324
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
|
+
}
|
|
496
|
+
withPassthroughRisk(result, securitySchemes, options);
|
|
497
|
+
if (passthroughFallback.length > 0) {
|
|
498
|
+
result.warnings.push(
|
|
499
|
+
`SECURITY WARNING: Security schemes with no authProviderMapper entry (${passthroughFallback.join(", ")}) get the MCP client's own token, because passthroughCallerToken is enabled.`
|
|
500
|
+
);
|
|
501
|
+
}
|
|
325
502
|
return result;
|
|
326
503
|
}
|
|
327
|
-
if (securitySchemes.size > 0) {
|
|
504
|
+
if (securitySchemes.size > 0 && options.passthroughCallerToken !== true) {
|
|
505
|
+
const schemesStr = Array.from(securitySchemes.keys()).join(", ");
|
|
328
506
|
result.securityRiskScore = "medium";
|
|
329
507
|
result.warnings.push(
|
|
330
|
-
`
|
|
331
|
-
securitySchemes
|
|
332
|
-
).join(", ")}`
|
|
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.`
|
|
333
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";
|
|
334
514
|
result.warnings.push(
|
|
335
|
-
|
|
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.`
|
|
336
516
|
);
|
|
337
517
|
}
|
|
518
|
+
return withPassthroughRisk(result, securitySchemes, options);
|
|
519
|
+
}
|
|
520
|
+
function canPassThroughCallerToken(options) {
|
|
521
|
+
return options.passthroughCallerToken === true && !options.securityResolver && !hasStaticAuth(options);
|
|
522
|
+
}
|
|
523
|
+
function withPassthroughRisk(result, securitySchemes, options) {
|
|
524
|
+
const bearerSchemes = [...securitySchemes].filter(([, security]) => isBearerScheme(security));
|
|
525
|
+
if (bearerSchemes.length === 0 || !canPassThroughCallerToken(options)) {
|
|
526
|
+
return result;
|
|
527
|
+
}
|
|
528
|
+
const schemesStr = bearerSchemes.map(([scheme]) => scheme).join(", ");
|
|
529
|
+
const when = options.authProviderMapper ? " whenever no authProviderMapper function returns a credential" : "";
|
|
530
|
+
result.securityRiskScore = "high";
|
|
531
|
+
result.warnings.push(
|
|
532
|
+
`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.`
|
|
533
|
+
);
|
|
338
534
|
return result;
|
|
339
535
|
}
|
|
340
|
-
async function resolveToolSecurity(tool2, ctx, options) {
|
|
536
|
+
async function resolveToolSecurity(tool2, ctx, options, request = {}) {
|
|
341
537
|
const securityResolver = new SecurityResolver();
|
|
342
538
|
const securityContext = await createSecurityContextFromAuth(tool2, ctx, options);
|
|
343
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;
|
|
344
540
|
const requiresSecurity = tool2.mapper.some((m) => m.security && m.required === true);
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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);
|
|
545
|
+
}
|
|
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);
|
|
359
591
|
}
|
|
360
|
-
return
|
|
592
|
+
return resolved;
|
|
361
593
|
}
|
|
362
594
|
|
|
363
595
|
// libs/adapters/src/openapi/openapi.tool.ts
|
|
@@ -606,7 +838,7 @@ function formatZodIssues(issues, prefix) {
|
|
|
606
838
|
}
|
|
607
839
|
|
|
608
840
|
// libs/adapters/src/openapi/openapi.utils.ts
|
|
609
|
-
import { PublicMcpError } from "@frontmcp/sdk";
|
|
841
|
+
import { PublicMcpError as PublicMcpError2 } from "@frontmcp/sdk";
|
|
610
842
|
import { trimTrailing, validateBaseUrl } from "@frontmcp/utils";
|
|
611
843
|
function coerceToString(value, paramName, location) {
|
|
612
844
|
if (value === null || value === void 0) {
|
|
@@ -664,7 +896,7 @@ function buildRequest(tool2, input, security, baseUrl) {
|
|
|
664
896
|
case "header": {
|
|
665
897
|
const headerValue = coerceToString(value, mapper.key, "header");
|
|
666
898
|
if (/[\r\n\x00\f\v]/.test(headerValue)) {
|
|
667
|
-
throw new
|
|
899
|
+
throw new PublicMcpError2(
|
|
668
900
|
`Invalid header value for '${mapper.key}': contains control characters (possible header injection attack)`,
|
|
669
901
|
"INVALID_HEADER_VALUE",
|
|
670
902
|
400
|
|
@@ -841,7 +1073,10 @@ function createOpenApiTool(openapiTool, options, logger) {
|
|
|
841
1073
|
inputTransforms,
|
|
842
1074
|
transformContext
|
|
843
1075
|
);
|
|
844
|
-
const security = await resolveToolSecurity(openapiTool, ctx, options
|
|
1076
|
+
const security = await resolveToolSecurity(openapiTool, ctx, options, {
|
|
1077
|
+
input: injectedInput,
|
|
1078
|
+
deferCredentialCheck: true
|
|
1079
|
+
});
|
|
845
1080
|
const { url, headers, body: requestBody } = buildRequest(openapiTool, injectedInput, security, options.baseUrl);
|
|
846
1081
|
applyAdditionalHeaders(headers, options.additionalHeaders);
|
|
847
1082
|
if (options.headersMapper) {
|
|
@@ -857,6 +1092,7 @@ function createOpenApiTool(openapiTool, options, logger) {
|
|
|
857
1092
|
throw new Error(`headersMapper failed for tool '${openapiTool.name}': ${errorMessage}`, { cause: err });
|
|
858
1093
|
}
|
|
859
1094
|
}
|
|
1095
|
+
assertRequestHasCredential(openapiTool, url, headers);
|
|
860
1096
|
let finalBody = requestBody;
|
|
861
1097
|
if (options.bodyMapper && requestBody) {
|
|
862
1098
|
try {
|
|
@@ -1141,28 +1377,7 @@ var OpenapiAdapter = class extends DynamicAdapter {
|
|
|
1141
1377
|
});
|
|
1142
1378
|
}
|
|
1143
1379
|
if (!validation.valid) {
|
|
1144
|
-
throw new Error(
|
|
1145
|
-
`[OpenAPI Adapter: ${this.options.name}] Invalid security configuration.
|
|
1146
|
-
Missing auth provider mappings for security schemes: ${validation.missingMappings.join(", ")}
|
|
1147
|
-
|
|
1148
|
-
Your OpenAPI spec requires these security schemes, but no auth configuration was provided.
|
|
1149
|
-
|
|
1150
|
-
Add one of the following to your adapter configuration:
|
|
1151
|
-
|
|
1152
|
-
1. authProviderMapper (recommended):
|
|
1153
|
-
authProviderMapper: {
|
|
1154
|
-
` + validation.missingMappings.map((s) => ` '${s}': (authInfo) => authInfo.user?.${s.toLowerCase()}Token,`).join("\n") + `
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
2. securityResolver:
|
|
1158
|
-
securityResolver: (tool, authInfo) => ({ jwt: authInfo.token })
|
|
1159
|
-
|
|
1160
|
-
3. staticAuth:
|
|
1161
|
-
staticAuth: { jwt: process.env.API_TOKEN }
|
|
1162
|
-
|
|
1163
|
-
4. Include security in input (NOT recommended for production):
|
|
1164
|
-
generateOptions: { includeSecurityInInput: true }`
|
|
1165
|
-
);
|
|
1380
|
+
throw new Error(formatMissingSecurityMappingsError(this.options.name, openapiTools, validation.missingMappings));
|
|
1166
1381
|
}
|
|
1167
1382
|
let transformedTools = openapiTools;
|
|
1168
1383
|
if (this.options.descriptionMode && this.options.descriptionMode !== "summaryOnly") {
|