@frontmcp/adapters 0.12.1 → 1.0.0-beta.1
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/index.js
CHANGED
|
@@ -29,6 +29,8 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
29
29
|
var index_exports = {};
|
|
30
30
|
__export(index_exports, {
|
|
31
31
|
FRONTMCP_EXTENSION_KEY: () => FRONTMCP_EXTENSION_KEY,
|
|
32
|
+
OpenAPIFetchError: () => OpenAPIFetchError,
|
|
33
|
+
OpenApiSpecPoller: () => OpenApiSpecPoller,
|
|
32
34
|
OpenapiAdapter: () => OpenapiAdapter,
|
|
33
35
|
forceJwtSecurity: () => forceJwtSecurity,
|
|
34
36
|
removeSecurityFromOperations: () => removeSecurityFromOperations
|
|
@@ -36,16 +38,17 @@ __export(index_exports, {
|
|
|
36
38
|
module.exports = __toCommonJS(index_exports);
|
|
37
39
|
|
|
38
40
|
// libs/adapters/src/openapi/openapi.adapter.ts
|
|
39
|
-
var
|
|
41
|
+
var import_sdk4 = require("@frontmcp/sdk");
|
|
40
42
|
var import_mcp_from_openapi2 = require("mcp-from-openapi");
|
|
41
43
|
|
|
42
44
|
// libs/adapters/src/openapi/openapi.tool.ts
|
|
43
45
|
var import_zod2 = require("zod");
|
|
44
|
-
var
|
|
46
|
+
var import_sdk2 = require("@frontmcp/sdk");
|
|
45
47
|
var import_zod_from_json_schema = require("zod-from-json-schema");
|
|
46
48
|
|
|
47
49
|
// libs/adapters/src/openapi/openapi.utils.ts
|
|
48
50
|
var import_utils = require("@frontmcp/utils");
|
|
51
|
+
var import_sdk = require("@frontmcp/sdk");
|
|
49
52
|
function coerceToString(value, paramName, location) {
|
|
50
53
|
if (value === null || value === void 0) {
|
|
51
54
|
return "";
|
|
@@ -102,8 +105,10 @@ function buildRequest(tool2, input, security, baseUrl) {
|
|
|
102
105
|
case "header": {
|
|
103
106
|
const headerValue = coerceToString(value, mapper.key, "header");
|
|
104
107
|
if (/[\r\n\x00\f\v]/.test(headerValue)) {
|
|
105
|
-
throw new
|
|
106
|
-
`Invalid header value for '${mapper.key}': contains control characters (possible header injection attack)
|
|
108
|
+
throw new import_sdk.PublicMcpError(
|
|
109
|
+
`Invalid header value for '${mapper.key}': contains control characters (possible header injection attack)`,
|
|
110
|
+
"INVALID_HEADER_VALUE",
|
|
111
|
+
400
|
|
107
112
|
);
|
|
108
113
|
}
|
|
109
114
|
headers.set(mapper.key, headerValue);
|
|
@@ -683,7 +688,7 @@ function createOpenApiTool(openapiTool, options, logger) {
|
|
|
683
688
|
if (toolTransform.ui) {
|
|
684
689
|
toolMetadata["ui"] = toolTransform.ui;
|
|
685
690
|
}
|
|
686
|
-
return (0,
|
|
691
|
+
return (0, import_sdk2.tool)(toolMetadata)(async (input, toolCtx) => {
|
|
687
692
|
const ctx = toolCtx.context;
|
|
688
693
|
const transformContext = {
|
|
689
694
|
ctx,
|
|
@@ -886,6 +891,174 @@ function getZodSchemaFromJsonSchema(jsonSchema, toolName, logger) {
|
|
|
886
891
|
}
|
|
887
892
|
}
|
|
888
893
|
|
|
894
|
+
// libs/adapters/src/openapi/openapi-spec-poller.ts
|
|
895
|
+
var import_utils2 = require("@frontmcp/utils");
|
|
896
|
+
|
|
897
|
+
// libs/adapters/src/openapi/openapi.errors.ts
|
|
898
|
+
var import_sdk3 = require("@frontmcp/sdk");
|
|
899
|
+
var OpenAPIFetchError = class extends import_sdk3.InternalMcpError {
|
|
900
|
+
constructor(url, status, statusText) {
|
|
901
|
+
super(`OpenAPI spec fetch failed from "${url}": ${status} ${statusText}`, "OPENAPI_FETCH_FAILED");
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
// libs/adapters/src/openapi/openapi-spec-poller.ts
|
|
906
|
+
var DEFAULT_RETRY = {
|
|
907
|
+
maxRetries: 3,
|
|
908
|
+
initialDelayMs: 1e3,
|
|
909
|
+
maxDelayMs: 1e4,
|
|
910
|
+
backoffMultiplier: 2
|
|
911
|
+
};
|
|
912
|
+
var OpenApiSpecPoller = class {
|
|
913
|
+
url;
|
|
914
|
+
intervalMs;
|
|
915
|
+
fetchTimeoutMs;
|
|
916
|
+
changeDetection;
|
|
917
|
+
retry;
|
|
918
|
+
unhealthyThreshold;
|
|
919
|
+
headers;
|
|
920
|
+
callbacks;
|
|
921
|
+
intervalTimer = null;
|
|
922
|
+
lastHash = null;
|
|
923
|
+
lastEtag = null;
|
|
924
|
+
lastModified = null;
|
|
925
|
+
consecutiveFailures = 0;
|
|
926
|
+
isPolling = false;
|
|
927
|
+
wasUnhealthy = false;
|
|
928
|
+
health = "unknown";
|
|
929
|
+
constructor(url, options = {}, callbacks = {}) {
|
|
930
|
+
this.url = url;
|
|
931
|
+
this.intervalMs = options.intervalMs ?? 6e4;
|
|
932
|
+
this.fetchTimeoutMs = options.fetchTimeoutMs ?? 1e4;
|
|
933
|
+
this.changeDetection = options.changeDetection ?? "auto";
|
|
934
|
+
this.retry = { ...DEFAULT_RETRY, ...options.retry };
|
|
935
|
+
this.unhealthyThreshold = options.unhealthyThreshold ?? 3;
|
|
936
|
+
this.headers = options.headers ?? {};
|
|
937
|
+
this.callbacks = callbacks;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Start polling for changes.
|
|
941
|
+
*/
|
|
942
|
+
start() {
|
|
943
|
+
if (this.intervalTimer) return;
|
|
944
|
+
this.poll().catch(() => {
|
|
945
|
+
});
|
|
946
|
+
this.intervalTimer = setInterval(() => {
|
|
947
|
+
this.poll().catch(() => {
|
|
948
|
+
});
|
|
949
|
+
}, this.intervalMs);
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Stop polling.
|
|
953
|
+
*/
|
|
954
|
+
stop() {
|
|
955
|
+
if (this.intervalTimer) {
|
|
956
|
+
clearInterval(this.intervalTimer);
|
|
957
|
+
this.intervalTimer = null;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Get current stats.
|
|
962
|
+
*/
|
|
963
|
+
getStats() {
|
|
964
|
+
return {
|
|
965
|
+
hash: this.lastHash,
|
|
966
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
967
|
+
health: this.health,
|
|
968
|
+
isRunning: this.intervalTimer !== null
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Perform a single poll cycle.
|
|
973
|
+
*/
|
|
974
|
+
async poll() {
|
|
975
|
+
if (this.isPolling) return;
|
|
976
|
+
this.isPolling = true;
|
|
977
|
+
try {
|
|
978
|
+
await this.fetchWithRetry();
|
|
979
|
+
} finally {
|
|
980
|
+
this.isPolling = false;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
async fetchWithRetry() {
|
|
984
|
+
const { maxRetries, initialDelayMs, maxDelayMs, backoffMultiplier } = this.retry;
|
|
985
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
986
|
+
try {
|
|
987
|
+
await this.doFetch();
|
|
988
|
+
if (this.consecutiveFailures > 0) {
|
|
989
|
+
this.consecutiveFailures = 0;
|
|
990
|
+
if (this.wasUnhealthy) {
|
|
991
|
+
this.wasUnhealthy = false;
|
|
992
|
+
this.health = "healthy";
|
|
993
|
+
this.callbacks.onRecovered?.();
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
this.health = "healthy";
|
|
997
|
+
return;
|
|
998
|
+
} catch (error) {
|
|
999
|
+
if (attempt === maxRetries) {
|
|
1000
|
+
this.consecutiveFailures++;
|
|
1001
|
+
this.callbacks.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
1002
|
+
if (this.consecutiveFailures >= this.unhealthyThreshold && !this.wasUnhealthy) {
|
|
1003
|
+
this.wasUnhealthy = true;
|
|
1004
|
+
this.health = "unhealthy";
|
|
1005
|
+
this.callbacks.onUnhealthy?.(this.consecutiveFailures);
|
|
1006
|
+
}
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const delay = Math.min(initialDelayMs * Math.pow(backoffMultiplier, attempt), maxDelayMs);
|
|
1010
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
async doFetch() {
|
|
1015
|
+
const headers = { ...this.headers };
|
|
1016
|
+
if (this.changeDetection !== "content-hash") {
|
|
1017
|
+
if (this.lastEtag) {
|
|
1018
|
+
headers["If-None-Match"] = this.lastEtag;
|
|
1019
|
+
}
|
|
1020
|
+
if (this.lastModified) {
|
|
1021
|
+
headers["If-Modified-Since"] = this.lastModified;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
const controller = new AbortController();
|
|
1025
|
+
const timeout = setTimeout(() => controller.abort(), this.fetchTimeoutMs);
|
|
1026
|
+
try {
|
|
1027
|
+
const response = await fetch(this.url, {
|
|
1028
|
+
headers,
|
|
1029
|
+
signal: controller.signal
|
|
1030
|
+
});
|
|
1031
|
+
if (response.status === 304) {
|
|
1032
|
+
this.callbacks.onUnchanged?.();
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
if (!response.ok) {
|
|
1036
|
+
throw new OpenAPIFetchError(this.url, response.status, response.statusText);
|
|
1037
|
+
}
|
|
1038
|
+
const etag = response.headers.get("etag");
|
|
1039
|
+
const lastModified = response.headers.get("last-modified");
|
|
1040
|
+
if (etag) this.lastEtag = etag;
|
|
1041
|
+
if (lastModified) this.lastModified = lastModified;
|
|
1042
|
+
const body = await response.text();
|
|
1043
|
+
const hash = (0, import_utils2.sha256Hex)(body);
|
|
1044
|
+
if (this.lastHash && this.lastHash === hash) {
|
|
1045
|
+
this.callbacks.onUnchanged?.();
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
this.lastHash = hash;
|
|
1049
|
+
this.callbacks.onChanged?.(body, hash);
|
|
1050
|
+
} finally {
|
|
1051
|
+
clearTimeout(timeout);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Dispose the poller and release resources.
|
|
1056
|
+
*/
|
|
1057
|
+
dispose() {
|
|
1058
|
+
this.stop();
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
|
|
889
1062
|
// libs/adapters/src/openapi/openapi.adapter.ts
|
|
890
1063
|
var RESERVED_KEYS2 = ["__proto__", "constructor", "prototype"];
|
|
891
1064
|
function createConsoleLogger(prefix) {
|
|
@@ -899,14 +1072,22 @@ function createConsoleLogger(prefix) {
|
|
|
899
1072
|
child: (childPrefix) => createConsoleLogger(`${prefix}:${childPrefix}`)
|
|
900
1073
|
};
|
|
901
1074
|
}
|
|
902
|
-
var OpenapiAdapter = class extends
|
|
1075
|
+
var OpenapiAdapter = class extends import_sdk4.DynamicAdapter {
|
|
903
1076
|
generator;
|
|
904
1077
|
logger;
|
|
905
1078
|
options;
|
|
1079
|
+
poller = null;
|
|
1080
|
+
updateCallbacks = /* @__PURE__ */ new Set();
|
|
1081
|
+
rebuildChain = Promise.resolve();
|
|
906
1082
|
constructor(options) {
|
|
907
1083
|
super();
|
|
908
1084
|
this.options = options;
|
|
909
1085
|
this.logger = options.logger ?? createConsoleLogger(`openapi:${options.name}`);
|
|
1086
|
+
if (options.polling?.enabled && !("url" in options)) {
|
|
1087
|
+
throw new Error(
|
|
1088
|
+
`[OpenAPI Adapter: ${options.name}] Polling requires URL-based options (use 'url' instead of 'spec').`
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
910
1091
|
}
|
|
911
1092
|
/**
|
|
912
1093
|
* Receive the SDK logger. Called by the SDK before fetch().
|
|
@@ -988,7 +1169,7 @@ Add one of the following to your adapter configuration:
|
|
|
988
1169
|
if (this.options.schemaTransforms) {
|
|
989
1170
|
transformedTools = transformedTools.map((tool2) => this.applySchemaTransforms(tool2));
|
|
990
1171
|
}
|
|
991
|
-
const dataTransforms = this.options.dataTransforms
|
|
1172
|
+
const dataTransforms = this.options.dataTransforms;
|
|
992
1173
|
if (this.options.outputSchema || dataTransforms) {
|
|
993
1174
|
transformedTools = await Promise.all(
|
|
994
1175
|
transformedTools.map((tool2) => this.applyOutputSchemaOptions(tool2, dataTransforms))
|
|
@@ -1508,9 +1689,69 @@ ${this.formatSchemaAsSummary(schema)}`;
|
|
|
1508
1689
|
}
|
|
1509
1690
|
return schema.type || "any";
|
|
1510
1691
|
}
|
|
1692
|
+
// ============================================================================
|
|
1693
|
+
// Polling Lifecycle
|
|
1694
|
+
// ============================================================================
|
|
1695
|
+
/**
|
|
1696
|
+
* Register a callback for when the adapter's tools are updated via polling.
|
|
1697
|
+
* Returns an unsubscribe function.
|
|
1698
|
+
*/
|
|
1699
|
+
onUpdate(callback) {
|
|
1700
|
+
this.updateCallbacks.add(callback);
|
|
1701
|
+
return () => {
|
|
1702
|
+
this.updateCallbacks.delete(callback);
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Start polling for spec changes.
|
|
1707
|
+
* Creates the poller and on spec change, re-runs fetch() and calls the update callback.
|
|
1708
|
+
* Rebuilds are serialized via a Promise chain to prevent races.
|
|
1709
|
+
*/
|
|
1710
|
+
startPolling() {
|
|
1711
|
+
if (this.poller) return;
|
|
1712
|
+
const polling = this.options.polling;
|
|
1713
|
+
if (!polling?.enabled || !("url" in this.options)) return;
|
|
1714
|
+
this.poller = new OpenApiSpecPoller(this.options.url, polling, {
|
|
1715
|
+
onChanged: (_spec, _hash) => {
|
|
1716
|
+
this.rebuildChain = this.rebuildChain.then(async () => {
|
|
1717
|
+
const previousGenerator = this.generator;
|
|
1718
|
+
try {
|
|
1719
|
+
this.generator = void 0;
|
|
1720
|
+
const response = await this.fetch();
|
|
1721
|
+
this.updateCallbacks.forEach((cb) => cb(response));
|
|
1722
|
+
this.logger.info("OpenAPI spec updated, tools rebuilt");
|
|
1723
|
+
} catch (error) {
|
|
1724
|
+
this.generator = previousGenerator;
|
|
1725
|
+
this.logger.error(`Failed to rebuild tools after spec change: ${error.message}`);
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
},
|
|
1729
|
+
onError: (error) => {
|
|
1730
|
+
this.logger.warn(`OpenAPI spec poll error: ${error.message}`);
|
|
1731
|
+
},
|
|
1732
|
+
onUnhealthy: (failures) => {
|
|
1733
|
+
this.logger.error(`OpenAPI spec poller unhealthy after ${failures} consecutive failures`);
|
|
1734
|
+
},
|
|
1735
|
+
onRecovered: () => {
|
|
1736
|
+
this.logger.info("OpenAPI spec poller recovered");
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
this.poller.start();
|
|
1740
|
+
this.logger.info(`Started polling OpenAPI spec at ${this.options.polling?.intervalMs ?? 6e4}ms intervals`);
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Stop polling for spec changes.
|
|
1744
|
+
*/
|
|
1745
|
+
stopPolling() {
|
|
1746
|
+
if (this.poller) {
|
|
1747
|
+
this.poller.dispose();
|
|
1748
|
+
this.poller = null;
|
|
1749
|
+
this.logger.info("Stopped polling OpenAPI spec");
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1511
1752
|
};
|
|
1512
1753
|
OpenapiAdapter = __decorateClass([
|
|
1513
|
-
(0,
|
|
1754
|
+
(0, import_sdk4.Adapter)({
|
|
1514
1755
|
name: "openapi",
|
|
1515
1756
|
description: "OpenAPI adapter for FrontMCP - Automatically generates MCP tools from OpenAPI specifications"
|
|
1516
1757
|
})
|
|
@@ -1622,6 +1863,8 @@ function removeSecurityFromOperations(spec, operations) {
|
|
|
1622
1863
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1623
1864
|
0 && (module.exports = {
|
|
1624
1865
|
FRONTMCP_EXTENSION_KEY,
|
|
1866
|
+
OpenAPIFetchError,
|
|
1867
|
+
OpenApiSpecPoller,
|
|
1625
1868
|
OpenapiAdapter,
|
|
1626
1869
|
forceJwtSecurity,
|
|
1627
1870
|
removeSecurityFromOperations
|
package/openapi/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { default } from './openapi.adapter';
|
|
2
2
|
export * from './openapi.types';
|
|
3
3
|
export * from './openapi.spec-utils';
|
|
4
|
-
export { OpenApiResponse } from './openapi.utils';
|
|
4
|
+
export type { OpenApiResponse } from './openapi.utils';
|
|
5
|
+
export { OpenApiSpecPoller } from './openapi-spec-poller';
|
|
6
|
+
export * from './openapi-spec-poller.types';
|
|
7
|
+
export { OpenAPIFetchError } from './openapi.errors';
|
|
5
8
|
//# sourceMappingURL=index.d.ts.map
|
package/openapi/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/openapi/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/openapi/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,cAAc,6BAA6B,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC"}
|
package/openapi/index.js
CHANGED
|
@@ -29,6 +29,8 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
29
29
|
var openapi_exports = {};
|
|
30
30
|
__export(openapi_exports, {
|
|
31
31
|
FRONTMCP_EXTENSION_KEY: () => FRONTMCP_EXTENSION_KEY,
|
|
32
|
+
OpenAPIFetchError: () => OpenAPIFetchError,
|
|
33
|
+
OpenApiSpecPoller: () => OpenApiSpecPoller,
|
|
32
34
|
default: () => OpenapiAdapter,
|
|
33
35
|
forceJwtSecurity: () => forceJwtSecurity,
|
|
34
36
|
removeSecurityFromOperations: () => removeSecurityFromOperations
|
|
@@ -36,16 +38,17 @@ __export(openapi_exports, {
|
|
|
36
38
|
module.exports = __toCommonJS(openapi_exports);
|
|
37
39
|
|
|
38
40
|
// libs/adapters/src/openapi/openapi.adapter.ts
|
|
39
|
-
var
|
|
41
|
+
var import_sdk4 = require("@frontmcp/sdk");
|
|
40
42
|
var import_mcp_from_openapi2 = require("mcp-from-openapi");
|
|
41
43
|
|
|
42
44
|
// libs/adapters/src/openapi/openapi.tool.ts
|
|
43
45
|
var import_zod2 = require("zod");
|
|
44
|
-
var
|
|
46
|
+
var import_sdk2 = require("@frontmcp/sdk");
|
|
45
47
|
var import_zod_from_json_schema = require("zod-from-json-schema");
|
|
46
48
|
|
|
47
49
|
// libs/adapters/src/openapi/openapi.utils.ts
|
|
48
50
|
var import_utils = require("@frontmcp/utils");
|
|
51
|
+
var import_sdk = require("@frontmcp/sdk");
|
|
49
52
|
function coerceToString(value, paramName, location) {
|
|
50
53
|
if (value === null || value === void 0) {
|
|
51
54
|
return "";
|
|
@@ -102,8 +105,10 @@ function buildRequest(tool2, input, security, baseUrl) {
|
|
|
102
105
|
case "header": {
|
|
103
106
|
const headerValue = coerceToString(value, mapper.key, "header");
|
|
104
107
|
if (/[\r\n\x00\f\v]/.test(headerValue)) {
|
|
105
|
-
throw new
|
|
106
|
-
`Invalid header value for '${mapper.key}': contains control characters (possible header injection attack)
|
|
108
|
+
throw new import_sdk.PublicMcpError(
|
|
109
|
+
`Invalid header value for '${mapper.key}': contains control characters (possible header injection attack)`,
|
|
110
|
+
"INVALID_HEADER_VALUE",
|
|
111
|
+
400
|
|
107
112
|
);
|
|
108
113
|
}
|
|
109
114
|
headers.set(mapper.key, headerValue);
|
|
@@ -683,7 +688,7 @@ function createOpenApiTool(openapiTool, options, logger) {
|
|
|
683
688
|
if (toolTransform.ui) {
|
|
684
689
|
toolMetadata["ui"] = toolTransform.ui;
|
|
685
690
|
}
|
|
686
|
-
return (0,
|
|
691
|
+
return (0, import_sdk2.tool)(toolMetadata)(async (input, toolCtx) => {
|
|
687
692
|
const ctx = toolCtx.context;
|
|
688
693
|
const transformContext = {
|
|
689
694
|
ctx,
|
|
@@ -886,6 +891,174 @@ function getZodSchemaFromJsonSchema(jsonSchema, toolName, logger) {
|
|
|
886
891
|
}
|
|
887
892
|
}
|
|
888
893
|
|
|
894
|
+
// libs/adapters/src/openapi/openapi-spec-poller.ts
|
|
895
|
+
var import_utils2 = require("@frontmcp/utils");
|
|
896
|
+
|
|
897
|
+
// libs/adapters/src/openapi/openapi.errors.ts
|
|
898
|
+
var import_sdk3 = require("@frontmcp/sdk");
|
|
899
|
+
var OpenAPIFetchError = class extends import_sdk3.InternalMcpError {
|
|
900
|
+
constructor(url, status, statusText) {
|
|
901
|
+
super(`OpenAPI spec fetch failed from "${url}": ${status} ${statusText}`, "OPENAPI_FETCH_FAILED");
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
// libs/adapters/src/openapi/openapi-spec-poller.ts
|
|
906
|
+
var DEFAULT_RETRY = {
|
|
907
|
+
maxRetries: 3,
|
|
908
|
+
initialDelayMs: 1e3,
|
|
909
|
+
maxDelayMs: 1e4,
|
|
910
|
+
backoffMultiplier: 2
|
|
911
|
+
};
|
|
912
|
+
var OpenApiSpecPoller = class {
|
|
913
|
+
url;
|
|
914
|
+
intervalMs;
|
|
915
|
+
fetchTimeoutMs;
|
|
916
|
+
changeDetection;
|
|
917
|
+
retry;
|
|
918
|
+
unhealthyThreshold;
|
|
919
|
+
headers;
|
|
920
|
+
callbacks;
|
|
921
|
+
intervalTimer = null;
|
|
922
|
+
lastHash = null;
|
|
923
|
+
lastEtag = null;
|
|
924
|
+
lastModified = null;
|
|
925
|
+
consecutiveFailures = 0;
|
|
926
|
+
isPolling = false;
|
|
927
|
+
wasUnhealthy = false;
|
|
928
|
+
health = "unknown";
|
|
929
|
+
constructor(url, options = {}, callbacks = {}) {
|
|
930
|
+
this.url = url;
|
|
931
|
+
this.intervalMs = options.intervalMs ?? 6e4;
|
|
932
|
+
this.fetchTimeoutMs = options.fetchTimeoutMs ?? 1e4;
|
|
933
|
+
this.changeDetection = options.changeDetection ?? "auto";
|
|
934
|
+
this.retry = { ...DEFAULT_RETRY, ...options.retry };
|
|
935
|
+
this.unhealthyThreshold = options.unhealthyThreshold ?? 3;
|
|
936
|
+
this.headers = options.headers ?? {};
|
|
937
|
+
this.callbacks = callbacks;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Start polling for changes.
|
|
941
|
+
*/
|
|
942
|
+
start() {
|
|
943
|
+
if (this.intervalTimer) return;
|
|
944
|
+
this.poll().catch(() => {
|
|
945
|
+
});
|
|
946
|
+
this.intervalTimer = setInterval(() => {
|
|
947
|
+
this.poll().catch(() => {
|
|
948
|
+
});
|
|
949
|
+
}, this.intervalMs);
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Stop polling.
|
|
953
|
+
*/
|
|
954
|
+
stop() {
|
|
955
|
+
if (this.intervalTimer) {
|
|
956
|
+
clearInterval(this.intervalTimer);
|
|
957
|
+
this.intervalTimer = null;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Get current stats.
|
|
962
|
+
*/
|
|
963
|
+
getStats() {
|
|
964
|
+
return {
|
|
965
|
+
hash: this.lastHash,
|
|
966
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
967
|
+
health: this.health,
|
|
968
|
+
isRunning: this.intervalTimer !== null
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Perform a single poll cycle.
|
|
973
|
+
*/
|
|
974
|
+
async poll() {
|
|
975
|
+
if (this.isPolling) return;
|
|
976
|
+
this.isPolling = true;
|
|
977
|
+
try {
|
|
978
|
+
await this.fetchWithRetry();
|
|
979
|
+
} finally {
|
|
980
|
+
this.isPolling = false;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
async fetchWithRetry() {
|
|
984
|
+
const { maxRetries, initialDelayMs, maxDelayMs, backoffMultiplier } = this.retry;
|
|
985
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
986
|
+
try {
|
|
987
|
+
await this.doFetch();
|
|
988
|
+
if (this.consecutiveFailures > 0) {
|
|
989
|
+
this.consecutiveFailures = 0;
|
|
990
|
+
if (this.wasUnhealthy) {
|
|
991
|
+
this.wasUnhealthy = false;
|
|
992
|
+
this.health = "healthy";
|
|
993
|
+
this.callbacks.onRecovered?.();
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
this.health = "healthy";
|
|
997
|
+
return;
|
|
998
|
+
} catch (error) {
|
|
999
|
+
if (attempt === maxRetries) {
|
|
1000
|
+
this.consecutiveFailures++;
|
|
1001
|
+
this.callbacks.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
1002
|
+
if (this.consecutiveFailures >= this.unhealthyThreshold && !this.wasUnhealthy) {
|
|
1003
|
+
this.wasUnhealthy = true;
|
|
1004
|
+
this.health = "unhealthy";
|
|
1005
|
+
this.callbacks.onUnhealthy?.(this.consecutiveFailures);
|
|
1006
|
+
}
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const delay = Math.min(initialDelayMs * Math.pow(backoffMultiplier, attempt), maxDelayMs);
|
|
1010
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
async doFetch() {
|
|
1015
|
+
const headers = { ...this.headers };
|
|
1016
|
+
if (this.changeDetection !== "content-hash") {
|
|
1017
|
+
if (this.lastEtag) {
|
|
1018
|
+
headers["If-None-Match"] = this.lastEtag;
|
|
1019
|
+
}
|
|
1020
|
+
if (this.lastModified) {
|
|
1021
|
+
headers["If-Modified-Since"] = this.lastModified;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
const controller = new AbortController();
|
|
1025
|
+
const timeout = setTimeout(() => controller.abort(), this.fetchTimeoutMs);
|
|
1026
|
+
try {
|
|
1027
|
+
const response = await fetch(this.url, {
|
|
1028
|
+
headers,
|
|
1029
|
+
signal: controller.signal
|
|
1030
|
+
});
|
|
1031
|
+
if (response.status === 304) {
|
|
1032
|
+
this.callbacks.onUnchanged?.();
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
if (!response.ok) {
|
|
1036
|
+
throw new OpenAPIFetchError(this.url, response.status, response.statusText);
|
|
1037
|
+
}
|
|
1038
|
+
const etag = response.headers.get("etag");
|
|
1039
|
+
const lastModified = response.headers.get("last-modified");
|
|
1040
|
+
if (etag) this.lastEtag = etag;
|
|
1041
|
+
if (lastModified) this.lastModified = lastModified;
|
|
1042
|
+
const body = await response.text();
|
|
1043
|
+
const hash = (0, import_utils2.sha256Hex)(body);
|
|
1044
|
+
if (this.lastHash && this.lastHash === hash) {
|
|
1045
|
+
this.callbacks.onUnchanged?.();
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
this.lastHash = hash;
|
|
1049
|
+
this.callbacks.onChanged?.(body, hash);
|
|
1050
|
+
} finally {
|
|
1051
|
+
clearTimeout(timeout);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Dispose the poller and release resources.
|
|
1056
|
+
*/
|
|
1057
|
+
dispose() {
|
|
1058
|
+
this.stop();
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
|
|
889
1062
|
// libs/adapters/src/openapi/openapi.adapter.ts
|
|
890
1063
|
var RESERVED_KEYS2 = ["__proto__", "constructor", "prototype"];
|
|
891
1064
|
function createConsoleLogger(prefix) {
|
|
@@ -899,14 +1072,22 @@ function createConsoleLogger(prefix) {
|
|
|
899
1072
|
child: (childPrefix) => createConsoleLogger(`${prefix}:${childPrefix}`)
|
|
900
1073
|
};
|
|
901
1074
|
}
|
|
902
|
-
var OpenapiAdapter = class extends
|
|
1075
|
+
var OpenapiAdapter = class extends import_sdk4.DynamicAdapter {
|
|
903
1076
|
generator;
|
|
904
1077
|
logger;
|
|
905
1078
|
options;
|
|
1079
|
+
poller = null;
|
|
1080
|
+
updateCallbacks = /* @__PURE__ */ new Set();
|
|
1081
|
+
rebuildChain = Promise.resolve();
|
|
906
1082
|
constructor(options) {
|
|
907
1083
|
super();
|
|
908
1084
|
this.options = options;
|
|
909
1085
|
this.logger = options.logger ?? createConsoleLogger(`openapi:${options.name}`);
|
|
1086
|
+
if (options.polling?.enabled && !("url" in options)) {
|
|
1087
|
+
throw new Error(
|
|
1088
|
+
`[OpenAPI Adapter: ${options.name}] Polling requires URL-based options (use 'url' instead of 'spec').`
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
910
1091
|
}
|
|
911
1092
|
/**
|
|
912
1093
|
* Receive the SDK logger. Called by the SDK before fetch().
|
|
@@ -988,7 +1169,7 @@ Add one of the following to your adapter configuration:
|
|
|
988
1169
|
if (this.options.schemaTransforms) {
|
|
989
1170
|
transformedTools = transformedTools.map((tool2) => this.applySchemaTransforms(tool2));
|
|
990
1171
|
}
|
|
991
|
-
const dataTransforms = this.options.dataTransforms
|
|
1172
|
+
const dataTransforms = this.options.dataTransforms;
|
|
992
1173
|
if (this.options.outputSchema || dataTransforms) {
|
|
993
1174
|
transformedTools = await Promise.all(
|
|
994
1175
|
transformedTools.map((tool2) => this.applyOutputSchemaOptions(tool2, dataTransforms))
|
|
@@ -1508,9 +1689,69 @@ ${this.formatSchemaAsSummary(schema)}`;
|
|
|
1508
1689
|
}
|
|
1509
1690
|
return schema.type || "any";
|
|
1510
1691
|
}
|
|
1692
|
+
// ============================================================================
|
|
1693
|
+
// Polling Lifecycle
|
|
1694
|
+
// ============================================================================
|
|
1695
|
+
/**
|
|
1696
|
+
* Register a callback for when the adapter's tools are updated via polling.
|
|
1697
|
+
* Returns an unsubscribe function.
|
|
1698
|
+
*/
|
|
1699
|
+
onUpdate(callback) {
|
|
1700
|
+
this.updateCallbacks.add(callback);
|
|
1701
|
+
return () => {
|
|
1702
|
+
this.updateCallbacks.delete(callback);
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Start polling for spec changes.
|
|
1707
|
+
* Creates the poller and on spec change, re-runs fetch() and calls the update callback.
|
|
1708
|
+
* Rebuilds are serialized via a Promise chain to prevent races.
|
|
1709
|
+
*/
|
|
1710
|
+
startPolling() {
|
|
1711
|
+
if (this.poller) return;
|
|
1712
|
+
const polling = this.options.polling;
|
|
1713
|
+
if (!polling?.enabled || !("url" in this.options)) return;
|
|
1714
|
+
this.poller = new OpenApiSpecPoller(this.options.url, polling, {
|
|
1715
|
+
onChanged: (_spec, _hash) => {
|
|
1716
|
+
this.rebuildChain = this.rebuildChain.then(async () => {
|
|
1717
|
+
const previousGenerator = this.generator;
|
|
1718
|
+
try {
|
|
1719
|
+
this.generator = void 0;
|
|
1720
|
+
const response = await this.fetch();
|
|
1721
|
+
this.updateCallbacks.forEach((cb) => cb(response));
|
|
1722
|
+
this.logger.info("OpenAPI spec updated, tools rebuilt");
|
|
1723
|
+
} catch (error) {
|
|
1724
|
+
this.generator = previousGenerator;
|
|
1725
|
+
this.logger.error(`Failed to rebuild tools after spec change: ${error.message}`);
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
},
|
|
1729
|
+
onError: (error) => {
|
|
1730
|
+
this.logger.warn(`OpenAPI spec poll error: ${error.message}`);
|
|
1731
|
+
},
|
|
1732
|
+
onUnhealthy: (failures) => {
|
|
1733
|
+
this.logger.error(`OpenAPI spec poller unhealthy after ${failures} consecutive failures`);
|
|
1734
|
+
},
|
|
1735
|
+
onRecovered: () => {
|
|
1736
|
+
this.logger.info("OpenAPI spec poller recovered");
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
this.poller.start();
|
|
1740
|
+
this.logger.info(`Started polling OpenAPI spec at ${this.options.polling?.intervalMs ?? 6e4}ms intervals`);
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Stop polling for spec changes.
|
|
1744
|
+
*/
|
|
1745
|
+
stopPolling() {
|
|
1746
|
+
if (this.poller) {
|
|
1747
|
+
this.poller.dispose();
|
|
1748
|
+
this.poller = null;
|
|
1749
|
+
this.logger.info("Stopped polling OpenAPI spec");
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1511
1752
|
};
|
|
1512
1753
|
OpenapiAdapter = __decorateClass([
|
|
1513
|
-
(0,
|
|
1754
|
+
(0, import_sdk4.Adapter)({
|
|
1514
1755
|
name: "openapi",
|
|
1515
1756
|
description: "OpenAPI adapter for FrontMCP - Automatically generates MCP tools from OpenAPI specifications"
|
|
1516
1757
|
})
|
|
@@ -1622,6 +1863,8 @@ function removeSecurityFromOperations(spec, operations) {
|
|
|
1622
1863
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1623
1864
|
0 && (module.exports = {
|
|
1624
1865
|
FRONTMCP_EXTENSION_KEY,
|
|
1866
|
+
OpenAPIFetchError,
|
|
1867
|
+
OpenApiSpecPoller,
|
|
1625
1868
|
forceJwtSecurity,
|
|
1626
1869
|
removeSecurityFromOperations
|
|
1627
1870
|
});
|