@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.
Files changed (37) hide show
  1. package/esm/index.mjs +21 -3
  2. package/esm/openapi/index.mjs +21 -3
  3. package/esm/package.json +7 -7
  4. package/esm/skills/index.mjs +258 -20
  5. package/index.js +21 -3
  6. package/openapi/index.js +21 -3
  7. package/openapi/openapi.adapter.d.ts +12 -0
  8. package/openapi/openapi.adapter.d.ts.map +1 -1
  9. package/openapi/openapi.types.d.ts +8 -1
  10. package/openapi/openapi.types.d.ts.map +1 -1
  11. package/package.json +7 -7
  12. package/skills/audit/audit-config.d.ts +1 -1
  13. package/skills/audit/audit-writer.d.ts +2 -1
  14. package/skills/audit/audit-writer.d.ts.map +1 -1
  15. package/skills/bundle/bundle-diff.d.ts.map +1 -1
  16. package/skills/bundle/bundle.schema.d.ts +1 -0
  17. package/skills/bundle/bundle.schema.d.ts.map +1 -1
  18. package/skills/bundle/bundle.types.d.ts +12 -3
  19. package/skills/bundle/bundle.types.d.ts.map +1 -1
  20. package/skills/deploy/manifest-to-config.d.ts +61 -0
  21. package/skills/deploy/manifest-to-config.d.ts.map +1 -0
  22. package/skills/index.d.ts +4 -2
  23. package/skills/index.d.ts.map +1 -1
  24. package/skills/index.js +261 -20
  25. package/skills/openapi/compile-skilled-bundle.d.ts +73 -0
  26. package/skills/openapi/compile-skilled-bundle.d.ts.map +1 -0
  27. package/skills/source-options.d.ts +8 -0
  28. package/skills/source-options.d.ts.map +1 -1
  29. package/skills/sources/index.d.ts +5 -4
  30. package/skills/sources/index.d.ts.map +1 -1
  31. package/skills/sources/inline.source.d.ts +16 -0
  32. package/skills/sources/inline.source.d.ts.map +1 -0
  33. package/skills/sources/npm.source.d.ts.map +1 -1
  34. package/skills/sources/saas-pull.source.d.ts +12 -2
  35. package/skills/sources/saas-pull.source.d.ts.map +1 -1
  36. package/skills/sources/skill-bundle-source.interface.d.ts +41 -0
  37. 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
- followRedirects: this.options.loadOptions?.followRedirects,
1190
- refResolution: this.options.loadOptions?.refResolution
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: this.options.loadOptions?.refResolution
1215
+ refResolution
1198
1216
  });
1199
1217
  } else {
1200
1218
  throw new Error("Either url or spec must be provided in OpenApiAdapterOptions");
@@ -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
- followRedirects: this.options.loadOptions?.followRedirects,
1190
- refResolution: this.options.loadOptions?.refResolution
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: this.options.loadOptions?.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.4.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.4.1",
68
- "@frontmcp/di": "1.4.1",
69
- "@frontmcp/sdk": "1.4.1",
70
- "@frontmcp/utils": "1.4.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.3.0",
72
+ "mcp-from-openapi": "2.5.0",
73
73
  "openapi-types": "^12.1.3"
74
74
  },
75
75
  "peerDependencies": {
76
- "@frontmcp/lazy-zod": "1.4.1"
76
+ "@frontmcp/lazy-zod": "1.5.0"
77
77
  }
78
78
  }
@@ -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 bundleSourceSchema = z2.discriminatedUnion("type", [staticSourceSchema, npmSourceSchema, saasSourceSchema]);
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(specifier);
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
- bundle = await this.fetchOnce();
724
- await this.persistCache(bundle);
725
- } catch (e) {
726
- this.logger.warn(
727
- `[saas-source] initial pull failed (${e.message}); attempting cached bundle fallback`
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.logger.warn(`[saas-source] using cached bundle "${bundle.bundleId}@${bundle.version}"`);
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
- followRedirects: this.options.loadOptions?.followRedirects,
1225
- refResolution: this.options.loadOptions?.refResolution
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: this.options.loadOptions?.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
- followRedirects: this.options.loadOptions?.followRedirects,
1225
- refResolution: this.options.loadOptions?.refResolution
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: this.options.loadOptions?.refResolution
1250
+ refResolution
1233
1251
  });
1234
1252
  } else {
1235
1253
  throw new Error("Either url or spec must be provided in OpenApiAdapterOptions");