@alfe.ai/mcp-bundler 0.0.1 → 0.1.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/dist/index.cjs CHANGED
@@ -1,4 +1,9 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let node_fs = require("node:fs");
3
+ let node_path = require("node:path");
4
+ let node_os = require("node:os");
5
+ let node_child_process = require("node:child_process");
6
+ let node_util = require("node:util");
2
7
  //#region src/tool-naming.ts
3
8
  /**
4
9
  * Tool name sanitization and collision handling.
@@ -450,11 +455,598 @@ var McpBundler = class {
450
455
  }
451
456
  };
452
457
  //#endregion
458
+ //#region src/store.ts
459
+ const DEFAULT_STORE_PATH = (0, node_path.join)((0, node_path.join)((0, node_os.homedir)(), ".alfe", "mcp"), "servers.json");
460
+ /** Inter-process lock tunings — exported as constants so tests can override. */
461
+ const LOCK_WAIT_MS = 5e3;
462
+ const LOCK_RETRY_INTERVAL_MS = 25;
463
+ const LOCK_STALE_MS = 1e4;
464
+ /**
465
+ * On-disk source of truth for the bundler's configured servers.
466
+ *
467
+ * Mutations go through `update()` (read-modify-write with atomic
468
+ * temp+rename) so a concurrent writer (e.g. two `alfe mcp add` invocations
469
+ * racing) can't lose data — the second writer reads the first's state.
470
+ *
471
+ * Schema is owner-tagged so `removeServersByOwner` can implement
472
+ * integration uninstall without touching CLI-owned or manual entries.
473
+ */
474
+ var Store = class {
475
+ storePath;
476
+ logger;
477
+ watcher;
478
+ watcherListeners = /* @__PURE__ */ new Set();
479
+ rewatchTimer;
480
+ constructor(opts = {}) {
481
+ this.storePath = opts.path ?? DEFAULT_STORE_PATH;
482
+ this.logger = opts.logger;
483
+ }
484
+ get path() {
485
+ return this.storePath;
486
+ }
487
+ read() {
488
+ if (!(0, node_fs.existsSync)(this.storePath)) return cloneEmpty();
489
+ try {
490
+ const raw = (0, node_fs.readFileSync)(this.storePath, "utf8");
491
+ return normalize(JSON.parse(raw));
492
+ } catch (err) {
493
+ this.logger?.warn("[mcp-bundler/store] failed to read store; returning empty", {
494
+ err: errMsg$1(err),
495
+ path: this.storePath
496
+ });
497
+ return cloneEmpty();
498
+ }
499
+ }
500
+ /**
501
+ * Read-modify-write with atomic temp+rename, guarded by an
502
+ * inter-process lock file. Caller passes a pure function that
503
+ * produces the next state; this serialises the mutation to disk in
504
+ * one rename, which is atomic on POSIX and on Windows when the
505
+ * target path is on the same volume.
506
+ *
507
+ * The lock guards the read-then-rename window so two processes
508
+ * (e.g. two `alfe mcp add` shells, or the CLI racing the daemon)
509
+ * can't drop each other's writes. The lock file is at
510
+ * `<storePath>.lock`; stale locks (older than `LOCK_STALE_MS`) are
511
+ * stolen so a crashed writer doesn't wedge the store.
512
+ *
513
+ * Pure-function shape (instead of a `read()` then `write(next)`
514
+ * pair) intentionally — it keeps the read-modify-write contract
515
+ * local to each caller so two updates back-to-back never see each
516
+ * other's partial state.
517
+ */
518
+ update(fn) {
519
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(this.storePath), { recursive: true });
520
+ const release = this.acquireLock();
521
+ try {
522
+ const next = fn(this.read());
523
+ const tempPath = `${this.storePath}.${String(process.pid)}.${String(Date.now())}.tmp`;
524
+ (0, node_fs.writeFileSync)(tempPath, JSON.stringify(next, null, 2), {
525
+ encoding: "utf8",
526
+ mode: 384
527
+ });
528
+ try {
529
+ (0, node_fs.renameSync)(tempPath, this.storePath);
530
+ } catch (err) {
531
+ try {
532
+ (0, node_fs.unlinkSync)(tempPath);
533
+ } catch {}
534
+ throw err;
535
+ }
536
+ return next;
537
+ } finally {
538
+ release();
539
+ }
540
+ }
541
+ /**
542
+ * Acquire an inter-process file lock by atomically creating a
543
+ * sentinel via `openSync(lockPath, 'wx')`. Spins with bounded
544
+ * backoff up to `LOCK_WAIT_MS`. If the lock file is older than
545
+ * `LOCK_STALE_MS` it's assumed orphaned (writer crashed mid-update)
546
+ * and stolen — the write window is sub-second in practice, so
547
+ * holding the lock for >5s means something went wrong.
548
+ *
549
+ * Returns the release function. Single-process callers are
550
+ * unaffected — re-entering the same process spins briefly while
551
+ * the prior call's `finally` runs.
552
+ */
553
+ acquireLock() {
554
+ const lockPath = `${this.storePath}.lock`;
555
+ const deadline = Date.now() + LOCK_WAIT_MS;
556
+ let fd = -1;
557
+ for (;;) try {
558
+ fd = (0, node_fs.openSync)(lockPath, "wx", 384);
559
+ break;
560
+ } catch (err) {
561
+ if (err.code !== "EEXIST") throw err;
562
+ if (this.lockIsStale(lockPath)) {
563
+ try {
564
+ (0, node_fs.unlinkSync)(lockPath);
565
+ } catch {}
566
+ continue;
567
+ }
568
+ if (Date.now() >= deadline) throw new Error(`Store.update: timed out waiting for ${lockPath} (held by another writer or stale lock)`);
569
+ const sleepUntil = Date.now() + LOCK_RETRY_INTERVAL_MS;
570
+ while (Date.now() < sleepUntil);
571
+ }
572
+ const held = fd;
573
+ return () => {
574
+ try {
575
+ (0, node_fs.closeSync)(held);
576
+ } catch {}
577
+ try {
578
+ (0, node_fs.unlinkSync)(lockPath);
579
+ } catch {}
580
+ };
581
+ }
582
+ lockIsStale(lockPath) {
583
+ try {
584
+ const st = (0, node_fs.statSync)(lockPath);
585
+ return Date.now() - st.mtimeMs > LOCK_STALE_MS;
586
+ } catch {
587
+ return false;
588
+ }
589
+ }
590
+ /**
591
+ * Watch the store file for external changes (e.g. another `alfe mcp add`
592
+ * shelling out from a separate process). Returns an unsubscribe fn.
593
+ *
594
+ * Coalesces bursts via a 50 ms debounce — editors and atomic-rename
595
+ * writers commonly fire multiple events per logical save.
596
+ */
597
+ watch(cb) {
598
+ this.watcherListeners.add(cb);
599
+ this.ensureWatcher();
600
+ return () => {
601
+ this.watcherListeners.delete(cb);
602
+ if (this.watcherListeners.size === 0) this.disposeWatcher();
603
+ };
604
+ }
605
+ dispose() {
606
+ this.watcherListeners.clear();
607
+ this.disposeWatcher();
608
+ }
609
+ ensureWatcher() {
610
+ if (this.watcher) return;
611
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(this.storePath), { recursive: true });
612
+ const dir = (0, node_path.dirname)(this.storePath);
613
+ const basename = this.storePath.slice(dir.length + 1);
614
+ let pending;
615
+ const fire = () => {
616
+ pending = void 0;
617
+ for (const cb of this.watcherListeners) try {
618
+ cb();
619
+ } catch (err) {
620
+ this.logger?.warn("[mcp-bundler/store] watcher listener threw", { err: errMsg$1(err) });
621
+ }
622
+ };
623
+ try {
624
+ this.watcher = (0, node_fs.watch)(dir, (_event, fn) => {
625
+ if (fn !== basename) return;
626
+ if (pending) clearTimeout(pending);
627
+ pending = setTimeout(fire, 50);
628
+ });
629
+ this.watcher.on("error", (err) => {
630
+ this.logger?.warn("[mcp-bundler/store] watcher error; retrying in 1s", { err: errMsg$1(err) });
631
+ this.disposeWatcher();
632
+ if (!this.rewatchTimer && this.watcherListeners.size > 0) {
633
+ this.rewatchTimer = setTimeout(() => {
634
+ this.rewatchTimer = void 0;
635
+ this.ensureWatcher();
636
+ }, 1e3);
637
+ this.rewatchTimer.unref();
638
+ }
639
+ });
640
+ } catch (err) {
641
+ this.logger?.warn("[mcp-bundler/store] failed to start watcher", { err: errMsg$1(err) });
642
+ }
643
+ }
644
+ disposeWatcher() {
645
+ if (this.watcher) {
646
+ try {
647
+ this.watcher.close();
648
+ } catch {}
649
+ this.watcher = void 0;
650
+ }
651
+ if (this.rewatchTimer) {
652
+ clearTimeout(this.rewatchTimer);
653
+ this.rewatchTimer = void 0;
654
+ }
655
+ }
656
+ };
657
+ function defaultStorePath() {
658
+ return DEFAULT_STORE_PATH;
659
+ }
660
+ /** Pull the runtime config (transport + transport-specific fields) out of a stored entry. */
661
+ function toServerConfig(entry) {
662
+ if (entry.transport === "stdio") {
663
+ const { command, args, env, cwd } = entry;
664
+ const cfg = { command };
665
+ if (args) cfg.args = args;
666
+ if (env) cfg.env = env;
667
+ if (cwd) cfg.cwd = cwd;
668
+ return cfg;
669
+ }
670
+ const { url, transport, headers, connectionTimeoutMs } = entry;
671
+ const cfg = {
672
+ url,
673
+ transport
674
+ };
675
+ if (headers) cfg.headers = headers;
676
+ if (connectionTimeoutMs !== void 0) cfg.connectionTimeoutMs = connectionTimeoutMs;
677
+ return cfg;
678
+ }
679
+ /** Build a stored entry from a runtime config + ownership metadata. */
680
+ function toStoredEntry(config, meta) {
681
+ const addedAt = meta.addedAt ?? (/* @__PURE__ */ new Date()).toISOString();
682
+ if ("command" in config) return {
683
+ transport: "stdio",
684
+ owner: meta.owner,
685
+ addedAt,
686
+ ...meta.version !== void 0 ? { version: meta.version } : {},
687
+ ...config
688
+ };
689
+ const transport = meta.transport ?? config.transport ?? "sse";
690
+ if (transport === "stdio") throw new Error("toStoredEntry: transport=stdio specified but config is remote-shaped");
691
+ return {
692
+ transport,
693
+ owner: meta.owner,
694
+ addedAt,
695
+ ...meta.version !== void 0 ? { version: meta.version } : {},
696
+ ...config
697
+ };
698
+ }
699
+ function cloneEmpty() {
700
+ return {
701
+ servers: {},
702
+ config: {},
703
+ _ownedOpenclawKeys: []
704
+ };
705
+ }
706
+ function normalize(raw) {
707
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return cloneEmpty();
708
+ const r = raw;
709
+ return {
710
+ servers: r.servers && typeof r.servers === "object" ? r.servers : {},
711
+ config: r.config && typeof r.config === "object" ? r.config : {},
712
+ _ownedOpenclawKeys: Array.isArray(r._ownedOpenclawKeys) ? r._ownedOpenclawKeys.slice() : []
713
+ };
714
+ }
715
+ function errMsg$1(err) {
716
+ return err instanceof Error ? err.message : String(err);
717
+ }
718
+ //#endregion
719
+ //#region src/manager.ts
720
+ const execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
721
+ const defaultOpenclawExecutor = {
722
+ async setBatch(batch) {
723
+ if (batch.length === 0) return;
724
+ await execFileAsync("openclaw", [
725
+ "config",
726
+ "set",
727
+ "--batch-json",
728
+ JSON.stringify(batch)
729
+ ], { timeout: 1e4 });
730
+ },
731
+ async unset(path) {
732
+ await execFileAsync("openclaw", [
733
+ "config",
734
+ "unset",
735
+ path
736
+ ], { timeout: 1e4 });
737
+ }
738
+ };
739
+ const DEFAULT_MIRROR_DEBOUNCE_MS = 250;
740
+ /**
741
+ * Bundler manager — owns the alfe store, mirrors it into openclaw.json,
742
+ * and surfaces a small CRUD API the CLI and integration applier both
743
+ * call into.
744
+ *
745
+ * The store is the Alfe-owned source of truth; openclaw.json is a
746
+ * derived mirror so the runtime keeps consuming its existing format.
747
+ */
748
+ var Manager = class {
749
+ store;
750
+ executor;
751
+ logger;
752
+ mirrorDebounceMs;
753
+ mirrorTimer;
754
+ mirrorPromise = Promise.resolve();
755
+ mirrorPending;
756
+ bundler;
757
+ changeListeners = /* @__PURE__ */ new Set();
758
+ storeUnsubscribe;
759
+ constructor(opts = {}) {
760
+ this.store = opts.store ?? new Store({ logger: opts.logger });
761
+ this.executor = opts.executor ?? defaultOpenclawExecutor;
762
+ this.logger = opts.logger;
763
+ this.mirrorDebounceMs = opts.mirrorDebounceMs ?? DEFAULT_MIRROR_DEBOUNCE_MS;
764
+ }
765
+ /** Direct accessor — handy for tests and the CLI's `alfe mcp list`. */
766
+ getStore() {
767
+ return this.store;
768
+ }
769
+ /**
770
+ * Register or overwrite a server entry. Resolves as soon as the store
771
+ * mutation is committed to disk — the openclaw.json mirror runs async
772
+ * in the background and is debounced so back-to-back calls coalesce
773
+ * into one runtime restart. Call `flush()` to await the mirror.
774
+ */
775
+ async addServer(config, opts) {
776
+ if (!opts.id) throw new Error("Manager.addServer: id is required");
777
+ const owner = opts.owner ?? "manual";
778
+ this.store.update((cur) => {
779
+ const previousAddedAt = lookupAddedAt(cur.servers, opts.id);
780
+ const entry = toStoredEntry(config, {
781
+ owner,
782
+ transport: opts.transport,
783
+ version: opts.version,
784
+ addedAt: previousAddedAt
785
+ });
786
+ return {
787
+ ...cur,
788
+ servers: {
789
+ ...cur.servers,
790
+ [opts.id]: entry
791
+ }
792
+ };
793
+ });
794
+ this.scheduleMirror();
795
+ this.fireChange();
796
+ return Promise.resolve();
797
+ }
798
+ /**
799
+ * Remove a single server entry. No-op if the id isn't in the store.
800
+ * Refuses to remove an entry whose owner doesn't match `expectedOwner`
801
+ * when supplied — the CLI uses this to guard `alfe mcp remove` from
802
+ * accidentally clobbering integration- or cli-owned entries.
803
+ *
804
+ * Resolves as soon as the store mutation is committed. Mirror runs
805
+ * async; call `flush()` to await it.
806
+ */
807
+ removeServer(id, opts = {}) {
808
+ const existing = lookupEntry(this.store.read().servers, id);
809
+ if (!existing) return Promise.resolve(false);
810
+ if (opts.expectedOwner && existing.owner !== opts.expectedOwner) return Promise.reject(/* @__PURE__ */ new Error(`Manager.removeServer: server "${id}" is owned by "${existing.owner}", not "${opts.expectedOwner}"`));
811
+ this.store.update((cur) => ({
812
+ ...cur,
813
+ servers: Object.fromEntries(Object.entries(cur.servers).filter(([k]) => k !== id))
814
+ }));
815
+ this.scheduleMirror();
816
+ this.fireChange();
817
+ return Promise.resolve(true);
818
+ }
819
+ /** Drop every entry whose owner matches — used by integration uninstall. */
820
+ async removeServersByOwner(owner) {
821
+ const removed = [];
822
+ this.store.update((cur) => {
823
+ const next = {};
824
+ for (const [id, entry] of Object.entries(cur.servers)) if (entry.owner === owner) removed.push(id);
825
+ else next[id] = entry;
826
+ if (removed.length === 0) return cur;
827
+ return {
828
+ ...cur,
829
+ servers: next
830
+ };
831
+ });
832
+ if (removed.length > 0) {
833
+ this.scheduleMirror();
834
+ this.fireChange();
835
+ }
836
+ return Promise.resolve(removed);
837
+ }
838
+ /** Read-only snapshot for `alfe mcp list` and similar UIs. */
839
+ listServers() {
840
+ const snap = this.store.read();
841
+ return Object.entries(snap.servers).map(([id, entry]) => ({
842
+ id,
843
+ entry
844
+ }));
845
+ }
846
+ /**
847
+ * Push the current store contents into a bundler instance (which owns
848
+ * connections / tools). Wires up a store watcher so external mutations
849
+ * (e.g. another shell running `alfe mcp add`) re-reconcile.
850
+ */
851
+ async loadIntoBundler(bundler) {
852
+ this.bundler = bundler;
853
+ await this.reconcileBundler();
854
+ this.storeUnsubscribe ??= this.store.watch(() => {
855
+ this.reconcileBundler().catch((err) => {
856
+ this.logger?.warn("[mcp-bundler/manager] watcher reconcile failed", { err: errMsg(err) });
857
+ });
858
+ });
859
+ }
860
+ /** Subscribe to store mutations. Returns an unsubscribe fn. */
861
+ onChange(cb) {
862
+ this.changeListeners.add(cb);
863
+ return () => {
864
+ this.changeListeners.delete(cb);
865
+ };
866
+ }
867
+ /**
868
+ * Cancel any pending mirror-write, flush the in-flight one, and stop
869
+ * watching the store. Safe to call multiple times.
870
+ */
871
+ async dispose() {
872
+ if (this.mirrorTimer) {
873
+ clearTimeout(this.mirrorTimer);
874
+ this.mirrorTimer = void 0;
875
+ await this.runMirror();
876
+ }
877
+ await this.mirrorPromise.catch(() => void 0);
878
+ if (this.storeUnsubscribe) {
879
+ this.storeUnsubscribe();
880
+ this.storeUnsubscribe = void 0;
881
+ }
882
+ this.store.dispose();
883
+ this.changeListeners.clear();
884
+ this.bundler = void 0;
885
+ }
886
+ /**
887
+ * Force the debounced mirror to run now and wait for it to finish.
888
+ * Surfaces the executor error if the mirror failed — callers wrap in
889
+ * try/catch (or .rejects in tests) if they need to handle it.
890
+ */
891
+ async flush() {
892
+ if (this.mirrorTimer) {
893
+ clearTimeout(this.mirrorTimer);
894
+ this.mirrorTimer = void 0;
895
+ await this.runMirror();
896
+ }
897
+ await this.mirrorPromise;
898
+ }
899
+ scheduleMirror() {
900
+ if (this.mirrorPending) return;
901
+ this.mirrorPromise = new Promise((resolve, reject) => {
902
+ this.mirrorPending = {
903
+ resolve,
904
+ reject
905
+ };
906
+ });
907
+ this.mirrorTimer = setTimeout(() => {
908
+ this.mirrorTimer = void 0;
909
+ this.runMirror();
910
+ }, this.mirrorDebounceMs);
911
+ this.mirrorTimer.unref();
912
+ }
913
+ async runMirror() {
914
+ const pending = this.mirrorPending;
915
+ this.mirrorPending = void 0;
916
+ try {
917
+ await this.applyMirror();
918
+ pending?.resolve();
919
+ } catch (err) {
920
+ pending?.reject(err);
921
+ this.logger?.error("[mcp-bundler/manager] mirror write failed", { err: errMsg(err) });
922
+ }
923
+ }
924
+ /**
925
+ * Compute the diff between this manager's owned set and what the store
926
+ * declares now, then apply the openclaw config delta. Foreign keys
927
+ * (entries in openclaw.json#mcp.servers.* not in our store) are
928
+ * preserved — we only touch the names we previously claimed.
929
+ */
930
+ async applyMirror() {
931
+ let previousOwned = [];
932
+ let desiredOwned = [];
933
+ let batch = [];
934
+ this.store.update((cur) => {
935
+ previousOwned = cur._ownedOpenclawKeys.slice();
936
+ const entries = Object.entries(cur.servers);
937
+ desiredOwned = entries.map(([id]) => id).sort();
938
+ batch = entries.flatMap(([id, entry]) => renderEntryToBatch(id, entry));
939
+ return {
940
+ ...cur,
941
+ _ownedOpenclawKeys: desiredOwned
942
+ };
943
+ });
944
+ await this.executor.setBatch(batch);
945
+ const toUnset = previousOwned.filter((k) => !desiredOwned.includes(k));
946
+ for (const id of toUnset) try {
947
+ await this.executor.unset(`mcp.servers.${id}`);
948
+ } catch (err) {
949
+ this.logger?.warn("[mcp-bundler/manager] mirror unset failed (continuing)", {
950
+ err: errMsg(err),
951
+ key: `mcp.servers.${id}`
952
+ });
953
+ }
954
+ if (this.bundler) try {
955
+ await this.reconcileBundler();
956
+ } catch (err) {
957
+ this.logger?.warn("[mcp-bundler/manager] post-mirror reconcile failed", { err: errMsg(err) });
958
+ }
959
+ }
960
+ async reconcileBundler() {
961
+ if (!this.bundler) return;
962
+ const snap = this.store.read();
963
+ const servers = {};
964
+ for (const [id, entry] of Object.entries(snap.servers)) servers[id] = toServerConfig(entry);
965
+ await this.bundler.reconcile(servers);
966
+ }
967
+ fireChange() {
968
+ for (const cb of this.changeListeners) try {
969
+ cb();
970
+ } catch (err) {
971
+ this.logger?.warn("[mcp-bundler/manager] onChange listener threw", { err: errMsg(err) });
972
+ }
973
+ }
974
+ };
975
+ /**
976
+ * Render one store entry as the set of dotted-path batch entries that
977
+ * `openclaw config set --batch-json` expects.
978
+ *
979
+ * Mirrors the format the integrations applier already emits today so the
980
+ * runtime sees the same shape regardless of which writer produced it.
981
+ */
982
+ function renderEntryToBatch(id, entry) {
983
+ const prefix = `mcp.servers.${id}`;
984
+ const out = [];
985
+ if (entry.transport === "stdio") {
986
+ out.push({
987
+ path: `${prefix}.command`,
988
+ value: entry.command
989
+ });
990
+ if (entry.args && entry.args.length > 0) out.push({
991
+ path: `${prefix}.args`,
992
+ value: entry.args
993
+ });
994
+ if (entry.env) for (const [k, v] of Object.entries(entry.env)) out.push({
995
+ path: `${prefix}.env.${k}`,
996
+ value: v
997
+ });
998
+ if (entry.cwd) out.push({
999
+ path: `${prefix}.cwd`,
1000
+ value: entry.cwd
1001
+ });
1002
+ return out;
1003
+ }
1004
+ out.push({
1005
+ path: `${prefix}.url`,
1006
+ value: entry.url
1007
+ });
1008
+ out.push({
1009
+ path: `${prefix}.transport`,
1010
+ value: entry.transport
1011
+ });
1012
+ if (entry.headers) for (const [k, v] of Object.entries(entry.headers)) out.push({
1013
+ path: `${prefix}.headers.${k}`,
1014
+ value: v
1015
+ });
1016
+ if (entry.connectionTimeoutMs !== void 0) out.push({
1017
+ path: `${prefix}.connectionTimeoutMs`,
1018
+ value: entry.connectionTimeoutMs
1019
+ });
1020
+ return out;
1021
+ }
1022
+ function errMsg(err) {
1023
+ return err instanceof Error ? err.message : String(err);
1024
+ }
1025
+ /**
1026
+ * Indexed access on `Record<string, T>` returns `T` (not `T | undefined`)
1027
+ * unless `noUncheckedIndexedAccess` is set in tsconfig. These helpers
1028
+ * make the optional-ness explicit so the lint rules that hate
1029
+ * always-truthy conditionals stop firing on real lookups.
1030
+ */
1031
+ function lookupEntry(servers, id) {
1032
+ return Object.hasOwn(servers, id) ? servers[id] : void 0;
1033
+ }
1034
+ function lookupAddedAt(servers, id) {
1035
+ const entry = lookupEntry(servers, id);
1036
+ return entry ? entry.addedAt : void 0;
1037
+ }
1038
+ //#endregion
453
1039
  exports.Connection = Connection;
1040
+ exports.Manager = Manager;
454
1041
  exports.McpBundler = McpBundler;
455
1042
  exports.STDIO_ENV_DENYLIST = STDIO_ENV_DENYLIST;
1043
+ exports.Store = Store;
456
1044
  exports.buildNamespacedToolName = buildNamespacedToolName;
457
1045
  exports.defaultConnect = defaultConnect;
1046
+ exports.defaultOpenclawExecutor = defaultOpenclawExecutor;
1047
+ exports.defaultStorePath = defaultStorePath;
458
1048
  exports.disambiguateAgainst = disambiguateAgainst;
459
1049
  exports.sanitizeNameSegment = sanitizeNameSegment;
460
1050
  exports.sanitizeStdioEnv = sanitizeStdioEnv;
1051
+ exports.toServerConfig = toServerConfig;
1052
+ exports.toStoredEntry = toStoredEntry;