@frontmcp/adapters 0.12.2 → 1.0.0-beta.2
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 +244 -3
- package/esm/openapi/index.mjs +244 -3
- package/esm/package.json +3 -3
- package/index.d.ts.map +1 -1
- package/index.js +251 -8
- package/openapi/index.d.ts +4 -1
- package/openapi/index.d.ts.map +1 -1
- package/openapi/index.js +251 -8
- 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 +18 -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 +3 -3
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().
|
|
@@ -962,7 +1141,7 @@ Add one of the following to your adapter configuration:
|
|
|
962
1141
|
if (this.options.schemaTransforms) {
|
|
963
1142
|
transformedTools = transformedTools.map((tool2) => this.applySchemaTransforms(tool2));
|
|
964
1143
|
}
|
|
965
|
-
const dataTransforms = this.options.dataTransforms
|
|
1144
|
+
const dataTransforms = this.options.dataTransforms;
|
|
966
1145
|
if (this.options.outputSchema || dataTransforms) {
|
|
967
1146
|
transformedTools = await Promise.all(
|
|
968
1147
|
transformedTools.map((tool2) => this.applyOutputSchemaOptions(tool2, dataTransforms))
|
|
@@ -1482,6 +1661,66 @@ ${this.formatSchemaAsSummary(schema)}`;
|
|
|
1482
1661
|
}
|
|
1483
1662
|
return schema.type || "any";
|
|
1484
1663
|
}
|
|
1664
|
+
// ============================================================================
|
|
1665
|
+
// Polling Lifecycle
|
|
1666
|
+
// ============================================================================
|
|
1667
|
+
/**
|
|
1668
|
+
* Register a callback for when the adapter's tools are updated via polling.
|
|
1669
|
+
* Returns an unsubscribe function.
|
|
1670
|
+
*/
|
|
1671
|
+
onUpdate(callback) {
|
|
1672
|
+
this.updateCallbacks.add(callback);
|
|
1673
|
+
return () => {
|
|
1674
|
+
this.updateCallbacks.delete(callback);
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
/**
|
|
1678
|
+
* Start polling for spec changes.
|
|
1679
|
+
* Creates the poller and on spec change, re-runs fetch() and calls the update callback.
|
|
1680
|
+
* Rebuilds are serialized via a Promise chain to prevent races.
|
|
1681
|
+
*/
|
|
1682
|
+
startPolling() {
|
|
1683
|
+
if (this.poller) return;
|
|
1684
|
+
const polling = this.options.polling;
|
|
1685
|
+
if (!polling?.enabled || !("url" in this.options)) return;
|
|
1686
|
+
this.poller = new OpenApiSpecPoller(this.options.url, polling, {
|
|
1687
|
+
onChanged: (_spec, _hash) => {
|
|
1688
|
+
this.rebuildChain = this.rebuildChain.then(async () => {
|
|
1689
|
+
const previousGenerator = this.generator;
|
|
1690
|
+
try {
|
|
1691
|
+
this.generator = void 0;
|
|
1692
|
+
const response = await this.fetch();
|
|
1693
|
+
this.updateCallbacks.forEach((cb) => cb(response));
|
|
1694
|
+
this.logger.info("OpenAPI spec updated, tools rebuilt");
|
|
1695
|
+
} catch (error) {
|
|
1696
|
+
this.generator = previousGenerator;
|
|
1697
|
+
this.logger.error(`Failed to rebuild tools after spec change: ${error.message}`);
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
},
|
|
1701
|
+
onError: (error) => {
|
|
1702
|
+
this.logger.warn(`OpenAPI spec poll error: ${error.message}`);
|
|
1703
|
+
},
|
|
1704
|
+
onUnhealthy: (failures) => {
|
|
1705
|
+
this.logger.error(`OpenAPI spec poller unhealthy after ${failures} consecutive failures`);
|
|
1706
|
+
},
|
|
1707
|
+
onRecovered: () => {
|
|
1708
|
+
this.logger.info("OpenAPI spec poller recovered");
|
|
1709
|
+
}
|
|
1710
|
+
});
|
|
1711
|
+
this.poller.start();
|
|
1712
|
+
this.logger.info(`Started polling OpenAPI spec at ${this.options.polling?.intervalMs ?? 6e4}ms intervals`);
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Stop polling for spec changes.
|
|
1716
|
+
*/
|
|
1717
|
+
stopPolling() {
|
|
1718
|
+
if (this.poller) {
|
|
1719
|
+
this.poller.dispose();
|
|
1720
|
+
this.poller = null;
|
|
1721
|
+
this.logger.info("Stopped polling OpenAPI spec");
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1485
1724
|
};
|
|
1486
1725
|
OpenapiAdapter = __decorateClass([
|
|
1487
1726
|
Adapter({
|
|
@@ -1595,6 +1834,8 @@ function removeSecurityFromOperations(spec, operations) {
|
|
|
1595
1834
|
}
|
|
1596
1835
|
export {
|
|
1597
1836
|
FRONTMCP_EXTENSION_KEY,
|
|
1837
|
+
OpenAPIFetchError,
|
|
1838
|
+
OpenApiSpecPoller,
|
|
1598
1839
|
OpenapiAdapter,
|
|
1599
1840
|
forceJwtSecurity,
|
|
1600
1841
|
removeSecurityFromOperations
|
package/esm/openapi/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().
|
|
@@ -962,7 +1141,7 @@ Add one of the following to your adapter configuration:
|
|
|
962
1141
|
if (this.options.schemaTransforms) {
|
|
963
1142
|
transformedTools = transformedTools.map((tool2) => this.applySchemaTransforms(tool2));
|
|
964
1143
|
}
|
|
965
|
-
const dataTransforms = this.options.dataTransforms
|
|
1144
|
+
const dataTransforms = this.options.dataTransforms;
|
|
966
1145
|
if (this.options.outputSchema || dataTransforms) {
|
|
967
1146
|
transformedTools = await Promise.all(
|
|
968
1147
|
transformedTools.map((tool2) => this.applyOutputSchemaOptions(tool2, dataTransforms))
|
|
@@ -1482,6 +1661,66 @@ ${this.formatSchemaAsSummary(schema)}`;
|
|
|
1482
1661
|
}
|
|
1483
1662
|
return schema.type || "any";
|
|
1484
1663
|
}
|
|
1664
|
+
// ============================================================================
|
|
1665
|
+
// Polling Lifecycle
|
|
1666
|
+
// ============================================================================
|
|
1667
|
+
/**
|
|
1668
|
+
* Register a callback for when the adapter's tools are updated via polling.
|
|
1669
|
+
* Returns an unsubscribe function.
|
|
1670
|
+
*/
|
|
1671
|
+
onUpdate(callback) {
|
|
1672
|
+
this.updateCallbacks.add(callback);
|
|
1673
|
+
return () => {
|
|
1674
|
+
this.updateCallbacks.delete(callback);
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
/**
|
|
1678
|
+
* Start polling for spec changes.
|
|
1679
|
+
* Creates the poller and on spec change, re-runs fetch() and calls the update callback.
|
|
1680
|
+
* Rebuilds are serialized via a Promise chain to prevent races.
|
|
1681
|
+
*/
|
|
1682
|
+
startPolling() {
|
|
1683
|
+
if (this.poller) return;
|
|
1684
|
+
const polling = this.options.polling;
|
|
1685
|
+
if (!polling?.enabled || !("url" in this.options)) return;
|
|
1686
|
+
this.poller = new OpenApiSpecPoller(this.options.url, polling, {
|
|
1687
|
+
onChanged: (_spec, _hash) => {
|
|
1688
|
+
this.rebuildChain = this.rebuildChain.then(async () => {
|
|
1689
|
+
const previousGenerator = this.generator;
|
|
1690
|
+
try {
|
|
1691
|
+
this.generator = void 0;
|
|
1692
|
+
const response = await this.fetch();
|
|
1693
|
+
this.updateCallbacks.forEach((cb) => cb(response));
|
|
1694
|
+
this.logger.info("OpenAPI spec updated, tools rebuilt");
|
|
1695
|
+
} catch (error) {
|
|
1696
|
+
this.generator = previousGenerator;
|
|
1697
|
+
this.logger.error(`Failed to rebuild tools after spec change: ${error.message}`);
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
},
|
|
1701
|
+
onError: (error) => {
|
|
1702
|
+
this.logger.warn(`OpenAPI spec poll error: ${error.message}`);
|
|
1703
|
+
},
|
|
1704
|
+
onUnhealthy: (failures) => {
|
|
1705
|
+
this.logger.error(`OpenAPI spec poller unhealthy after ${failures} consecutive failures`);
|
|
1706
|
+
},
|
|
1707
|
+
onRecovered: () => {
|
|
1708
|
+
this.logger.info("OpenAPI spec poller recovered");
|
|
1709
|
+
}
|
|
1710
|
+
});
|
|
1711
|
+
this.poller.start();
|
|
1712
|
+
this.logger.info(`Started polling OpenAPI spec at ${this.options.polling?.intervalMs ?? 6e4}ms intervals`);
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Stop polling for spec changes.
|
|
1716
|
+
*/
|
|
1717
|
+
stopPolling() {
|
|
1718
|
+
if (this.poller) {
|
|
1719
|
+
this.poller.dispose();
|
|
1720
|
+
this.poller = null;
|
|
1721
|
+
this.logger.info("Stopped polling OpenAPI spec");
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1485
1724
|
};
|
|
1486
1725
|
OpenapiAdapter = __decorateClass([
|
|
1487
1726
|
Adapter({
|
|
@@ -1595,6 +1834,8 @@ function removeSecurityFromOperations(spec, operations) {
|
|
|
1595
1834
|
}
|
|
1596
1835
|
export {
|
|
1597
1836
|
FRONTMCP_EXTENSION_KEY,
|
|
1837
|
+
OpenAPIFetchError,
|
|
1838
|
+
OpenApiSpecPoller,
|
|
1598
1839
|
OpenapiAdapter as default,
|
|
1599
1840
|
forceJwtSecurity,
|
|
1600
1841
|
removeSecurityFromOperations
|
package/esm/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/adapters",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-beta.2",
|
|
4
4
|
"description": "Adapters for the FrontMCP framework",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"homepage": "https://docs.agentfront.dev",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"node": ">=22.0.0"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@frontmcp/utils": "0.
|
|
58
|
-
"@frontmcp/sdk": "0.
|
|
57
|
+
"@frontmcp/utils": "1.0.0-beta.2",
|
|
58
|
+
"@frontmcp/sdk": "1.0.0-beta.2",
|
|
59
59
|
"zod": "^4.0.0",
|
|
60
60
|
"openapi-types": "^12.1.3",
|
|
61
61
|
"mcp-from-openapi": "2.1.2",
|
package/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,WAAW,CAAC;AACtD,cAAc,WAAW,CAAC"}
|