@frontmcp/adapters 1.4.1 → 1.5.0
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 +21 -3
- package/esm/openapi/index.mjs +21 -3
- package/esm/package.json +7 -7
- package/esm/skills/index.mjs +258 -20
- package/index.js +21 -3
- package/openapi/index.js +21 -3
- package/openapi/openapi.adapter.d.ts +12 -0
- package/openapi/openapi.adapter.d.ts.map +1 -1
- package/openapi/openapi.types.d.ts +8 -1
- package/openapi/openapi.types.d.ts.map +1 -1
- package/package.json +7 -7
- package/skills/audit/audit-config.d.ts +1 -1
- package/skills/audit/audit-writer.d.ts +2 -1
- package/skills/audit/audit-writer.d.ts.map +1 -1
- package/skills/bundle/bundle-diff.d.ts.map +1 -1
- package/skills/bundle/bundle.schema.d.ts +1 -0
- package/skills/bundle/bundle.schema.d.ts.map +1 -1
- package/skills/bundle/bundle.types.d.ts +12 -3
- package/skills/bundle/bundle.types.d.ts.map +1 -1
- package/skills/deploy/manifest-to-config.d.ts +61 -0
- package/skills/deploy/manifest-to-config.d.ts.map +1 -0
- package/skills/index.d.ts +4 -2
- package/skills/index.d.ts.map +1 -1
- package/skills/index.js +261 -20
- package/skills/openapi/compile-skilled-bundle.d.ts +73 -0
- package/skills/openapi/compile-skilled-bundle.d.ts.map +1 -0
- package/skills/source-options.d.ts +8 -0
- package/skills/source-options.d.ts.map +1 -1
- package/skills/sources/index.d.ts +5 -4
- package/skills/sources/index.d.ts.map +1 -1
- package/skills/sources/inline.source.d.ts +16 -0
- package/skills/sources/inline.source.d.ts.map +1 -0
- package/skills/sources/npm.source.d.ts.map +1 -1
- package/skills/sources/saas-pull.source.d.ts +12 -2
- package/skills/sources/saas-pull.source.d.ts.map +1 -1
- package/skills/sources/skill-bundle-source.interface.d.ts +41 -0
- package/skills/sources/skill-bundle-source.interface.d.ts.map +1 -1
package/esm/index.mjs
CHANGED
|
@@ -1178,7 +1178,22 @@ Add one of the following to your adapter configuration:
|
|
|
1178
1178
|
* Initialize the OpenAPI tool generator from URL or spec
|
|
1179
1179
|
* @private
|
|
1180
1180
|
*/
|
|
1181
|
+
/**
|
|
1182
|
+
* Resolve the `$ref` resolution policy with FrontMCP's secure defaults.
|
|
1183
|
+
*
|
|
1184
|
+
* SECURITY (GHSA-65h7-9wrw-629c): external `$ref` resolution is **disabled by
|
|
1185
|
+
* default** — an untrusted spec can otherwise smuggle SSRF via external refs.
|
|
1186
|
+
* Internal-only (`#/...`) refs and inline `spec:` are unaffected. Developers
|
|
1187
|
+
* who genuinely need external refs opt back in by setting
|
|
1188
|
+
* `loadOptions.refResolution` explicitly (and then own its allow/deny lists;
|
|
1189
|
+
* `mcp-from-openapi` ≥ 2.5.0 DNS-resolves and re-validates redirects).
|
|
1190
|
+
* @private
|
|
1191
|
+
*/
|
|
1192
|
+
resolveRefResolution() {
|
|
1193
|
+
return this.options.loadOptions?.refResolution ?? { allowedProtocols: [] };
|
|
1194
|
+
}
|
|
1181
1195
|
async initializeGenerator() {
|
|
1196
|
+
const refResolution = this.resolveRefResolution();
|
|
1182
1197
|
if ("url" in this.options) {
|
|
1183
1198
|
return await OpenAPIToolGenerator.fromURL(this.options.url, {
|
|
1184
1199
|
baseUrl: this.options.baseUrl,
|
|
@@ -1186,15 +1201,18 @@ Add one of the following to your adapter configuration:
|
|
|
1186
1201
|
dereference: this.options.loadOptions?.dereference ?? true,
|
|
1187
1202
|
headers: this.options.loadOptions?.headers,
|
|
1188
1203
|
timeout: this.options.loadOptions?.timeout,
|
|
1189
|
-
|
|
1190
|
-
|
|
1204
|
+
// SECURITY: do not follow spec-URL redirects by default — a 3xx to an
|
|
1205
|
+
// internal target would otherwise be fetched. With mcp-from-openapi
|
|
1206
|
+
// ≥ 2.5.0 each hop is re-validated; opt in with loadOptions.followRedirects.
|
|
1207
|
+
followRedirects: this.options.loadOptions?.followRedirects ?? false,
|
|
1208
|
+
refResolution
|
|
1191
1209
|
});
|
|
1192
1210
|
} else if ("spec" in this.options) {
|
|
1193
1211
|
return await OpenAPIToolGenerator.fromJSON(this.options.spec, {
|
|
1194
1212
|
baseUrl: this.options.baseUrl,
|
|
1195
1213
|
validate: this.options.loadOptions?.validate ?? true,
|
|
1196
1214
|
dereference: this.options.loadOptions?.dereference ?? true,
|
|
1197
|
-
refResolution
|
|
1215
|
+
refResolution
|
|
1198
1216
|
});
|
|
1199
1217
|
} else {
|
|
1200
1218
|
throw new Error("Either url or spec must be provided in OpenApiAdapterOptions");
|
package/esm/openapi/index.mjs
CHANGED
|
@@ -1178,7 +1178,22 @@ Add one of the following to your adapter configuration:
|
|
|
1178
1178
|
* Initialize the OpenAPI tool generator from URL or spec
|
|
1179
1179
|
* @private
|
|
1180
1180
|
*/
|
|
1181
|
+
/**
|
|
1182
|
+
* Resolve the `$ref` resolution policy with FrontMCP's secure defaults.
|
|
1183
|
+
*
|
|
1184
|
+
* SECURITY (GHSA-65h7-9wrw-629c): external `$ref` resolution is **disabled by
|
|
1185
|
+
* default** — an untrusted spec can otherwise smuggle SSRF via external refs.
|
|
1186
|
+
* Internal-only (`#/...`) refs and inline `spec:` are unaffected. Developers
|
|
1187
|
+
* who genuinely need external refs opt back in by setting
|
|
1188
|
+
* `loadOptions.refResolution` explicitly (and then own its allow/deny lists;
|
|
1189
|
+
* `mcp-from-openapi` ≥ 2.5.0 DNS-resolves and re-validates redirects).
|
|
1190
|
+
* @private
|
|
1191
|
+
*/
|
|
1192
|
+
resolveRefResolution() {
|
|
1193
|
+
return this.options.loadOptions?.refResolution ?? { allowedProtocols: [] };
|
|
1194
|
+
}
|
|
1181
1195
|
async initializeGenerator() {
|
|
1196
|
+
const refResolution = this.resolveRefResolution();
|
|
1182
1197
|
if ("url" in this.options) {
|
|
1183
1198
|
return await OpenAPIToolGenerator.fromURL(this.options.url, {
|
|
1184
1199
|
baseUrl: this.options.baseUrl,
|
|
@@ -1186,15 +1201,18 @@ Add one of the following to your adapter configuration:
|
|
|
1186
1201
|
dereference: this.options.loadOptions?.dereference ?? true,
|
|
1187
1202
|
headers: this.options.loadOptions?.headers,
|
|
1188
1203
|
timeout: this.options.loadOptions?.timeout,
|
|
1189
|
-
|
|
1190
|
-
|
|
1204
|
+
// SECURITY: do not follow spec-URL redirects by default — a 3xx to an
|
|
1205
|
+
// internal target would otherwise be fetched. With mcp-from-openapi
|
|
1206
|
+
// ≥ 2.5.0 each hop is re-validated; opt in with loadOptions.followRedirects.
|
|
1207
|
+
followRedirects: this.options.loadOptions?.followRedirects ?? false,
|
|
1208
|
+
refResolution
|
|
1191
1209
|
});
|
|
1192
1210
|
} else if ("spec" in this.options) {
|
|
1193
1211
|
return await OpenAPIToolGenerator.fromJSON(this.options.spec, {
|
|
1194
1212
|
baseUrl: this.options.baseUrl,
|
|
1195
1213
|
validate: this.options.loadOptions?.validate ?? true,
|
|
1196
1214
|
dereference: this.options.loadOptions?.dereference ?? true,
|
|
1197
|
-
refResolution
|
|
1215
|
+
refResolution
|
|
1198
1216
|
});
|
|
1199
1217
|
} else {
|
|
1200
1218
|
throw new Error("Either url or spec must be provided in OpenApiAdapterOptions");
|
package/esm/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/adapters",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
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.
|
|
68
|
-
"@frontmcp/di": "1.
|
|
69
|
-
"@frontmcp/sdk": "1.
|
|
70
|
-
"@frontmcp/utils": "1.
|
|
67
|
+
"@frontmcp/auth": "1.5.0",
|
|
68
|
+
"@frontmcp/di": "1.5.0",
|
|
69
|
+
"@frontmcp/sdk": "1.5.0",
|
|
70
|
+
"@frontmcp/utils": "1.5.0",
|
|
71
71
|
"js-yaml": "^4.1.0",
|
|
72
|
-
"mcp-from-openapi": "2.
|
|
72
|
+
"mcp-from-openapi": "2.5.0",
|
|
73
73
|
"openapi-types": "^12.1.3"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
|
-
"@frontmcp/lazy-zod": "1.
|
|
76
|
+
"@frontmcp/lazy-zod": "1.5.0"
|
|
77
77
|
}
|
|
78
78
|
}
|
package/esm/skills/index.mjs
CHANGED
|
@@ -73,6 +73,9 @@ var operationDescriptorSchema = z.object({
|
|
|
73
73
|
mapper: z.array(parameterMapperSchema).max(256),
|
|
74
74
|
authBindingRef: z.string().min(1).max(128),
|
|
75
75
|
requiredAuthorities: authoritiesPolicySchema.optional(),
|
|
76
|
+
// Explicit opt-in that a policy-less op is intentionally public — consulted
|
|
77
|
+
// only when the deployment runs the executor with `unprotectedOps: 'deny'`.
|
|
78
|
+
public: z.boolean().optional(),
|
|
76
79
|
maxResponseBytes: z.number().int().positive().max(64 * 1024 * 1024).optional(),
|
|
77
80
|
timeoutMs: z.number().int().positive().max(6e5).optional(),
|
|
78
81
|
summary: z.string().max(512).optional(),
|
|
@@ -153,6 +156,7 @@ var OverlayParseError = class extends Error {
|
|
|
153
156
|
this.errors = errors;
|
|
154
157
|
this.name = "OverlayParseError";
|
|
155
158
|
}
|
|
159
|
+
errors;
|
|
156
160
|
};
|
|
157
161
|
function parseRaw(input) {
|
|
158
162
|
switch (input.kind) {
|
|
@@ -210,7 +214,7 @@ function parseOverlay(input) {
|
|
|
210
214
|
}
|
|
211
215
|
|
|
212
216
|
// libs/adapters/src/skills/bundle/bundle-diff.ts
|
|
213
|
-
var opEqual = (a, b) => a.operationId === b.operationId && a.serviceId === b.serviceId && a.httpMethod === b.httpMethod && a.pathTemplate === b.pathTemplate && a.authBindingRef === b.authBindingRef && JSON.stringify(a.mapper ?? null) === JSON.stringify(b.mapper ?? null) && JSON.stringify(a.inputSchema) === JSON.stringify(b.inputSchema) && JSON.stringify(a.outputSchema) === JSON.stringify(b.outputSchema) && JSON.stringify(a.requiredAuthorities ?? null) === JSON.stringify(b.requiredAuthorities ?? null) && a.maxResponseBytes === b.maxResponseBytes && a.timeoutMs === b.timeoutMs;
|
|
217
|
+
var opEqual = (a, b) => a.operationId === b.operationId && a.serviceId === b.serviceId && a.httpMethod === b.httpMethod && a.pathTemplate === b.pathTemplate && a.authBindingRef === b.authBindingRef && JSON.stringify(a.mapper ?? null) === JSON.stringify(b.mapper ?? null) && JSON.stringify(a.inputSchema) === JSON.stringify(b.inputSchema) && JSON.stringify(a.outputSchema) === JSON.stringify(b.outputSchema) && JSON.stringify(a.requiredAuthorities ?? null) === JSON.stringify(b.requiredAuthorities ?? null) && (a.public ?? false) === (b.public ?? false) && a.maxResponseBytes === b.maxResponseBytes && a.timeoutMs === b.timeoutMs;
|
|
214
218
|
var skillEqual = (a, b) => a.id === b.id && a.name === b.name && a.description === b.description && a.instructions === b.instructions && JSON.stringify(a.tags ?? []) === JSON.stringify(b.tags ?? []) && JSON.stringify(a.operationIds) === JSON.stringify(b.operationIds) && JSON.stringify(a.requiredAuthorities ?? null) === JSON.stringify(b.requiredAuthorities ?? null);
|
|
215
219
|
function diffBundles(oldBundle, newBundle) {
|
|
216
220
|
const oldSkills = new Map((oldBundle?.skills ?? []).map((s) => [s.id, s]));
|
|
@@ -451,6 +455,7 @@ var BundlePinnedError = class extends Error {
|
|
|
451
455
|
this.pinnedVersion = pinnedVersion;
|
|
452
456
|
this.name = "BundlePinnedError";
|
|
453
457
|
}
|
|
458
|
+
pinnedVersion;
|
|
454
459
|
};
|
|
455
460
|
|
|
456
461
|
// libs/adapters/src/skills/dependency/skill-dag.ts
|
|
@@ -460,6 +465,7 @@ var SkillDependencyCycleError = class extends Error {
|
|
|
460
465
|
this.cycle = cycle;
|
|
461
466
|
this.name = "SkillDependencyCycleError";
|
|
462
467
|
}
|
|
468
|
+
cycle;
|
|
463
469
|
};
|
|
464
470
|
var SkillDependencyMissingError = class extends Error {
|
|
465
471
|
constructor(skillId, missingId) {
|
|
@@ -468,6 +474,8 @@ var SkillDependencyMissingError = class extends Error {
|
|
|
468
474
|
this.missingId = missingId;
|
|
469
475
|
this.name = "SkillDependencyMissingError";
|
|
470
476
|
}
|
|
477
|
+
skillId;
|
|
478
|
+
missingId;
|
|
471
479
|
};
|
|
472
480
|
var SkillDependencyInvariantError = class extends Error {
|
|
473
481
|
constructor(message) {
|
|
@@ -630,7 +638,22 @@ var saasSourceSchema = z2.object({
|
|
|
630
638
|
/** Expected issuer (`iss`) claim. */
|
|
631
639
|
expectedIssuer: z2.string().min(1)
|
|
632
640
|
});
|
|
633
|
-
var
|
|
641
|
+
var inlineSourceSchema = z2.object({
|
|
642
|
+
type: z2.literal("inline"),
|
|
643
|
+
/**
|
|
644
|
+
* The skilled-OpenAPI bundle object, embedded directly (no filesystem, no
|
|
645
|
+
* network). The right source for V8-isolate runtimes (Cloudflare Workers),
|
|
646
|
+
* where `static` (fs) and `npm` can't run and `saas` needs an endpoint. The
|
|
647
|
+
* object is validated by the overlay parser, same as file/remote bundles.
|
|
648
|
+
*/
|
|
649
|
+
content: z2.unknown()
|
|
650
|
+
});
|
|
651
|
+
var bundleSourceSchema = z2.discriminatedUnion("type", [
|
|
652
|
+
staticSourceSchema,
|
|
653
|
+
npmSourceSchema,
|
|
654
|
+
saasSourceSchema,
|
|
655
|
+
inlineSourceSchema
|
|
656
|
+
]);
|
|
634
657
|
var signatureKeySchema = z2.object({
|
|
635
658
|
/** Stable key id (matches `kid` claim in the bundle JWT). */
|
|
636
659
|
keyId: z2.string().min(1),
|
|
@@ -640,13 +663,50 @@ var signatureKeySchema = z2.object({
|
|
|
640
663
|
publicKeyPem: z2.string().min(1)
|
|
641
664
|
});
|
|
642
665
|
|
|
666
|
+
// libs/adapters/src/skills/sources/inline.source.ts
|
|
667
|
+
var InlineSource = class {
|
|
668
|
+
constructor(options, logger) {
|
|
669
|
+
this.options = options;
|
|
670
|
+
this.logger = logger;
|
|
671
|
+
}
|
|
672
|
+
options;
|
|
673
|
+
logger;
|
|
674
|
+
id = "inline:bundle";
|
|
675
|
+
listeners = /* @__PURE__ */ new Set();
|
|
676
|
+
async start() {
|
|
677
|
+
const bundle = this.parse();
|
|
678
|
+
for (const fn of this.listeners) {
|
|
679
|
+
try {
|
|
680
|
+
fn(bundle);
|
|
681
|
+
} catch (e) {
|
|
682
|
+
this.logger.warn(`[inline-source] listener threw: ${e.message}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
onChange(listener) {
|
|
687
|
+
this.listeners.add(listener);
|
|
688
|
+
return () => {
|
|
689
|
+
this.listeners.delete(listener);
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
async stop() {
|
|
693
|
+
this.listeners.clear();
|
|
694
|
+
}
|
|
695
|
+
parse() {
|
|
696
|
+
return parseOverlay({ kind: "object", content: this.options.content });
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
|
|
643
700
|
// libs/adapters/src/skills/sources/npm.source.ts
|
|
701
|
+
var npmDynamicImport;
|
|
644
702
|
var NpmSource = class {
|
|
645
703
|
constructor(options, logger) {
|
|
646
704
|
this.options = options;
|
|
647
705
|
this.logger = logger;
|
|
648
706
|
this.id = `npm:${this.options.packageName}${this.options.exportName ? `#${this.options.exportName}` : ""}`;
|
|
649
707
|
}
|
|
708
|
+
options;
|
|
709
|
+
logger;
|
|
650
710
|
id;
|
|
651
711
|
listeners = /* @__PURE__ */ new Set();
|
|
652
712
|
cached;
|
|
@@ -686,9 +746,16 @@ var NpmSource = class {
|
|
|
686
746
|
}
|
|
687
747
|
return parseOverlay({ kind: "object", content: exported });
|
|
688
748
|
}
|
|
689
|
-
// Indirected so tests can stub.
|
|
749
|
+
// Indirected so tests can stub. The dynamic `import()` is wrapped in a
|
|
750
|
+
// lazily-built `new Function` so static bundlers (esbuild / `wrangler dev` /
|
|
751
|
+
// miniflare) don't try to resolve the runtime-computed package specifier at
|
|
752
|
+
// build time — installing an npm bundle is a Node-only path that never runs
|
|
753
|
+
// on a V8 isolate, but `createBundleSource` keeps NpmSource statically
|
|
754
|
+
// reachable in the worker bundle. The Function is only built when this runs
|
|
755
|
+
// (Node), so a Worker that merely bundles it never evaluates it.
|
|
690
756
|
dynamicImport(specifier) {
|
|
691
|
-
return import(
|
|
757
|
+
npmDynamicImport ??= new Function("s", "return import(s)");
|
|
758
|
+
return npmDynamicImport(specifier);
|
|
692
759
|
}
|
|
693
760
|
notify(bundle) {
|
|
694
761
|
for (const fn of this.listeners) {
|
|
@@ -706,35 +773,49 @@ import { dirname, ensureDir, pathResolve, readFile, writeFile } from "@frontmcp/
|
|
|
706
773
|
var DEFAULT_CACHE_DIR = ".frontmcp/skilled-openapi";
|
|
707
774
|
var DEFAULT_PULL_TIMEOUT_MS = 3e4;
|
|
708
775
|
var SaasPullSource = class {
|
|
709
|
-
constructor(options, cacheDir, logger) {
|
|
776
|
+
constructor(options, cacheDir, logger, deps = {}) {
|
|
710
777
|
this.options = options;
|
|
711
778
|
this.cacheDir = cacheDir;
|
|
712
779
|
this.logger = logger;
|
|
780
|
+
this.deps = deps;
|
|
713
781
|
this.id = `saas:${this.options.endpoint}`;
|
|
714
782
|
}
|
|
783
|
+
options;
|
|
784
|
+
cacheDir;
|
|
785
|
+
logger;
|
|
786
|
+
deps;
|
|
715
787
|
id;
|
|
716
788
|
listeners = /* @__PURE__ */ new Set();
|
|
717
789
|
pollHandle;
|
|
718
790
|
inFlight = false;
|
|
719
791
|
stopped = false;
|
|
720
792
|
async start() {
|
|
793
|
+
if (this.stopped || this.inFlight) {
|
|
794
|
+
this.schedulePoll();
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
this.inFlight = true;
|
|
721
798
|
let bundle;
|
|
722
799
|
try {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
bundle = await this.loadCache();
|
|
730
|
-
if (!bundle) {
|
|
731
|
-
throw new Error(
|
|
732
|
-
`[saas-source] initial pull failed and no cached bundle is available at ${this.cachePath()}: ${e.message}`
|
|
800
|
+
try {
|
|
801
|
+
bundle = await this.fetchOnce();
|
|
802
|
+
await this.persistCache(bundle);
|
|
803
|
+
} catch (e) {
|
|
804
|
+
this.logger.warn(
|
|
805
|
+
`[saas-source] initial pull failed (${e.message}); attempting cached bundle fallback`
|
|
733
806
|
);
|
|
807
|
+
bundle = await this.loadCache();
|
|
808
|
+
if (!bundle) {
|
|
809
|
+
throw new Error(
|
|
810
|
+
`[saas-source] initial pull failed and no cached bundle is available at ${this.cachePath()}: ${e.message}`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
this.logger.warn(`[saas-source] using cached bundle "${bundle.bundleId}@${bundle.version}"`);
|
|
734
814
|
}
|
|
735
|
-
this.
|
|
815
|
+
this.notify(bundle);
|
|
816
|
+
} finally {
|
|
817
|
+
this.inFlight = false;
|
|
736
818
|
}
|
|
737
|
-
this.notify(bundle);
|
|
738
819
|
this.schedulePoll();
|
|
739
820
|
}
|
|
740
821
|
onChange(listener) {
|
|
@@ -752,12 +833,32 @@ var SaasPullSource = class {
|
|
|
752
833
|
this.listeners.clear();
|
|
753
834
|
}
|
|
754
835
|
schedulePoll() {
|
|
755
|
-
if (this.stopped) return;
|
|
836
|
+
if (this.stopped || this.deps.disablePolling) return;
|
|
837
|
+
if (this.pollHandle) clearTimeout(this.pollHandle);
|
|
756
838
|
this.pollHandle = setTimeout(() => {
|
|
757
839
|
void this.pollOnce();
|
|
758
840
|
}, this.options.pollIntervalMs);
|
|
759
841
|
this.pollHandle.unref?.();
|
|
760
842
|
}
|
|
843
|
+
/**
|
|
844
|
+
* Manually pull a fresh bundle, persist it, and notify listeners. Drive this
|
|
845
|
+
* from a Cron Trigger / Durable Object alarm on runtimes without background
|
|
846
|
+
* timers (where `disablePolling` is set). Single-flight: returns `undefined`
|
|
847
|
+
* if a pull is already in flight; throws if the pull itself fails (the caller
|
|
848
|
+
* — e.g. a scheduled handler — decides how to surface it).
|
|
849
|
+
*/
|
|
850
|
+
async refresh() {
|
|
851
|
+
if (this.stopped || this.inFlight) return void 0;
|
|
852
|
+
this.inFlight = true;
|
|
853
|
+
try {
|
|
854
|
+
const bundle = await this.fetchOnce();
|
|
855
|
+
await this.persistCache(bundle);
|
|
856
|
+
this.notify(bundle);
|
|
857
|
+
return bundle;
|
|
858
|
+
} finally {
|
|
859
|
+
this.inFlight = false;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
761
862
|
async pollOnce() {
|
|
762
863
|
if (this.stopped) return;
|
|
763
864
|
if (this.inFlight) {
|
|
@@ -808,6 +909,10 @@ var SaasPullSource = class {
|
|
|
808
909
|
}
|
|
809
910
|
async persistCache(bundle) {
|
|
810
911
|
try {
|
|
912
|
+
if (this.deps.cache) {
|
|
913
|
+
await this.deps.cache.write(bundle);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
811
916
|
const filePath = this.cachePath();
|
|
812
917
|
await ensureDir(dirname(filePath));
|
|
813
918
|
await writeFile(filePath, JSON.stringify(bundle));
|
|
@@ -817,6 +922,11 @@ var SaasPullSource = class {
|
|
|
817
922
|
}
|
|
818
923
|
async loadCache() {
|
|
819
924
|
try {
|
|
925
|
+
if (this.deps.cache) {
|
|
926
|
+
const cached = await this.deps.cache.read();
|
|
927
|
+
if (cached === void 0 || cached === null) return void 0;
|
|
928
|
+
return parseOverlay({ kind: "object", content: cached });
|
|
929
|
+
}
|
|
820
930
|
const filePath = this.cachePath();
|
|
821
931
|
const raw = await readFile(filePath, "utf8");
|
|
822
932
|
return parseOverlay({ kind: "json", content: raw });
|
|
@@ -843,6 +953,8 @@ var StaticSource = class {
|
|
|
843
953
|
this.logger = logger;
|
|
844
954
|
this.id = `static:${this.options.path}`;
|
|
845
955
|
}
|
|
956
|
+
options;
|
|
957
|
+
logger;
|
|
846
958
|
id;
|
|
847
959
|
listeners = /* @__PURE__ */ new Set();
|
|
848
960
|
watcher;
|
|
@@ -927,17 +1039,74 @@ var StaticSource = class {
|
|
|
927
1039
|
};
|
|
928
1040
|
|
|
929
1041
|
// libs/adapters/src/skills/sources/index.ts
|
|
930
|
-
function createBundleSource(source, cacheDir, logger) {
|
|
1042
|
+
function createBundleSource(source, cacheDir, logger, deps) {
|
|
931
1043
|
switch (source.type) {
|
|
932
1044
|
case "static":
|
|
933
1045
|
return new StaticSource(source, logger);
|
|
934
1046
|
case "npm":
|
|
935
1047
|
return new NpmSource(source, logger);
|
|
936
1048
|
case "saas":
|
|
937
|
-
return new SaasPullSource(source, cacheDir, logger);
|
|
1049
|
+
return new SaasPullSource(source, cacheDir, logger, deps);
|
|
1050
|
+
case "inline":
|
|
1051
|
+
return new InlineSource(source, logger);
|
|
938
1052
|
}
|
|
939
1053
|
}
|
|
940
1054
|
|
|
1055
|
+
// libs/adapters/src/skills/openapi/compile-skilled-bundle.ts
|
|
1056
|
+
import { OpenAPIToolGenerator } from "mcp-from-openapi";
|
|
1057
|
+
async function compileSkilledBundleFromOpenApi(openapi, skills, options) {
|
|
1058
|
+
const serviceId = options.serviceId ?? "api";
|
|
1059
|
+
const baseUrl = options.baseUrl ?? openapi.servers?.[0]?.url;
|
|
1060
|
+
if (!baseUrl) {
|
|
1061
|
+
throw new Error("compileSkilledBundleFromOpenApi: no baseUrl \u2014 pass options.baseUrl or declare servers[] in the OpenAPI doc.");
|
|
1062
|
+
}
|
|
1063
|
+
const generator = await OpenAPIToolGenerator.fromJSON(openapi);
|
|
1064
|
+
const tools = await generator.generateTools();
|
|
1065
|
+
const byOperationId = /* @__PURE__ */ new Map();
|
|
1066
|
+
for (const tool of tools) {
|
|
1067
|
+
const opId = tool.metadata.operationId;
|
|
1068
|
+
if (opId) byOperationId.set(opId, tool);
|
|
1069
|
+
}
|
|
1070
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
1071
|
+
for (const skill of skills) for (const opId of skill.operationIds) referenced.add(opId);
|
|
1072
|
+
const operations = {};
|
|
1073
|
+
for (const opId of referenced) {
|
|
1074
|
+
const tool = byOperationId.get(opId);
|
|
1075
|
+
if (!tool) {
|
|
1076
|
+
throw new Error(`compileSkilledBundleFromOpenApi: operationId "${opId}" is referenced by a skill but not defined in the OpenAPI document.`);
|
|
1077
|
+
}
|
|
1078
|
+
operations[opId] = {
|
|
1079
|
+
operationId: opId,
|
|
1080
|
+
serviceId,
|
|
1081
|
+
httpMethod: tool.metadata.method.toUpperCase(),
|
|
1082
|
+
pathTemplate: tool.metadata.path,
|
|
1083
|
+
summary: tool.metadata.operationSummary ?? tool.description,
|
|
1084
|
+
inputSchema: tool.inputSchema,
|
|
1085
|
+
outputSchema: tool.outputSchema ?? { type: "object" },
|
|
1086
|
+
mapper: tool.mapper,
|
|
1087
|
+
authBindingRef: "default"
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
return {
|
|
1091
|
+
schemaVersion: 1,
|
|
1092
|
+
bundleId: options.bundleId,
|
|
1093
|
+
version: options.version,
|
|
1094
|
+
generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1095
|
+
sourceDigest: options.sourceDigest ?? "0".repeat(64),
|
|
1096
|
+
services: [{ id: serviceId, baseUrl }],
|
|
1097
|
+
authBindings: { default: options.authBinding ?? { kind: "none" } },
|
|
1098
|
+
skills: skills.map((s) => ({
|
|
1099
|
+
id: s.id,
|
|
1100
|
+
name: s.name,
|
|
1101
|
+
description: s.description,
|
|
1102
|
+
instructions: s.instructions,
|
|
1103
|
+
tags: s.tags ?? [],
|
|
1104
|
+
operationIds: s.operationIds
|
|
1105
|
+
})),
|
|
1106
|
+
operations
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
|
|
941
1110
|
// libs/adapters/src/skills/sources/filesystem-skills.source.ts
|
|
942
1111
|
import {
|
|
943
1112
|
basename,
|
|
@@ -962,6 +1131,7 @@ var FilesystemSkillsSource = class {
|
|
|
962
1131
|
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
963
1132
|
this.id = `filesystem-skills:${this.options.skillsDir}`;
|
|
964
1133
|
}
|
|
1134
|
+
logger;
|
|
965
1135
|
id;
|
|
966
1136
|
options;
|
|
967
1137
|
listeners = /* @__PURE__ */ new Set();
|
|
@@ -1360,6 +1530,8 @@ var BundlePushJwtVerifier = class {
|
|
|
1360
1530
|
jwksUri: options.jwksUri
|
|
1361
1531
|
};
|
|
1362
1532
|
}
|
|
1533
|
+
options;
|
|
1534
|
+
logger;
|
|
1363
1535
|
providerRef;
|
|
1364
1536
|
jwks;
|
|
1365
1537
|
/**
|
|
@@ -1762,6 +1934,8 @@ var StorageAdapterAuditStore = class {
|
|
|
1762
1934
|
this.adapter = adapter;
|
|
1763
1935
|
this.options = options;
|
|
1764
1936
|
}
|
|
1937
|
+
adapter;
|
|
1938
|
+
options;
|
|
1765
1939
|
get sequenceKey() {
|
|
1766
1940
|
return this.options.sequenceKey ?? SKILL_AUDIT_KEYS.sequence;
|
|
1767
1941
|
}
|
|
@@ -1838,6 +2012,10 @@ var SkillAuditWriter = class {
|
|
|
1838
2012
|
this.subjectHashSecret = options.subjectHashSecret ?? this.deriveDefaultSubjectSecret();
|
|
1839
2013
|
this.maxQueueDepth = options.maxQueueDepth ?? SKILL_AUDIT_QUEUE_MAX;
|
|
1840
2014
|
}
|
|
2015
|
+
store;
|
|
2016
|
+
signer;
|
|
2017
|
+
logger;
|
|
2018
|
+
metrics;
|
|
1841
2019
|
/**
|
|
1842
2020
|
* Single-flight queue: every append() chains onto this promise so the
|
|
1843
2021
|
* read-tail-then-append-with-prevHash sequence stays atomic per process.
|
|
@@ -2452,6 +2630,63 @@ function crossValidateManifest(manifest) {
|
|
|
2452
2630
|
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
|
2453
2631
|
}
|
|
2454
2632
|
|
|
2633
|
+
// libs/adapters/src/skills/deploy/manifest-to-config.ts
|
|
2634
|
+
function specStem(ref) {
|
|
2635
|
+
const noQuery = ref.split("?")[0].split("#")[0];
|
|
2636
|
+
const base = noQuery.split("/").pop() ?? noQuery;
|
|
2637
|
+
const stem = base.replace(/\.(ya?ml|json)$/i, "");
|
|
2638
|
+
const safe = (stem || base).replace(/[^a-zA-Z0-9_.-]/g, "-");
|
|
2639
|
+
return safe || "spec";
|
|
2640
|
+
}
|
|
2641
|
+
function normalizeSpec(source) {
|
|
2642
|
+
if (typeof source === "string") {
|
|
2643
|
+
const id = specStem(source);
|
|
2644
|
+
return { id, spec: source, bindingName: id };
|
|
2645
|
+
}
|
|
2646
|
+
return {
|
|
2647
|
+
id: source.id,
|
|
2648
|
+
spec: source.spec,
|
|
2649
|
+
baseUrl: source.baseUrl,
|
|
2650
|
+
bindingName: source.bindingName ?? source.id
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
function buildFrontMcpConfigFromManifest(manifest) {
|
|
2654
|
+
const info = {
|
|
2655
|
+
name: manifest.server.info.name,
|
|
2656
|
+
version: manifest.server.info.version,
|
|
2657
|
+
...manifest.server.info.title !== void 0 ? { title: manifest.server.info.title } : {}
|
|
2658
|
+
};
|
|
2659
|
+
const skills = {
|
|
2660
|
+
source: manifest.skills.source,
|
|
2661
|
+
...manifest.skills.alwaysLoad ? { alwaysLoad: manifest.skills.alwaysLoad } : {},
|
|
2662
|
+
...manifest.skills.tags ? { tags: manifest.skills.tags } : {}
|
|
2663
|
+
};
|
|
2664
|
+
let specs = [];
|
|
2665
|
+
let specsDir;
|
|
2666
|
+
if (typeof manifest.specs === "string") {
|
|
2667
|
+
specsDir = manifest.specs;
|
|
2668
|
+
} else {
|
|
2669
|
+
specs = manifest.specs.map(normalizeSpec);
|
|
2670
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2671
|
+
for (const s of specs) {
|
|
2672
|
+
if (seen.has(s.id)) {
|
|
2673
|
+
throw new Error(
|
|
2674
|
+
`manifest specs: duplicate spec id "${s.id}" (derived from the filename stem). Give the colliding spec an explicit { id } in frontmcp.deploy.yaml.`
|
|
2675
|
+
);
|
|
2676
|
+
}
|
|
2677
|
+
seen.add(s.id);
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
return {
|
|
2681
|
+
info,
|
|
2682
|
+
...manifest.server.instructions !== void 0 ? { instructions: manifest.server.instructions } : {},
|
|
2683
|
+
skillsConfig: { enabled: true, injectInstructions: "append" },
|
|
2684
|
+
skills,
|
|
2685
|
+
specs,
|
|
2686
|
+
...specsDir !== void 0 ? { specsDir } : {}
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2455
2690
|
// libs/adapters/src/skills/classifier/openapi-classify.ts
|
|
2456
2691
|
function classifyOperations(specId, ops) {
|
|
2457
2692
|
if (!specId || specId.length === 0) {
|
|
@@ -2770,6 +3005,7 @@ export {
|
|
|
2770
3005
|
ClassificationRegistry,
|
|
2771
3006
|
FilesystemSkillsSource,
|
|
2772
3007
|
Hs256AuditSigner,
|
|
3008
|
+
InlineSource,
|
|
2773
3009
|
MemoryAuditStore,
|
|
2774
3010
|
NpmSource,
|
|
2775
3011
|
OverlayParseError,
|
|
@@ -2789,6 +3025,7 @@ export {
|
|
|
2789
3025
|
WebhookReplayGuard,
|
|
2790
3026
|
applyClassificationOverrides,
|
|
2791
3027
|
applyEnvironmentOverlay,
|
|
3028
|
+
buildFrontMcpConfigFromManifest,
|
|
2792
3029
|
buildKnownOps,
|
|
2793
3030
|
buildResourceChangeNotification,
|
|
2794
3031
|
bundleDigest,
|
|
@@ -2798,6 +3035,7 @@ export {
|
|
|
2798
3035
|
canonicalizeRecordForSigning,
|
|
2799
3036
|
classifyOne,
|
|
2800
3037
|
classifyOperations,
|
|
3038
|
+
compileSkilledBundleFromOpenApi,
|
|
2801
3039
|
createBundleSource,
|
|
2802
3040
|
crossValidate,
|
|
2803
3041
|
crossValidateManifest,
|
package/index.js
CHANGED
|
@@ -1213,7 +1213,22 @@ Add one of the following to your adapter configuration:
|
|
|
1213
1213
|
* Initialize the OpenAPI tool generator from URL or spec
|
|
1214
1214
|
* @private
|
|
1215
1215
|
*/
|
|
1216
|
+
/**
|
|
1217
|
+
* Resolve the `$ref` resolution policy with FrontMCP's secure defaults.
|
|
1218
|
+
*
|
|
1219
|
+
* SECURITY (GHSA-65h7-9wrw-629c): external `$ref` resolution is **disabled by
|
|
1220
|
+
* default** — an untrusted spec can otherwise smuggle SSRF via external refs.
|
|
1221
|
+
* Internal-only (`#/...`) refs and inline `spec:` are unaffected. Developers
|
|
1222
|
+
* who genuinely need external refs opt back in by setting
|
|
1223
|
+
* `loadOptions.refResolution` explicitly (and then own its allow/deny lists;
|
|
1224
|
+
* `mcp-from-openapi` ≥ 2.5.0 DNS-resolves and re-validates redirects).
|
|
1225
|
+
* @private
|
|
1226
|
+
*/
|
|
1227
|
+
resolveRefResolution() {
|
|
1228
|
+
return this.options.loadOptions?.refResolution ?? { allowedProtocols: [] };
|
|
1229
|
+
}
|
|
1216
1230
|
async initializeGenerator() {
|
|
1231
|
+
const refResolution = this.resolveRefResolution();
|
|
1217
1232
|
if ("url" in this.options) {
|
|
1218
1233
|
return await import_mcp_from_openapi2.OpenAPIToolGenerator.fromURL(this.options.url, {
|
|
1219
1234
|
baseUrl: this.options.baseUrl,
|
|
@@ -1221,15 +1236,18 @@ Add one of the following to your adapter configuration:
|
|
|
1221
1236
|
dereference: this.options.loadOptions?.dereference ?? true,
|
|
1222
1237
|
headers: this.options.loadOptions?.headers,
|
|
1223
1238
|
timeout: this.options.loadOptions?.timeout,
|
|
1224
|
-
|
|
1225
|
-
|
|
1239
|
+
// SECURITY: do not follow spec-URL redirects by default — a 3xx to an
|
|
1240
|
+
// internal target would otherwise be fetched. With mcp-from-openapi
|
|
1241
|
+
// ≥ 2.5.0 each hop is re-validated; opt in with loadOptions.followRedirects.
|
|
1242
|
+
followRedirects: this.options.loadOptions?.followRedirects ?? false,
|
|
1243
|
+
refResolution
|
|
1226
1244
|
});
|
|
1227
1245
|
} else if ("spec" in this.options) {
|
|
1228
1246
|
return await import_mcp_from_openapi2.OpenAPIToolGenerator.fromJSON(this.options.spec, {
|
|
1229
1247
|
baseUrl: this.options.baseUrl,
|
|
1230
1248
|
validate: this.options.loadOptions?.validate ?? true,
|
|
1231
1249
|
dereference: this.options.loadOptions?.dereference ?? true,
|
|
1232
|
-
refResolution
|
|
1250
|
+
refResolution
|
|
1233
1251
|
});
|
|
1234
1252
|
} else {
|
|
1235
1253
|
throw new Error("Either url or spec must be provided in OpenApiAdapterOptions");
|
package/openapi/index.js
CHANGED
|
@@ -1213,7 +1213,22 @@ Add one of the following to your adapter configuration:
|
|
|
1213
1213
|
* Initialize the OpenAPI tool generator from URL or spec
|
|
1214
1214
|
* @private
|
|
1215
1215
|
*/
|
|
1216
|
+
/**
|
|
1217
|
+
* Resolve the `$ref` resolution policy with FrontMCP's secure defaults.
|
|
1218
|
+
*
|
|
1219
|
+
* SECURITY (GHSA-65h7-9wrw-629c): external `$ref` resolution is **disabled by
|
|
1220
|
+
* default** — an untrusted spec can otherwise smuggle SSRF via external refs.
|
|
1221
|
+
* Internal-only (`#/...`) refs and inline `spec:` are unaffected. Developers
|
|
1222
|
+
* who genuinely need external refs opt back in by setting
|
|
1223
|
+
* `loadOptions.refResolution` explicitly (and then own its allow/deny lists;
|
|
1224
|
+
* `mcp-from-openapi` ≥ 2.5.0 DNS-resolves and re-validates redirects).
|
|
1225
|
+
* @private
|
|
1226
|
+
*/
|
|
1227
|
+
resolveRefResolution() {
|
|
1228
|
+
return this.options.loadOptions?.refResolution ?? { allowedProtocols: [] };
|
|
1229
|
+
}
|
|
1216
1230
|
async initializeGenerator() {
|
|
1231
|
+
const refResolution = this.resolveRefResolution();
|
|
1217
1232
|
if ("url" in this.options) {
|
|
1218
1233
|
return await import_mcp_from_openapi2.OpenAPIToolGenerator.fromURL(this.options.url, {
|
|
1219
1234
|
baseUrl: this.options.baseUrl,
|
|
@@ -1221,15 +1236,18 @@ Add one of the following to your adapter configuration:
|
|
|
1221
1236
|
dereference: this.options.loadOptions?.dereference ?? true,
|
|
1222
1237
|
headers: this.options.loadOptions?.headers,
|
|
1223
1238
|
timeout: this.options.loadOptions?.timeout,
|
|
1224
|
-
|
|
1225
|
-
|
|
1239
|
+
// SECURITY: do not follow spec-URL redirects by default — a 3xx to an
|
|
1240
|
+
// internal target would otherwise be fetched. With mcp-from-openapi
|
|
1241
|
+
// ≥ 2.5.0 each hop is re-validated; opt in with loadOptions.followRedirects.
|
|
1242
|
+
followRedirects: this.options.loadOptions?.followRedirects ?? false,
|
|
1243
|
+
refResolution
|
|
1226
1244
|
});
|
|
1227
1245
|
} else if ("spec" in this.options) {
|
|
1228
1246
|
return await import_mcp_from_openapi2.OpenAPIToolGenerator.fromJSON(this.options.spec, {
|
|
1229
1247
|
baseUrl: this.options.baseUrl,
|
|
1230
1248
|
validate: this.options.loadOptions?.validate ?? true,
|
|
1231
1249
|
dereference: this.options.loadOptions?.dereference ?? true,
|
|
1232
|
-
refResolution
|
|
1250
|
+
refResolution
|
|
1233
1251
|
});
|
|
1234
1252
|
} else {
|
|
1235
1253
|
throw new Error("Either url or spec must be provided in OpenApiAdapterOptions");
|