@frontmcp/adapters 1.9.1 → 1.9.3
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 +136 -30
- package/esm/openapi/index.mjs +136 -30
- package/esm/package.json +6 -6
- package/esm/skills/index.mjs +5 -1
- package/index.js +131 -28
- package/openapi/index.js +131 -28
- package/openapi/openapi.adapter.d.ts +6 -0
- package/openapi/openapi.adapter.d.ts.map +1 -1
- package/openapi/openapi.frontmcp-schema.d.ts.map +1 -1
- package/openapi/openapi.tool.d.ts.map +1 -1
- package/openapi/openapi.types.d.ts +9 -2
- package/openapi/openapi.types.d.ts.map +1 -1
- package/openapi/openapi.utils.d.ts.map +1 -1
- package/package.json +6 -6
- package/skills/index.js +5 -1
- package/skills/sources/saas-pull.source.d.ts +2 -0
- package/skills/sources/saas-pull.source.d.ts.map +1 -1
package/esm/index.mjs
CHANGED
|
@@ -10,7 +10,10 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// libs/adapters/src/openapi/openapi.adapter.ts
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
normalizeSsrfOptions as normalizeSsrfOptions2,
|
|
15
|
+
OpenAPIToolGenerator
|
|
16
|
+
} from "mcp-from-openapi";
|
|
14
17
|
import { Adapter, DynamicAdapter } from "@frontmcp/sdk";
|
|
15
18
|
|
|
16
19
|
// libs/adapters/src/openapi/openapi-spec-poller.ts
|
|
@@ -605,7 +608,7 @@ async function resolveInputSecurity(tool2, input, options, securityResolver) {
|
|
|
605
608
|
|
|
606
609
|
// libs/adapters/src/openapi/openapi.tool.ts
|
|
607
610
|
import { z as z2 } from "@frontmcp/lazy-zod";
|
|
608
|
-
import { tool } from "@frontmcp/sdk";
|
|
611
|
+
import { InvalidInputError, tool } from "@frontmcp/sdk";
|
|
609
612
|
import { isRedirectResponse } from "@frontmcp/utils";
|
|
610
613
|
|
|
611
614
|
// libs/adapters/src/openapi/openapi.frontmcp-schema.ts
|
|
@@ -651,7 +654,9 @@ var KNOWN_EXTENSION_FIELDS = /* @__PURE__ */ new Set([
|
|
|
651
654
|
"codecall",
|
|
652
655
|
"tags",
|
|
653
656
|
"hideFromDiscovery",
|
|
654
|
-
"examples"
|
|
657
|
+
"examples",
|
|
658
|
+
"meta",
|
|
659
|
+
"icons"
|
|
655
660
|
]);
|
|
656
661
|
var KNOWN_ANNOTATION_FIELDS = /* @__PURE__ */ new Set([
|
|
657
662
|
"title",
|
|
@@ -875,6 +880,10 @@ function appendCookie(headers, name, value) {
|
|
|
875
880
|
const combined = existing ? `${existing}; ${cookiePair}` : cookiePair;
|
|
876
881
|
headers.set("Cookie", combined);
|
|
877
882
|
}
|
|
883
|
+
function specDefault(tool2, inputKey) {
|
|
884
|
+
const property = tool2.inputSchema.properties?.[inputKey];
|
|
885
|
+
return typeof property === "object" ? property.default : void 0;
|
|
886
|
+
}
|
|
878
887
|
function buildRequest(tool2, input, security, baseUrl) {
|
|
879
888
|
const rawBaseUrl = tool2.metadata.servers?.[0]?.url || baseUrl;
|
|
880
889
|
validateBaseUrl(rawBaseUrl);
|
|
@@ -888,7 +897,7 @@ function buildRequest(tool2, input, security, baseUrl) {
|
|
|
888
897
|
let body;
|
|
889
898
|
for (const mapper of tool2.mapper) {
|
|
890
899
|
if (mapper.security) continue;
|
|
891
|
-
const value = input[mapper.inputKey];
|
|
900
|
+
const value = input[mapper.inputKey] ?? (mapper.required ? specDefault(tool2, mapper.inputKey) : void 0);
|
|
892
901
|
if (value === void 0 || value === null) {
|
|
893
902
|
if (mapper.required) {
|
|
894
903
|
throw new Error(
|
|
@@ -1002,9 +1011,11 @@ function createOpenApiTool(openapiTool, options, logger) {
|
|
|
1002
1011
|
const inputTransforms = metadata.adapter?.inputTransforms ?? [];
|
|
1003
1012
|
const toolTransform = metadata.adapter?.toolTransform ?? {};
|
|
1004
1013
|
const postToolTransform = metadata.adapter?.postToolTransform;
|
|
1014
|
+
const serverFilledKeys = new Set(inputTransforms.map((transform) => transform.inputKey));
|
|
1005
1015
|
const frontmcpValidation = validateFrontMcpExtension(metadata.frontmcp, openapiTool.name, logger);
|
|
1006
1016
|
const frontmcpExt = frontmcpValidation.data;
|
|
1007
1017
|
const schemaResult = getZodSchemaFromJsonSchema(openapiTool.inputSchema, openapiTool.name, logger);
|
|
1018
|
+
const argumentCheck = schemaResult.conversionFailed ? void 0 : getZodSchemaFromJsonSchema(toArgumentJsonSchema(openapiTool), openapiTool.name, logger);
|
|
1008
1019
|
let wrappedOutputSchema;
|
|
1009
1020
|
if (openapiTool.outputSchema !== void 0) {
|
|
1010
1021
|
const baseOutputSchema = openapiTool.outputSchema ?? { type: "string" };
|
|
@@ -1022,11 +1033,15 @@ function createOpenApiTool(openapiTool, options, logger) {
|
|
|
1022
1033
|
const toolMetadata = {
|
|
1023
1034
|
id: openapiTool.name,
|
|
1024
1035
|
name: openapiTool.name,
|
|
1036
|
+
...openapiTool.title && { title: openapiTool.title },
|
|
1025
1037
|
description: openapiTool.description,
|
|
1026
1038
|
inputSchema: schemaResult.schema.shape || {},
|
|
1027
1039
|
rawInputSchema: openapiTool.inputSchema,
|
|
1028
1040
|
// Add output schema for tool/list to expose (only if not moved to description)
|
|
1029
|
-
...wrappedOutputSchema && { rawOutputSchema: wrappedOutputSchema }
|
|
1041
|
+
...wrappedOutputSchema && { rawOutputSchema: wrappedOutputSchema },
|
|
1042
|
+
...openapiTool.annotations && { annotations: { ...openapiTool.annotations } },
|
|
1043
|
+
...openapiTool.icons?.length && { icons: openapiTool.icons.map((icon) => ({ ...icon })) },
|
|
1044
|
+
...openapiTool._meta && { _meta: { ...openapiTool._meta } }
|
|
1030
1045
|
};
|
|
1031
1046
|
if (schemaResult.conversionFailed) {
|
|
1032
1047
|
toolMetadata["_schemaConversionFailed"] = true;
|
|
@@ -1034,7 +1049,10 @@ function createOpenApiTool(openapiTool, options, logger) {
|
|
|
1034
1049
|
}
|
|
1035
1050
|
if (frontmcpExt) {
|
|
1036
1051
|
if (frontmcpExt.annotations) {
|
|
1037
|
-
toolMetadata["annotations"] = {
|
|
1052
|
+
toolMetadata["annotations"] = {
|
|
1053
|
+
...toolMetadata["annotations"] || {},
|
|
1054
|
+
...frontmcpExt.annotations
|
|
1055
|
+
};
|
|
1038
1056
|
}
|
|
1039
1057
|
if (frontmcpExt.tags) {
|
|
1040
1058
|
toolMetadata["tags"] = [...frontmcpExt.tags];
|
|
@@ -1074,16 +1092,20 @@ function createOpenApiTool(openapiTool, options, logger) {
|
|
|
1074
1092
|
}
|
|
1075
1093
|
return tool(toolMetadata)(async (input, toolCtx) => {
|
|
1076
1094
|
const ctx = toolCtx.context;
|
|
1095
|
+
const modelInput = omitKeys(input, serverFilledKeys);
|
|
1096
|
+
if (argumentCheck && !argumentCheck.conversionFailed) {
|
|
1097
|
+
const validation = argumentCheck.schema.safeParse(modelInput);
|
|
1098
|
+
if (!validation.success) {
|
|
1099
|
+
const problems = validation.error.issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`);
|
|
1100
|
+
throw new InvalidInputError(`Invalid arguments for tool '${openapiTool.name}': ${problems.join("; ")}`);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1077
1103
|
const transformContext = {
|
|
1078
1104
|
ctx,
|
|
1079
1105
|
env: process.env,
|
|
1080
1106
|
tool: openapiTool
|
|
1081
1107
|
};
|
|
1082
|
-
const injectedInput = await injectTransformedValues(
|
|
1083
|
-
input,
|
|
1084
|
-
inputTransforms,
|
|
1085
|
-
transformContext
|
|
1086
|
-
);
|
|
1108
|
+
const injectedInput = await injectTransformedValues(modelInput, inputTransforms, transformContext);
|
|
1087
1109
|
const security = await resolveToolSecurity(openapiTool, ctx, options, {
|
|
1088
1110
|
input: injectedInput,
|
|
1089
1111
|
deferCredentialCheck: true
|
|
@@ -1268,6 +1290,10 @@ async function safeInject(transform, ctx, timeoutMs = DEFAULT_TRANSFORM_TIMEOUT_
|
|
|
1268
1290
|
function isPlainObject(value) {
|
|
1269
1291
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1270
1292
|
}
|
|
1293
|
+
function omitKeys(input, keys) {
|
|
1294
|
+
if (keys.size === 0 || !isPlainObject(input)) return input;
|
|
1295
|
+
return Object.fromEntries(Object.entries(input).filter(([key]) => !keys.has(key)));
|
|
1296
|
+
}
|
|
1271
1297
|
async function injectTransformedValues(input, transforms, ctx) {
|
|
1272
1298
|
if (!isPlainObject(input)) {
|
|
1273
1299
|
throw new Error(`Invalid input type: expected object, got ${input === null ? "null" : typeof input}`);
|
|
@@ -1287,6 +1313,64 @@ async function injectTransformedValues(input, transforms, ctx) {
|
|
|
1287
1313
|
}
|
|
1288
1314
|
return result;
|
|
1289
1315
|
}
|
|
1316
|
+
var SUBSCHEMA_KEYWORDS = [
|
|
1317
|
+
"items",
|
|
1318
|
+
"additionalProperties",
|
|
1319
|
+
"not",
|
|
1320
|
+
"if",
|
|
1321
|
+
"then",
|
|
1322
|
+
"else",
|
|
1323
|
+
"contains",
|
|
1324
|
+
"propertyNames",
|
|
1325
|
+
"unevaluatedItems",
|
|
1326
|
+
"unevaluatedProperties"
|
|
1327
|
+
];
|
|
1328
|
+
var SUBSCHEMA_LIST_KEYWORDS = ["allOf", "anyOf", "prefixItems"];
|
|
1329
|
+
var SUBSCHEMA_MAP_KEYWORDS = ["properties", "patternProperties", "dependentSchemas", "$defs", "definitions"];
|
|
1330
|
+
function toArgumentJsonSchema(openapiTool) {
|
|
1331
|
+
const argumentSchema = relaxSchema(openapiTool.inputSchema);
|
|
1332
|
+
const credentialKeys = new Set(
|
|
1333
|
+
openapiTool.mapper.filter((mapper) => mapper.security).map((mapper) => mapper.inputKey)
|
|
1334
|
+
);
|
|
1335
|
+
const required = argumentSchema["required"];
|
|
1336
|
+
if (Array.isArray(required)) {
|
|
1337
|
+
argumentSchema["required"] = required.filter((key) => !credentialKeys.has(key));
|
|
1338
|
+
}
|
|
1339
|
+
return argumentSchema;
|
|
1340
|
+
}
|
|
1341
|
+
function relaxSchema(schema) {
|
|
1342
|
+
if (Array.isArray(schema)) return schema.map(relaxSchema);
|
|
1343
|
+
if (typeof schema !== "object" || schema === null) return schema;
|
|
1344
|
+
const relaxed = { ...schema };
|
|
1345
|
+
delete relaxed["format"];
|
|
1346
|
+
for (const keyword of SUBSCHEMA_KEYWORDS) {
|
|
1347
|
+
if (keyword in relaxed) relaxed[keyword] = relaxSchema(relaxed[keyword]);
|
|
1348
|
+
}
|
|
1349
|
+
for (const keyword of SUBSCHEMA_LIST_KEYWORDS) {
|
|
1350
|
+
const subschemas = relaxed[keyword];
|
|
1351
|
+
if (Array.isArray(subschemas)) relaxed[keyword] = subschemas.map(relaxSchema);
|
|
1352
|
+
}
|
|
1353
|
+
for (const keyword of SUBSCHEMA_MAP_KEYWORDS) {
|
|
1354
|
+
const subschemasByName = relaxed[keyword];
|
|
1355
|
+
if (typeof subschemasByName === "object" && subschemasByName !== null) {
|
|
1356
|
+
relaxed[keyword] = Object.fromEntries(
|
|
1357
|
+
Object.entries(subschemasByName).map(([name, subschema]) => [name, relaxSchema(subschema)])
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
const exclusiveBranches = relaxed["oneOf"];
|
|
1362
|
+
if (Array.isArray(exclusiveBranches)) {
|
|
1363
|
+
delete relaxed["oneOf"];
|
|
1364
|
+
const branches = exclusiveBranches.map(relaxSchema);
|
|
1365
|
+
if (Array.isArray(relaxed["anyOf"])) {
|
|
1366
|
+
const allOf = Array.isArray(relaxed["allOf"]) ? relaxed["allOf"] : [];
|
|
1367
|
+
relaxed["allOf"] = [...allOf, { anyOf: branches }];
|
|
1368
|
+
} else {
|
|
1369
|
+
relaxed["anyOf"] = branches;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
return relaxed;
|
|
1373
|
+
}
|
|
1290
1374
|
function getZodSchemaFromJsonSchema(jsonSchema, toolName, logger) {
|
|
1291
1375
|
if (typeof jsonSchema !== "object" || jsonSchema === null) {
|
|
1292
1376
|
logger.warn(`[${toolName}] No valid JSON schema provided, using permissive schema`);
|
|
@@ -1314,6 +1398,18 @@ function getZodSchemaFromJsonSchema(jsonSchema, toolName, logger) {
|
|
|
1314
1398
|
|
|
1315
1399
|
// libs/adapters/src/openapi/openapi.adapter.ts
|
|
1316
1400
|
var RESERVED_KEYS2 = ["__proto__", "constructor", "prototype"];
|
|
1401
|
+
var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
|
|
1402
|
+
function normalizeMethods(adapterName, optionName, methods) {
|
|
1403
|
+
return methods?.map((method) => {
|
|
1404
|
+
const lowerMethod = method.toLowerCase();
|
|
1405
|
+
if (!HTTP_METHODS.includes(lowerMethod)) {
|
|
1406
|
+
throw new Error(
|
|
1407
|
+
`[OpenAPI Adapter: ${adapterName}] generateOptions.${optionName} lists "${method}", which is not an HTTP method; use one of ${HTTP_METHODS.join(", ")}.`
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
return lowerMethod;
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1317
1413
|
function createConsoleLogger(prefix) {
|
|
1318
1414
|
const formatMessage = (level, msg) => `[${prefix}] ${level}: ${msg}`;
|
|
1319
1415
|
return {
|
|
@@ -1332,9 +1428,14 @@ var OpenapiAdapter = class extends DynamicAdapter {
|
|
|
1332
1428
|
poller = null;
|
|
1333
1429
|
updateCallbacks = /* @__PURE__ */ new Set();
|
|
1334
1430
|
rebuildChain = Promise.resolve();
|
|
1431
|
+
methodFilters;
|
|
1335
1432
|
constructor(options) {
|
|
1336
1433
|
super();
|
|
1337
1434
|
this.options = options;
|
|
1435
|
+
this.methodFilters = {
|
|
1436
|
+
includeMethods: normalizeMethods(options.name, "includeMethods", options.generateOptions?.includeMethods),
|
|
1437
|
+
excludeMethods: normalizeMethods(options.name, "excludeMethods", options.generateOptions?.excludeMethods)
|
|
1438
|
+
};
|
|
1338
1439
|
this.logger = options.logger ?? createConsoleLogger(`openapi:${options.name}`);
|
|
1339
1440
|
if (options.polling?.enabled && !("url" in options)) {
|
|
1340
1441
|
throw new Error(
|
|
@@ -1358,6 +1459,8 @@ var OpenapiAdapter = class extends DynamicAdapter {
|
|
|
1358
1459
|
const generateOptions = this.options.generateOptions;
|
|
1359
1460
|
let openapiTools = await this.generator.generateTools({
|
|
1360
1461
|
...generateOptions,
|
|
1462
|
+
...this.methodFilters,
|
|
1463
|
+
filterFn: this.operationFilter(),
|
|
1361
1464
|
preferredStatusCodes: generateOptions?.preferredStatusCodes ?? [200, 201, 202, 204],
|
|
1362
1465
|
includeDeprecated: generateOptions?.includeDeprecated ?? false,
|
|
1363
1466
|
includeAllResponses: generateOptions?.includeAllResponses ?? true,
|
|
@@ -1443,28 +1546,31 @@ var OpenapiAdapter = class extends DynamicAdapter {
|
|
|
1443
1546
|
resolveRefResolution() {
|
|
1444
1547
|
return this.options.loadOptions?.refResolution ?? { allowedProtocols: [] };
|
|
1445
1548
|
}
|
|
1549
|
+
/**
|
|
1550
|
+
* `generateOptions.filterFn`, and with `includeOperations` set, only the operations it names: the
|
|
1551
|
+
* generator lets an operation with no `operationId` through that list.
|
|
1552
|
+
*/
|
|
1553
|
+
operationFilter() {
|
|
1554
|
+
const { includeOperations, filterFn } = this.options.generateOptions ?? {};
|
|
1555
|
+
if (!includeOperations) return filterFn;
|
|
1556
|
+
return (operation) => operation.operationId !== void 0 && includeOperations.includes(operation.operationId) && (filterFn?.(operation) ?? true);
|
|
1557
|
+
}
|
|
1446
1558
|
async initializeGenerator() {
|
|
1447
|
-
const
|
|
1559
|
+
const loadOptions = {
|
|
1560
|
+
...this.options.loadOptions,
|
|
1561
|
+
baseUrl: this.options.baseUrl,
|
|
1562
|
+
validate: this.options.loadOptions?.validate ?? true,
|
|
1563
|
+
dereference: this.options.loadOptions?.dereference ?? true,
|
|
1564
|
+
// SECURITY: do not follow spec-URL redirects by default — a 3xx to an
|
|
1565
|
+
// internal target would otherwise be fetched. With mcp-from-openapi
|
|
1566
|
+
// ≥ 2.5.0 each hop is re-validated; opt in with loadOptions.followRedirects.
|
|
1567
|
+
followRedirects: this.options.loadOptions?.followRedirects ?? false,
|
|
1568
|
+
refResolution: this.resolveRefResolution()
|
|
1569
|
+
};
|
|
1448
1570
|
if ("url" in this.options) {
|
|
1449
|
-
return await OpenAPIToolGenerator.fromURL(this.options.url,
|
|
1450
|
-
baseUrl: this.options.baseUrl,
|
|
1451
|
-
validate: this.options.loadOptions?.validate ?? true,
|
|
1452
|
-
dereference: this.options.loadOptions?.dereference ?? true,
|
|
1453
|
-
headers: this.options.loadOptions?.headers,
|
|
1454
|
-
timeout: this.options.loadOptions?.timeout,
|
|
1455
|
-
// SECURITY: do not follow spec-URL redirects by default — a 3xx to an
|
|
1456
|
-
// internal target would otherwise be fetched. With mcp-from-openapi
|
|
1457
|
-
// ≥ 2.5.0 each hop is re-validated; opt in with loadOptions.followRedirects.
|
|
1458
|
-
followRedirects: this.options.loadOptions?.followRedirects ?? false,
|
|
1459
|
-
refResolution
|
|
1460
|
-
});
|
|
1571
|
+
return await OpenAPIToolGenerator.fromURL(this.options.url, loadOptions);
|
|
1461
1572
|
} else if ("spec" in this.options) {
|
|
1462
|
-
return await OpenAPIToolGenerator.fromJSON(this.options.spec,
|
|
1463
|
-
baseUrl: this.options.baseUrl,
|
|
1464
|
-
validate: this.options.loadOptions?.validate ?? true,
|
|
1465
|
-
dereference: this.options.loadOptions?.dereference ?? true,
|
|
1466
|
-
refResolution
|
|
1467
|
-
});
|
|
1573
|
+
return await OpenAPIToolGenerator.fromJSON(this.options.spec, loadOptions);
|
|
1468
1574
|
} else {
|
|
1469
1575
|
throw new Error("Either url or spec must be provided in OpenApiAdapterOptions");
|
|
1470
1576
|
}
|
package/esm/openapi/index.mjs
CHANGED
|
@@ -10,7 +10,10 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// libs/adapters/src/openapi/openapi.adapter.ts
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
normalizeSsrfOptions as normalizeSsrfOptions2,
|
|
15
|
+
OpenAPIToolGenerator
|
|
16
|
+
} from "mcp-from-openapi";
|
|
14
17
|
import { Adapter, DynamicAdapter } from "@frontmcp/sdk";
|
|
15
18
|
|
|
16
19
|
// libs/adapters/src/openapi/openapi-spec-poller.ts
|
|
@@ -605,7 +608,7 @@ async function resolveInputSecurity(tool2, input, options, securityResolver) {
|
|
|
605
608
|
|
|
606
609
|
// libs/adapters/src/openapi/openapi.tool.ts
|
|
607
610
|
import { z as z2 } from "@frontmcp/lazy-zod";
|
|
608
|
-
import { tool } from "@frontmcp/sdk";
|
|
611
|
+
import { InvalidInputError, tool } from "@frontmcp/sdk";
|
|
609
612
|
import { isRedirectResponse } from "@frontmcp/utils";
|
|
610
613
|
|
|
611
614
|
// libs/adapters/src/openapi/openapi.frontmcp-schema.ts
|
|
@@ -651,7 +654,9 @@ var KNOWN_EXTENSION_FIELDS = /* @__PURE__ */ new Set([
|
|
|
651
654
|
"codecall",
|
|
652
655
|
"tags",
|
|
653
656
|
"hideFromDiscovery",
|
|
654
|
-
"examples"
|
|
657
|
+
"examples",
|
|
658
|
+
"meta",
|
|
659
|
+
"icons"
|
|
655
660
|
]);
|
|
656
661
|
var KNOWN_ANNOTATION_FIELDS = /* @__PURE__ */ new Set([
|
|
657
662
|
"title",
|
|
@@ -875,6 +880,10 @@ function appendCookie(headers, name, value) {
|
|
|
875
880
|
const combined = existing ? `${existing}; ${cookiePair}` : cookiePair;
|
|
876
881
|
headers.set("Cookie", combined);
|
|
877
882
|
}
|
|
883
|
+
function specDefault(tool2, inputKey) {
|
|
884
|
+
const property = tool2.inputSchema.properties?.[inputKey];
|
|
885
|
+
return typeof property === "object" ? property.default : void 0;
|
|
886
|
+
}
|
|
878
887
|
function buildRequest(tool2, input, security, baseUrl) {
|
|
879
888
|
const rawBaseUrl = tool2.metadata.servers?.[0]?.url || baseUrl;
|
|
880
889
|
validateBaseUrl(rawBaseUrl);
|
|
@@ -888,7 +897,7 @@ function buildRequest(tool2, input, security, baseUrl) {
|
|
|
888
897
|
let body;
|
|
889
898
|
for (const mapper of tool2.mapper) {
|
|
890
899
|
if (mapper.security) continue;
|
|
891
|
-
const value = input[mapper.inputKey];
|
|
900
|
+
const value = input[mapper.inputKey] ?? (mapper.required ? specDefault(tool2, mapper.inputKey) : void 0);
|
|
892
901
|
if (value === void 0 || value === null) {
|
|
893
902
|
if (mapper.required) {
|
|
894
903
|
throw new Error(
|
|
@@ -1002,9 +1011,11 @@ function createOpenApiTool(openapiTool, options, logger) {
|
|
|
1002
1011
|
const inputTransforms = metadata.adapter?.inputTransforms ?? [];
|
|
1003
1012
|
const toolTransform = metadata.adapter?.toolTransform ?? {};
|
|
1004
1013
|
const postToolTransform = metadata.adapter?.postToolTransform;
|
|
1014
|
+
const serverFilledKeys = new Set(inputTransforms.map((transform) => transform.inputKey));
|
|
1005
1015
|
const frontmcpValidation = validateFrontMcpExtension(metadata.frontmcp, openapiTool.name, logger);
|
|
1006
1016
|
const frontmcpExt = frontmcpValidation.data;
|
|
1007
1017
|
const schemaResult = getZodSchemaFromJsonSchema(openapiTool.inputSchema, openapiTool.name, logger);
|
|
1018
|
+
const argumentCheck = schemaResult.conversionFailed ? void 0 : getZodSchemaFromJsonSchema(toArgumentJsonSchema(openapiTool), openapiTool.name, logger);
|
|
1008
1019
|
let wrappedOutputSchema;
|
|
1009
1020
|
if (openapiTool.outputSchema !== void 0) {
|
|
1010
1021
|
const baseOutputSchema = openapiTool.outputSchema ?? { type: "string" };
|
|
@@ -1022,11 +1033,15 @@ function createOpenApiTool(openapiTool, options, logger) {
|
|
|
1022
1033
|
const toolMetadata = {
|
|
1023
1034
|
id: openapiTool.name,
|
|
1024
1035
|
name: openapiTool.name,
|
|
1036
|
+
...openapiTool.title && { title: openapiTool.title },
|
|
1025
1037
|
description: openapiTool.description,
|
|
1026
1038
|
inputSchema: schemaResult.schema.shape || {},
|
|
1027
1039
|
rawInputSchema: openapiTool.inputSchema,
|
|
1028
1040
|
// Add output schema for tool/list to expose (only if not moved to description)
|
|
1029
|
-
...wrappedOutputSchema && { rawOutputSchema: wrappedOutputSchema }
|
|
1041
|
+
...wrappedOutputSchema && { rawOutputSchema: wrappedOutputSchema },
|
|
1042
|
+
...openapiTool.annotations && { annotations: { ...openapiTool.annotations } },
|
|
1043
|
+
...openapiTool.icons?.length && { icons: openapiTool.icons.map((icon) => ({ ...icon })) },
|
|
1044
|
+
...openapiTool._meta && { _meta: { ...openapiTool._meta } }
|
|
1030
1045
|
};
|
|
1031
1046
|
if (schemaResult.conversionFailed) {
|
|
1032
1047
|
toolMetadata["_schemaConversionFailed"] = true;
|
|
@@ -1034,7 +1049,10 @@ function createOpenApiTool(openapiTool, options, logger) {
|
|
|
1034
1049
|
}
|
|
1035
1050
|
if (frontmcpExt) {
|
|
1036
1051
|
if (frontmcpExt.annotations) {
|
|
1037
|
-
toolMetadata["annotations"] = {
|
|
1052
|
+
toolMetadata["annotations"] = {
|
|
1053
|
+
...toolMetadata["annotations"] || {},
|
|
1054
|
+
...frontmcpExt.annotations
|
|
1055
|
+
};
|
|
1038
1056
|
}
|
|
1039
1057
|
if (frontmcpExt.tags) {
|
|
1040
1058
|
toolMetadata["tags"] = [...frontmcpExt.tags];
|
|
@@ -1074,16 +1092,20 @@ function createOpenApiTool(openapiTool, options, logger) {
|
|
|
1074
1092
|
}
|
|
1075
1093
|
return tool(toolMetadata)(async (input, toolCtx) => {
|
|
1076
1094
|
const ctx = toolCtx.context;
|
|
1095
|
+
const modelInput = omitKeys(input, serverFilledKeys);
|
|
1096
|
+
if (argumentCheck && !argumentCheck.conversionFailed) {
|
|
1097
|
+
const validation = argumentCheck.schema.safeParse(modelInput);
|
|
1098
|
+
if (!validation.success) {
|
|
1099
|
+
const problems = validation.error.issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`);
|
|
1100
|
+
throw new InvalidInputError(`Invalid arguments for tool '${openapiTool.name}': ${problems.join("; ")}`);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1077
1103
|
const transformContext = {
|
|
1078
1104
|
ctx,
|
|
1079
1105
|
env: process.env,
|
|
1080
1106
|
tool: openapiTool
|
|
1081
1107
|
};
|
|
1082
|
-
const injectedInput = await injectTransformedValues(
|
|
1083
|
-
input,
|
|
1084
|
-
inputTransforms,
|
|
1085
|
-
transformContext
|
|
1086
|
-
);
|
|
1108
|
+
const injectedInput = await injectTransformedValues(modelInput, inputTransforms, transformContext);
|
|
1087
1109
|
const security = await resolveToolSecurity(openapiTool, ctx, options, {
|
|
1088
1110
|
input: injectedInput,
|
|
1089
1111
|
deferCredentialCheck: true
|
|
@@ -1268,6 +1290,10 @@ async function safeInject(transform, ctx, timeoutMs = DEFAULT_TRANSFORM_TIMEOUT_
|
|
|
1268
1290
|
function isPlainObject(value) {
|
|
1269
1291
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1270
1292
|
}
|
|
1293
|
+
function omitKeys(input, keys) {
|
|
1294
|
+
if (keys.size === 0 || !isPlainObject(input)) return input;
|
|
1295
|
+
return Object.fromEntries(Object.entries(input).filter(([key]) => !keys.has(key)));
|
|
1296
|
+
}
|
|
1271
1297
|
async function injectTransformedValues(input, transforms, ctx) {
|
|
1272
1298
|
if (!isPlainObject(input)) {
|
|
1273
1299
|
throw new Error(`Invalid input type: expected object, got ${input === null ? "null" : typeof input}`);
|
|
@@ -1287,6 +1313,64 @@ async function injectTransformedValues(input, transforms, ctx) {
|
|
|
1287
1313
|
}
|
|
1288
1314
|
return result;
|
|
1289
1315
|
}
|
|
1316
|
+
var SUBSCHEMA_KEYWORDS = [
|
|
1317
|
+
"items",
|
|
1318
|
+
"additionalProperties",
|
|
1319
|
+
"not",
|
|
1320
|
+
"if",
|
|
1321
|
+
"then",
|
|
1322
|
+
"else",
|
|
1323
|
+
"contains",
|
|
1324
|
+
"propertyNames",
|
|
1325
|
+
"unevaluatedItems",
|
|
1326
|
+
"unevaluatedProperties"
|
|
1327
|
+
];
|
|
1328
|
+
var SUBSCHEMA_LIST_KEYWORDS = ["allOf", "anyOf", "prefixItems"];
|
|
1329
|
+
var SUBSCHEMA_MAP_KEYWORDS = ["properties", "patternProperties", "dependentSchemas", "$defs", "definitions"];
|
|
1330
|
+
function toArgumentJsonSchema(openapiTool) {
|
|
1331
|
+
const argumentSchema = relaxSchema(openapiTool.inputSchema);
|
|
1332
|
+
const credentialKeys = new Set(
|
|
1333
|
+
openapiTool.mapper.filter((mapper) => mapper.security).map((mapper) => mapper.inputKey)
|
|
1334
|
+
);
|
|
1335
|
+
const required = argumentSchema["required"];
|
|
1336
|
+
if (Array.isArray(required)) {
|
|
1337
|
+
argumentSchema["required"] = required.filter((key) => !credentialKeys.has(key));
|
|
1338
|
+
}
|
|
1339
|
+
return argumentSchema;
|
|
1340
|
+
}
|
|
1341
|
+
function relaxSchema(schema) {
|
|
1342
|
+
if (Array.isArray(schema)) return schema.map(relaxSchema);
|
|
1343
|
+
if (typeof schema !== "object" || schema === null) return schema;
|
|
1344
|
+
const relaxed = { ...schema };
|
|
1345
|
+
delete relaxed["format"];
|
|
1346
|
+
for (const keyword of SUBSCHEMA_KEYWORDS) {
|
|
1347
|
+
if (keyword in relaxed) relaxed[keyword] = relaxSchema(relaxed[keyword]);
|
|
1348
|
+
}
|
|
1349
|
+
for (const keyword of SUBSCHEMA_LIST_KEYWORDS) {
|
|
1350
|
+
const subschemas = relaxed[keyword];
|
|
1351
|
+
if (Array.isArray(subschemas)) relaxed[keyword] = subschemas.map(relaxSchema);
|
|
1352
|
+
}
|
|
1353
|
+
for (const keyword of SUBSCHEMA_MAP_KEYWORDS) {
|
|
1354
|
+
const subschemasByName = relaxed[keyword];
|
|
1355
|
+
if (typeof subschemasByName === "object" && subschemasByName !== null) {
|
|
1356
|
+
relaxed[keyword] = Object.fromEntries(
|
|
1357
|
+
Object.entries(subschemasByName).map(([name, subschema]) => [name, relaxSchema(subschema)])
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
const exclusiveBranches = relaxed["oneOf"];
|
|
1362
|
+
if (Array.isArray(exclusiveBranches)) {
|
|
1363
|
+
delete relaxed["oneOf"];
|
|
1364
|
+
const branches = exclusiveBranches.map(relaxSchema);
|
|
1365
|
+
if (Array.isArray(relaxed["anyOf"])) {
|
|
1366
|
+
const allOf = Array.isArray(relaxed["allOf"]) ? relaxed["allOf"] : [];
|
|
1367
|
+
relaxed["allOf"] = [...allOf, { anyOf: branches }];
|
|
1368
|
+
} else {
|
|
1369
|
+
relaxed["anyOf"] = branches;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
return relaxed;
|
|
1373
|
+
}
|
|
1290
1374
|
function getZodSchemaFromJsonSchema(jsonSchema, toolName, logger) {
|
|
1291
1375
|
if (typeof jsonSchema !== "object" || jsonSchema === null) {
|
|
1292
1376
|
logger.warn(`[${toolName}] No valid JSON schema provided, using permissive schema`);
|
|
@@ -1314,6 +1398,18 @@ function getZodSchemaFromJsonSchema(jsonSchema, toolName, logger) {
|
|
|
1314
1398
|
|
|
1315
1399
|
// libs/adapters/src/openapi/openapi.adapter.ts
|
|
1316
1400
|
var RESERVED_KEYS2 = ["__proto__", "constructor", "prototype"];
|
|
1401
|
+
var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
|
|
1402
|
+
function normalizeMethods(adapterName, optionName, methods) {
|
|
1403
|
+
return methods?.map((method) => {
|
|
1404
|
+
const lowerMethod = method.toLowerCase();
|
|
1405
|
+
if (!HTTP_METHODS.includes(lowerMethod)) {
|
|
1406
|
+
throw new Error(
|
|
1407
|
+
`[OpenAPI Adapter: ${adapterName}] generateOptions.${optionName} lists "${method}", which is not an HTTP method; use one of ${HTTP_METHODS.join(", ")}.`
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
return lowerMethod;
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1317
1413
|
function createConsoleLogger(prefix) {
|
|
1318
1414
|
const formatMessage = (level, msg) => `[${prefix}] ${level}: ${msg}`;
|
|
1319
1415
|
return {
|
|
@@ -1332,9 +1428,14 @@ var OpenapiAdapter = class extends DynamicAdapter {
|
|
|
1332
1428
|
poller = null;
|
|
1333
1429
|
updateCallbacks = /* @__PURE__ */ new Set();
|
|
1334
1430
|
rebuildChain = Promise.resolve();
|
|
1431
|
+
methodFilters;
|
|
1335
1432
|
constructor(options) {
|
|
1336
1433
|
super();
|
|
1337
1434
|
this.options = options;
|
|
1435
|
+
this.methodFilters = {
|
|
1436
|
+
includeMethods: normalizeMethods(options.name, "includeMethods", options.generateOptions?.includeMethods),
|
|
1437
|
+
excludeMethods: normalizeMethods(options.name, "excludeMethods", options.generateOptions?.excludeMethods)
|
|
1438
|
+
};
|
|
1338
1439
|
this.logger = options.logger ?? createConsoleLogger(`openapi:${options.name}`);
|
|
1339
1440
|
if (options.polling?.enabled && !("url" in options)) {
|
|
1340
1441
|
throw new Error(
|
|
@@ -1358,6 +1459,8 @@ var OpenapiAdapter = class extends DynamicAdapter {
|
|
|
1358
1459
|
const generateOptions = this.options.generateOptions;
|
|
1359
1460
|
let openapiTools = await this.generator.generateTools({
|
|
1360
1461
|
...generateOptions,
|
|
1462
|
+
...this.methodFilters,
|
|
1463
|
+
filterFn: this.operationFilter(),
|
|
1361
1464
|
preferredStatusCodes: generateOptions?.preferredStatusCodes ?? [200, 201, 202, 204],
|
|
1362
1465
|
includeDeprecated: generateOptions?.includeDeprecated ?? false,
|
|
1363
1466
|
includeAllResponses: generateOptions?.includeAllResponses ?? true,
|
|
@@ -1443,28 +1546,31 @@ var OpenapiAdapter = class extends DynamicAdapter {
|
|
|
1443
1546
|
resolveRefResolution() {
|
|
1444
1547
|
return this.options.loadOptions?.refResolution ?? { allowedProtocols: [] };
|
|
1445
1548
|
}
|
|
1549
|
+
/**
|
|
1550
|
+
* `generateOptions.filterFn`, and with `includeOperations` set, only the operations it names: the
|
|
1551
|
+
* generator lets an operation with no `operationId` through that list.
|
|
1552
|
+
*/
|
|
1553
|
+
operationFilter() {
|
|
1554
|
+
const { includeOperations, filterFn } = this.options.generateOptions ?? {};
|
|
1555
|
+
if (!includeOperations) return filterFn;
|
|
1556
|
+
return (operation) => operation.operationId !== void 0 && includeOperations.includes(operation.operationId) && (filterFn?.(operation) ?? true);
|
|
1557
|
+
}
|
|
1446
1558
|
async initializeGenerator() {
|
|
1447
|
-
const
|
|
1559
|
+
const loadOptions = {
|
|
1560
|
+
...this.options.loadOptions,
|
|
1561
|
+
baseUrl: this.options.baseUrl,
|
|
1562
|
+
validate: this.options.loadOptions?.validate ?? true,
|
|
1563
|
+
dereference: this.options.loadOptions?.dereference ?? true,
|
|
1564
|
+
// SECURITY: do not follow spec-URL redirects by default — a 3xx to an
|
|
1565
|
+
// internal target would otherwise be fetched. With mcp-from-openapi
|
|
1566
|
+
// ≥ 2.5.0 each hop is re-validated; opt in with loadOptions.followRedirects.
|
|
1567
|
+
followRedirects: this.options.loadOptions?.followRedirects ?? false,
|
|
1568
|
+
refResolution: this.resolveRefResolution()
|
|
1569
|
+
};
|
|
1448
1570
|
if ("url" in this.options) {
|
|
1449
|
-
return await OpenAPIToolGenerator.fromURL(this.options.url,
|
|
1450
|
-
baseUrl: this.options.baseUrl,
|
|
1451
|
-
validate: this.options.loadOptions?.validate ?? true,
|
|
1452
|
-
dereference: this.options.loadOptions?.dereference ?? true,
|
|
1453
|
-
headers: this.options.loadOptions?.headers,
|
|
1454
|
-
timeout: this.options.loadOptions?.timeout,
|
|
1455
|
-
// SECURITY: do not follow spec-URL redirects by default — a 3xx to an
|
|
1456
|
-
// internal target would otherwise be fetched. With mcp-from-openapi
|
|
1457
|
-
// ≥ 2.5.0 each hop is re-validated; opt in with loadOptions.followRedirects.
|
|
1458
|
-
followRedirects: this.options.loadOptions?.followRedirects ?? false,
|
|
1459
|
-
refResolution
|
|
1460
|
-
});
|
|
1571
|
+
return await OpenAPIToolGenerator.fromURL(this.options.url, loadOptions);
|
|
1461
1572
|
} else if ("spec" in this.options) {
|
|
1462
|
-
return await OpenAPIToolGenerator.fromJSON(this.options.spec,
|
|
1463
|
-
baseUrl: this.options.baseUrl,
|
|
1464
|
-
validate: this.options.loadOptions?.validate ?? true,
|
|
1465
|
-
dereference: this.options.loadOptions?.dereference ?? true,
|
|
1466
|
-
refResolution
|
|
1467
|
-
});
|
|
1573
|
+
return await OpenAPIToolGenerator.fromJSON(this.options.spec, loadOptions);
|
|
1468
1574
|
} else {
|
|
1469
1575
|
throw new Error("Either url or spec must be provided in OpenApiAdapterOptions");
|
|
1470
1576
|
}
|
package/esm/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/adapters",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.3",
|
|
4
4
|
"description": "Adapters for the FrontMCP framework",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"homepage": "https://docs.agentfront.dev",
|
|
@@ -64,15 +64,15 @@
|
|
|
64
64
|
"node": ">=24.0.0"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@frontmcp/auth": "1.9.
|
|
68
|
-
"@frontmcp/di": "1.9.
|
|
69
|
-
"@frontmcp/sdk": "1.9.
|
|
70
|
-
"@frontmcp/utils": "1.9.
|
|
67
|
+
"@frontmcp/auth": "1.9.3",
|
|
68
|
+
"@frontmcp/di": "1.9.3",
|
|
69
|
+
"@frontmcp/sdk": "1.9.3",
|
|
70
|
+
"@frontmcp/utils": "1.9.3",
|
|
71
71
|
"js-yaml": "^4.1.0",
|
|
72
72
|
"mcp-from-openapi": "2.8.0",
|
|
73
73
|
"openapi-types": "^12.1.3"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
|
-
"@frontmcp/lazy-zod": "1.9.
|
|
76
|
+
"@frontmcp/lazy-zod": "1.9.3"
|
|
77
77
|
}
|
|
78
78
|
}
|
package/esm/skills/index.mjs
CHANGED
|
@@ -843,7 +843,7 @@ var SaasPullSource = class {
|
|
|
843
843
|
bundle = await this.loadCache();
|
|
844
844
|
if (!bundle) {
|
|
845
845
|
throw new Error(
|
|
846
|
-
`[saas-source] initial pull failed and no cached bundle is available
|
|
846
|
+
`[saas-source] initial pull failed and no cached bundle is available ${this.cacheLocation()}: ${e.message}`,
|
|
847
847
|
{ cause: e }
|
|
848
848
|
);
|
|
849
849
|
}
|
|
@@ -989,6 +989,10 @@ var SaasPullSource = class {
|
|
|
989
989
|
trimmed.startsWith("{") || trimmed.startsWith("[") ? { kind: "json", content: body } : { kind: "yaml", content: body }
|
|
990
990
|
);
|
|
991
991
|
}
|
|
992
|
+
/** Where the last-good bundle is kept, for messages: an injected store (a Worker's KV) has no file path. */
|
|
993
|
+
cacheLocation() {
|
|
994
|
+
return this.deps.cache ? "in the injected bundle cache" : `at ${this.cachePath()}`;
|
|
995
|
+
}
|
|
992
996
|
cachePath() {
|
|
993
997
|
const dir = this.cacheDir ?? DEFAULT_CACHE_DIR;
|
|
994
998
|
return pathResolve(dir, `${this.options.expectedAudience.replace(/[^a-zA-Z0-9_.-]/g, "_")}.json`);
|