@frontmcp/adapters 0.12.2 → 1.0.0-beta.10
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 +291 -18
- package/esm/openapi/index.mjs +291 -18
- package/esm/package.json +4 -4
- package/index.d.ts.map +1 -1
- package/index.js +298 -23
- package/openapi/index.d.ts +4 -1
- package/openapi/index.d.ts.map +1 -1
- package/openapi/index.js +298 -23
- package/openapi/openapi-spec-poller.d.ts +56 -0
- package/openapi/openapi-spec-poller.d.ts.map +1 -0
- package/openapi/openapi-spec-poller.types.d.ts +74 -0
- package/openapi/openapi-spec-poller.types.d.ts.map +1 -0
- package/openapi/openapi.adapter.d.ts +23 -0
- package/openapi/openapi.adapter.d.ts.map +1 -1
- package/openapi/openapi.errors.d.ts +8 -0
- package/openapi/openapi.errors.d.ts.map +1 -0
- package/openapi/openapi.types.d.ts +25 -10
- package/openapi/openapi.types.d.ts.map +1 -1
- package/openapi/openapi.utils.d.ts.map +1 -1
- package/package.json +4 -4
package/esm/index.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { convertJsonSchemaToZod } from "zod-from-json-schema";
|
|
|
20
20
|
|
|
21
21
|
// libs/adapters/src/openapi/openapi.utils.ts
|
|
22
22
|
import { validateBaseUrl } from "@frontmcp/utils";
|
|
23
|
+
import { PublicMcpError } from "@frontmcp/sdk";
|
|
23
24
|
function coerceToString(value, paramName, location) {
|
|
24
25
|
if (value === null || value === void 0) {
|
|
25
26
|
return "";
|
|
@@ -76,8 +77,10 @@ function buildRequest(tool2, input, security, baseUrl) {
|
|
|
76
77
|
case "header": {
|
|
77
78
|
const headerValue = coerceToString(value, mapper.key, "header");
|
|
78
79
|
if (/[\r\n\x00\f\v]/.test(headerValue)) {
|
|
79
|
-
throw new
|
|
80
|
-
`Invalid header value for '${mapper.key}': contains control characters (possible header injection attack)
|
|
80
|
+
throw new PublicMcpError(
|
|
81
|
+
`Invalid header value for '${mapper.key}': contains control characters (possible header injection attack)`,
|
|
82
|
+
"INVALID_HEADER_VALUE",
|
|
83
|
+
400
|
|
81
84
|
);
|
|
82
85
|
}
|
|
83
86
|
headers.set(mapper.key, headerValue);
|
|
@@ -860,6 +863,174 @@ function getZodSchemaFromJsonSchema(jsonSchema, toolName, logger) {
|
|
|
860
863
|
}
|
|
861
864
|
}
|
|
862
865
|
|
|
866
|
+
// libs/adapters/src/openapi/openapi-spec-poller.ts
|
|
867
|
+
import { sha256Hex } from "@frontmcp/utils";
|
|
868
|
+
|
|
869
|
+
// libs/adapters/src/openapi/openapi.errors.ts
|
|
870
|
+
import { InternalMcpError } from "@frontmcp/sdk";
|
|
871
|
+
var OpenAPIFetchError = class extends InternalMcpError {
|
|
872
|
+
constructor(url, status, statusText) {
|
|
873
|
+
super(`OpenAPI spec fetch failed from "${url}": ${status} ${statusText}`, "OPENAPI_FETCH_FAILED");
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
// libs/adapters/src/openapi/openapi-spec-poller.ts
|
|
878
|
+
var DEFAULT_RETRY = {
|
|
879
|
+
maxRetries: 3,
|
|
880
|
+
initialDelayMs: 1e3,
|
|
881
|
+
maxDelayMs: 1e4,
|
|
882
|
+
backoffMultiplier: 2
|
|
883
|
+
};
|
|
884
|
+
var OpenApiSpecPoller = class {
|
|
885
|
+
url;
|
|
886
|
+
intervalMs;
|
|
887
|
+
fetchTimeoutMs;
|
|
888
|
+
changeDetection;
|
|
889
|
+
retry;
|
|
890
|
+
unhealthyThreshold;
|
|
891
|
+
headers;
|
|
892
|
+
callbacks;
|
|
893
|
+
intervalTimer = null;
|
|
894
|
+
lastHash = null;
|
|
895
|
+
lastEtag = null;
|
|
896
|
+
lastModified = null;
|
|
897
|
+
consecutiveFailures = 0;
|
|
898
|
+
isPolling = false;
|
|
899
|
+
wasUnhealthy = false;
|
|
900
|
+
health = "unknown";
|
|
901
|
+
constructor(url, options = {}, callbacks = {}) {
|
|
902
|
+
this.url = url;
|
|
903
|
+
this.intervalMs = options.intervalMs ?? 6e4;
|
|
904
|
+
this.fetchTimeoutMs = options.fetchTimeoutMs ?? 1e4;
|
|
905
|
+
this.changeDetection = options.changeDetection ?? "auto";
|
|
906
|
+
this.retry = { ...DEFAULT_RETRY, ...options.retry };
|
|
907
|
+
this.unhealthyThreshold = options.unhealthyThreshold ?? 3;
|
|
908
|
+
this.headers = options.headers ?? {};
|
|
909
|
+
this.callbacks = callbacks;
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Start polling for changes.
|
|
913
|
+
*/
|
|
914
|
+
start() {
|
|
915
|
+
if (this.intervalTimer) return;
|
|
916
|
+
this.poll().catch(() => {
|
|
917
|
+
});
|
|
918
|
+
this.intervalTimer = setInterval(() => {
|
|
919
|
+
this.poll().catch(() => {
|
|
920
|
+
});
|
|
921
|
+
}, this.intervalMs);
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Stop polling.
|
|
925
|
+
*/
|
|
926
|
+
stop() {
|
|
927
|
+
if (this.intervalTimer) {
|
|
928
|
+
clearInterval(this.intervalTimer);
|
|
929
|
+
this.intervalTimer = null;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Get current stats.
|
|
934
|
+
*/
|
|
935
|
+
getStats() {
|
|
936
|
+
return {
|
|
937
|
+
hash: this.lastHash,
|
|
938
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
939
|
+
health: this.health,
|
|
940
|
+
isRunning: this.intervalTimer !== null
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Perform a single poll cycle.
|
|
945
|
+
*/
|
|
946
|
+
async poll() {
|
|
947
|
+
if (this.isPolling) return;
|
|
948
|
+
this.isPolling = true;
|
|
949
|
+
try {
|
|
950
|
+
await this.fetchWithRetry();
|
|
951
|
+
} finally {
|
|
952
|
+
this.isPolling = false;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
async fetchWithRetry() {
|
|
956
|
+
const { maxRetries, initialDelayMs, maxDelayMs, backoffMultiplier } = this.retry;
|
|
957
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
958
|
+
try {
|
|
959
|
+
await this.doFetch();
|
|
960
|
+
if (this.consecutiveFailures > 0) {
|
|
961
|
+
this.consecutiveFailures = 0;
|
|
962
|
+
if (this.wasUnhealthy) {
|
|
963
|
+
this.wasUnhealthy = false;
|
|
964
|
+
this.health = "healthy";
|
|
965
|
+
this.callbacks.onRecovered?.();
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
this.health = "healthy";
|
|
969
|
+
return;
|
|
970
|
+
} catch (error) {
|
|
971
|
+
if (attempt === maxRetries) {
|
|
972
|
+
this.consecutiveFailures++;
|
|
973
|
+
this.callbacks.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
974
|
+
if (this.consecutiveFailures >= this.unhealthyThreshold && !this.wasUnhealthy) {
|
|
975
|
+
this.wasUnhealthy = true;
|
|
976
|
+
this.health = "unhealthy";
|
|
977
|
+
this.callbacks.onUnhealthy?.(this.consecutiveFailures);
|
|
978
|
+
}
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const delay = Math.min(initialDelayMs * Math.pow(backoffMultiplier, attempt), maxDelayMs);
|
|
982
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
async doFetch() {
|
|
987
|
+
const headers = { ...this.headers };
|
|
988
|
+
if (this.changeDetection !== "content-hash") {
|
|
989
|
+
if (this.lastEtag) {
|
|
990
|
+
headers["If-None-Match"] = this.lastEtag;
|
|
991
|
+
}
|
|
992
|
+
if (this.lastModified) {
|
|
993
|
+
headers["If-Modified-Since"] = this.lastModified;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
const controller = new AbortController();
|
|
997
|
+
const timeout = setTimeout(() => controller.abort(), this.fetchTimeoutMs);
|
|
998
|
+
try {
|
|
999
|
+
const response = await fetch(this.url, {
|
|
1000
|
+
headers,
|
|
1001
|
+
signal: controller.signal
|
|
1002
|
+
});
|
|
1003
|
+
if (response.status === 304) {
|
|
1004
|
+
this.callbacks.onUnchanged?.();
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
if (!response.ok) {
|
|
1008
|
+
throw new OpenAPIFetchError(this.url, response.status, response.statusText);
|
|
1009
|
+
}
|
|
1010
|
+
const etag = response.headers.get("etag");
|
|
1011
|
+
const lastModified = response.headers.get("last-modified");
|
|
1012
|
+
if (etag) this.lastEtag = etag;
|
|
1013
|
+
if (lastModified) this.lastModified = lastModified;
|
|
1014
|
+
const body = await response.text();
|
|
1015
|
+
const hash = sha256Hex(body);
|
|
1016
|
+
if (this.lastHash && this.lastHash === hash) {
|
|
1017
|
+
this.callbacks.onUnchanged?.();
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
this.lastHash = hash;
|
|
1021
|
+
this.callbacks.onChanged?.(body, hash);
|
|
1022
|
+
} finally {
|
|
1023
|
+
clearTimeout(timeout);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Dispose the poller and release resources.
|
|
1028
|
+
*/
|
|
1029
|
+
dispose() {
|
|
1030
|
+
this.stop();
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
|
|
863
1034
|
// libs/adapters/src/openapi/openapi.adapter.ts
|
|
864
1035
|
var RESERVED_KEYS2 = ["__proto__", "constructor", "prototype"];
|
|
865
1036
|
function createConsoleLogger(prefix) {
|
|
@@ -877,10 +1048,18 @@ var OpenapiAdapter = class extends DynamicAdapter {
|
|
|
877
1048
|
generator;
|
|
878
1049
|
logger;
|
|
879
1050
|
options;
|
|
1051
|
+
poller = null;
|
|
1052
|
+
updateCallbacks = /* @__PURE__ */ new Set();
|
|
1053
|
+
rebuildChain = Promise.resolve();
|
|
880
1054
|
constructor(options) {
|
|
881
1055
|
super();
|
|
882
1056
|
this.options = options;
|
|
883
1057
|
this.logger = options.logger ?? createConsoleLogger(`openapi:${options.name}`);
|
|
1058
|
+
if (options.polling?.enabled && !("url" in options)) {
|
|
1059
|
+
throw new Error(
|
|
1060
|
+
`[OpenAPI Adapter: ${options.name}] Polling requires URL-based options (use 'url' instead of 'spec').`
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
884
1063
|
}
|
|
885
1064
|
/**
|
|
886
1065
|
* Receive the SDK logger. Called by the SDK before fetch().
|
|
@@ -956,13 +1135,31 @@ Add one of the following to your adapter configuration:
|
|
|
956
1135
|
if (this.options.toolTransforms) {
|
|
957
1136
|
transformedTools = transformedTools.map((tool2) => this.applyToolTransforms(tool2));
|
|
958
1137
|
}
|
|
1138
|
+
const nameMap = /* @__PURE__ */ new Map();
|
|
1139
|
+
for (const tool2 of transformedTools) {
|
|
1140
|
+
const meta = tool2.metadata;
|
|
1141
|
+
const source = `${meta["method"]?.toUpperCase() ?? "?"} ${meta["path"] ?? "?"}`;
|
|
1142
|
+
const existing = nameMap.get(tool2.name);
|
|
1143
|
+
if (existing) {
|
|
1144
|
+
existing.push(source);
|
|
1145
|
+
} else {
|
|
1146
|
+
nameMap.set(tool2.name, [source]);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
for (const [name, sources] of nameMap) {
|
|
1150
|
+
if (sources.length > 1) {
|
|
1151
|
+
throw new Error(
|
|
1152
|
+
`Tool name collision: "${name}" produced by ${sources.length} operations: ${sources.join(", ")}. Rename conflicting operations in your OpenAPI spec or use toolTransforms.perTool to assign unique names.`
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
959
1156
|
if (this.options.inputTransforms) {
|
|
960
1157
|
transformedTools = transformedTools.map((tool2) => this.applyInputTransforms(tool2));
|
|
961
1158
|
}
|
|
962
1159
|
if (this.options.schemaTransforms) {
|
|
963
1160
|
transformedTools = transformedTools.map((tool2) => this.applySchemaTransforms(tool2));
|
|
964
1161
|
}
|
|
965
|
-
const dataTransforms = this.options.dataTransforms
|
|
1162
|
+
const dataTransforms = this.options.dataTransforms;
|
|
966
1163
|
if (this.options.outputSchema || dataTransforms) {
|
|
967
1164
|
transformedTools = await Promise.all(
|
|
968
1165
|
transformedTools.map((tool2) => this.applyOutputSchemaOptions(tool2, dataTransforms))
|
|
@@ -1038,6 +1235,15 @@ ${opDescription}`;
|
|
|
1038
1235
|
description
|
|
1039
1236
|
};
|
|
1040
1237
|
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Look up a value in a perTool map by tool name, using own-property check.
|
|
1240
|
+
* @private
|
|
1241
|
+
*/
|
|
1242
|
+
resolvePerTool(perTool, tool2) {
|
|
1243
|
+
if (!perTool) return void 0;
|
|
1244
|
+
if (Object.prototype.hasOwnProperty.call(perTool, tool2.name)) return perTool[tool2.name];
|
|
1245
|
+
return void 0;
|
|
1246
|
+
}
|
|
1041
1247
|
/**
|
|
1042
1248
|
* Collect tool transforms for a specific tool
|
|
1043
1249
|
* @private
|
|
@@ -1058,8 +1264,9 @@ ${opDescription}`;
|
|
|
1058
1264
|
result.examples = [...opts.global.examples];
|
|
1059
1265
|
}
|
|
1060
1266
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1267
|
+
const perToolMatch = this.resolvePerTool(opts.perTool, tool2);
|
|
1268
|
+
if (perToolMatch) {
|
|
1269
|
+
const perTool = perToolMatch;
|
|
1063
1270
|
if (perTool.name) result.name = perTool.name;
|
|
1064
1271
|
if (perTool.description) result.description = perTool.description;
|
|
1065
1272
|
if (perTool.hideFromDiscovery !== void 0) result.hideFromDiscovery = perTool.hideFromDiscovery;
|
|
@@ -1136,8 +1343,9 @@ ${opDescription}`;
|
|
|
1136
1343
|
if (opts.global) {
|
|
1137
1344
|
transforms.push(...opts.global);
|
|
1138
1345
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1346
|
+
const perToolInputTransforms = this.resolvePerTool(opts.perTool, tool2);
|
|
1347
|
+
if (perToolInputTransforms) {
|
|
1348
|
+
transforms.push(...perToolInputTransforms);
|
|
1141
1349
|
}
|
|
1142
1350
|
if (opts.generator) {
|
|
1143
1351
|
transforms.push(...opts.generator(tool2));
|
|
@@ -1284,8 +1492,9 @@ ${opDescription}`;
|
|
|
1284
1492
|
const generated = opts.generator(tool2);
|
|
1285
1493
|
if (generated) return generated;
|
|
1286
1494
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1495
|
+
const perToolInputSchema = this.resolvePerTool(opts.perTool, tool2);
|
|
1496
|
+
if (perToolInputSchema) {
|
|
1497
|
+
return perToolInputSchema;
|
|
1289
1498
|
}
|
|
1290
1499
|
return opts.global;
|
|
1291
1500
|
}
|
|
@@ -1300,8 +1509,9 @@ ${opDescription}`;
|
|
|
1300
1509
|
const generated = opts.generator(tool2);
|
|
1301
1510
|
if (generated) return generated;
|
|
1302
1511
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1512
|
+
const perToolOutputSchema = this.resolvePerTool(opts.perTool, tool2);
|
|
1513
|
+
if (perToolOutputSchema) {
|
|
1514
|
+
return perToolOutputSchema;
|
|
1305
1515
|
}
|
|
1306
1516
|
return opts.global;
|
|
1307
1517
|
}
|
|
@@ -1403,8 +1613,9 @@ ${this.formatSchemaAsSummary(schema)}`;
|
|
|
1403
1613
|
if (opts.global) {
|
|
1404
1614
|
result = { ...opts.global };
|
|
1405
1615
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1616
|
+
const perToolPre = this.resolvePerTool(opts.perTool, tool2);
|
|
1617
|
+
if (perToolPre) {
|
|
1618
|
+
result = { ...result, ...perToolPre };
|
|
1408
1619
|
}
|
|
1409
1620
|
if (opts.generator) {
|
|
1410
1621
|
const generated = opts.generator(tool2);
|
|
@@ -1425,12 +1636,12 @@ ${this.formatSchemaAsSummary(schema)}`;
|
|
|
1425
1636
|
if (opts.global) {
|
|
1426
1637
|
result = { ...opts.global };
|
|
1427
1638
|
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1639
|
+
const perToolPost = this.resolvePerTool(opts.perTool, tool2);
|
|
1640
|
+
if (perToolPost) {
|
|
1430
1641
|
result = result ? {
|
|
1431
|
-
transform:
|
|
1432
|
-
filter:
|
|
1433
|
-
} :
|
|
1642
|
+
transform: perToolPost.transform,
|
|
1643
|
+
filter: perToolPost.filter ?? result.filter
|
|
1644
|
+
} : perToolPost;
|
|
1434
1645
|
}
|
|
1435
1646
|
if (opts.generator) {
|
|
1436
1647
|
const generated = opts.generator(tool2);
|
|
@@ -1482,6 +1693,66 @@ ${this.formatSchemaAsSummary(schema)}`;
|
|
|
1482
1693
|
}
|
|
1483
1694
|
return schema.type || "any";
|
|
1484
1695
|
}
|
|
1696
|
+
// ============================================================================
|
|
1697
|
+
// Polling Lifecycle
|
|
1698
|
+
// ============================================================================
|
|
1699
|
+
/**
|
|
1700
|
+
* Register a callback for when the adapter's tools are updated via polling.
|
|
1701
|
+
* Returns an unsubscribe function.
|
|
1702
|
+
*/
|
|
1703
|
+
onUpdate(callback) {
|
|
1704
|
+
this.updateCallbacks.add(callback);
|
|
1705
|
+
return () => {
|
|
1706
|
+
this.updateCallbacks.delete(callback);
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Start polling for spec changes.
|
|
1711
|
+
* Creates the poller and on spec change, re-runs fetch() and calls the update callback.
|
|
1712
|
+
* Rebuilds are serialized via a Promise chain to prevent races.
|
|
1713
|
+
*/
|
|
1714
|
+
startPolling() {
|
|
1715
|
+
if (this.poller) return;
|
|
1716
|
+
const polling = this.options.polling;
|
|
1717
|
+
if (!polling?.enabled || !("url" in this.options)) return;
|
|
1718
|
+
this.poller = new OpenApiSpecPoller(this.options.url, polling, {
|
|
1719
|
+
onChanged: (_spec, _hash) => {
|
|
1720
|
+
this.rebuildChain = this.rebuildChain.then(async () => {
|
|
1721
|
+
const previousGenerator = this.generator;
|
|
1722
|
+
try {
|
|
1723
|
+
this.generator = void 0;
|
|
1724
|
+
const response = await this.fetch();
|
|
1725
|
+
this.updateCallbacks.forEach((cb) => cb(response));
|
|
1726
|
+
this.logger.info("OpenAPI spec updated, tools rebuilt");
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
this.generator = previousGenerator;
|
|
1729
|
+
this.logger.error(`Failed to rebuild tools after spec change: ${error.message}`);
|
|
1730
|
+
}
|
|
1731
|
+
});
|
|
1732
|
+
},
|
|
1733
|
+
onError: (error) => {
|
|
1734
|
+
this.logger.warn(`OpenAPI spec poll error: ${error.message}`);
|
|
1735
|
+
},
|
|
1736
|
+
onUnhealthy: (failures) => {
|
|
1737
|
+
this.logger.error(`OpenAPI spec poller unhealthy after ${failures} consecutive failures`);
|
|
1738
|
+
},
|
|
1739
|
+
onRecovered: () => {
|
|
1740
|
+
this.logger.info("OpenAPI spec poller recovered");
|
|
1741
|
+
}
|
|
1742
|
+
});
|
|
1743
|
+
this.poller.start();
|
|
1744
|
+
this.logger.info(`Started polling OpenAPI spec at ${this.options.polling?.intervalMs ?? 6e4}ms intervals`);
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Stop polling for spec changes.
|
|
1748
|
+
*/
|
|
1749
|
+
stopPolling() {
|
|
1750
|
+
if (this.poller) {
|
|
1751
|
+
this.poller.dispose();
|
|
1752
|
+
this.poller = null;
|
|
1753
|
+
this.logger.info("Stopped polling OpenAPI spec");
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1485
1756
|
};
|
|
1486
1757
|
OpenapiAdapter = __decorateClass([
|
|
1487
1758
|
Adapter({
|
|
@@ -1595,6 +1866,8 @@ function removeSecurityFromOperations(spec, operations) {
|
|
|
1595
1866
|
}
|
|
1596
1867
|
export {
|
|
1597
1868
|
FRONTMCP_EXTENSION_KEY,
|
|
1869
|
+
OpenAPIFetchError,
|
|
1870
|
+
OpenApiSpecPoller,
|
|
1598
1871
|
OpenapiAdapter,
|
|
1599
1872
|
forceJwtSecurity,
|
|
1600
1873
|
removeSecurityFromOperations
|