@memoraone/mcp 0.1.57 → 0.1.58

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/cli.cjs CHANGED
@@ -231,7 +231,7 @@ var require_package = __commonJS({
231
231
  "package.json"(exports2, module2) {
232
232
  module2.exports = {
233
233
  name: "@memoraone/mcp",
234
- version: "0.1.57",
234
+ version: "0.1.58",
235
235
  type: "module",
236
236
  main: "dist/index.cjs",
237
237
  exports: {
package/dist/gateway.cjs CHANGED
@@ -2233,6 +2233,21 @@ async function reconcileResolvedBindingWithDisk(cached, options = {}) {
2233
2233
  }
2234
2234
  return { binding: fresh, cacheRefreshed: true };
2235
2235
  }
2236
+ async function checkRuntimeBindingValidity(cached, options = {}) {
2237
+ try {
2238
+ const { binding: current } = await reconcileResolvedBindingWithDisk(cached, options);
2239
+ if (current.status !== "connected") {
2240
+ return { valid: false, reason: `binding status is ${current.status}` };
2241
+ }
2242
+ if (!bindingRelevantValuesMatch(cached, current)) {
2243
+ return { valid: false, reason: "binding metadata changed" };
2244
+ }
2245
+ return { valid: true };
2246
+ } catch (err) {
2247
+ const reason = err instanceof Error && err.message.trim() ? err.message : String(err);
2248
+ return { valid: false, reason };
2249
+ }
2250
+ }
2236
2251
  function encodeResolvedBinding(binding) {
2237
2252
  const payload = {
2238
2253
  repositoryBindingId: binding.repositoryBindingId,
@@ -11739,12 +11754,330 @@ var path50 = __toESM(require("path"), 1);
11739
11754
  var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
11740
11755
  var import_types2 = require("@modelcontextprotocol/sdk/types.js");
11741
11756
 
11757
+ // src/heartbeat.ts
11758
+ var crypto4 = __toESM(require("crypto"), 1);
11759
+ var fs37 = __toESM(require("fs"), 1);
11760
+ var path48 = __toESM(require("path"), 1);
11761
+
11762
+ // src/localState/mcpSessionId.ts
11763
+ var import_node_crypto29 = require("crypto");
11764
+ var MCS_PREFIX = "mcs_";
11765
+ function generateMcpSessionId(random = () => (0, import_node_crypto29.randomBytes)(32)) {
11766
+ const bytes = random();
11767
+ if (bytes.length !== 32) {
11768
+ throw new Error("[memoraone-mcp] mcp session id requires exactly 32 random bytes");
11769
+ }
11770
+ return `${MCS_PREFIX}${bytes.toString("base64url")}`;
11771
+ }
11772
+
11773
+ // src/heartbeat.ts
11774
+ function fingerprintAccessToken(accessToken) {
11775
+ return crypto4.createHash("sha256").update(accessToken).digest("hex").slice(0, 12);
11776
+ }
11777
+ function isHeartbeatDebugEnabled() {
11778
+ const value = String(process.env.MEMORAONE_DEBUG_HEARTBEAT ?? "").trim().toLowerCase();
11779
+ return ["1", "true", "yes", "on"].includes(value);
11780
+ }
11781
+ var LOCAL_MCP_HEARTBEAT_INTERVAL_MS = 1e3;
11782
+ function resolveHeartbeatIntervalMs() {
11783
+ return LOCAL_MCP_HEARTBEAT_INTERVAL_MS;
11784
+ }
11785
+ function redactSensitiveText(text2) {
11786
+ return text2.replace(/mcs_[A-Za-z0-9_-]+/g, "mcs_[redacted]").replace(/mia_[A-Za-z0-9_-]+/g, "mia_[redacted]").replace(/mir_[A-Za-z0-9_-]+/g, "mir_[redacted]").replace(/mcc_[A-Za-z0-9_-]+/g, "mcc_[redacted]").replace(/Bearer\s+\S+/gi, "Bearer [redacted]");
11787
+ }
11788
+ function resolvePackageVersion() {
11789
+ const fromEnv = process.env.npm_package_version?.trim();
11790
+ if (fromEnv) return fromEnv;
11791
+ const moduleDir = typeof __dirname !== "undefined" ? __dirname : void 0;
11792
+ const candidates = [
11793
+ ...moduleDir ? [path48.join(moduleDir, "..", "package.json"), path48.join(moduleDir, "package.json")] : [],
11794
+ path48.join(process.cwd(), "package.json"),
11795
+ path48.join(process.cwd(), "packages", "mcp", "package.json")
11796
+ ];
11797
+ for (const candidate of candidates) {
11798
+ try {
11799
+ const pkg = JSON.parse(fs37.readFileSync(candidate, "utf8"));
11800
+ if (typeof pkg.version === "string" && pkg.version.trim()) {
11801
+ return pkg.version.trim();
11802
+ }
11803
+ } catch {
11804
+ }
11805
+ }
11806
+ return void 0;
11807
+ }
11808
+ function parseActiveFlag(payload) {
11809
+ if (!payload || typeof payload !== "object") return null;
11810
+ const active = payload.active;
11811
+ if (typeof active === "boolean") return active;
11812
+ return null;
11813
+ }
11814
+ async function announceIdeSession(client, ctx) {
11815
+ const body = {
11816
+ session_id: ctx.sessionId,
11817
+ ide_type: ctx.ideType
11818
+ };
11819
+ if (ctx.packageVersion) {
11820
+ body.package_version = ctx.packageVersion;
11821
+ }
11822
+ let lastErr;
11823
+ for (let attempt = 0; attempt < 2; attempt++) {
11824
+ try {
11825
+ const raw = await client.post("/v1/local-mcp/session/announce", body, { log: false });
11826
+ const data = raw ?? {};
11827
+ if (data.ok !== true || typeof data.active !== "boolean" || typeof data.ide_type !== "string") {
11828
+ throw new Error("[memoraone-mcp] Invalid session announce response");
11829
+ }
11830
+ return {
11831
+ ok: true,
11832
+ active: data.active,
11833
+ ideType: data.ide_type,
11834
+ announcedAt: typeof data.announced_at === "string" ? data.announced_at : ""
11835
+ };
11836
+ } catch (err) {
11837
+ lastErr = err;
11838
+ if (err instanceof MemoraOneHttpError && (err.status === 404 || err.status === 405)) {
11839
+ throw new Error(
11840
+ "[memoraone-mcp] Session announce endpoint is not supported by this API (POST /v1/local-mcp/session/announce)"
11841
+ );
11842
+ }
11843
+ const msg = String(err);
11844
+ const transient = /fetch failed|ECONNRESET|ETIMEDOUT|ENOTFOUND|network|socket/i.test(msg) && !(err instanceof MemoraOneHttpError && err.status >= 400 && err.status < 500);
11845
+ if (!transient || attempt === 1) {
11846
+ break;
11847
+ }
11848
+ }
11849
+ }
11850
+ const safe = redactSensitiveText(String(lastErr));
11851
+ throw new Error(`[memoraone-mcp] Session announce failed: ${safe}`);
11852
+ }
11853
+ async function sendProjectHeartbeat(client, ctx) {
11854
+ try {
11855
+ const pid = ctx.projectId?.trim();
11856
+ if (isHeartbeatDebugEnabled()) {
11857
+ process.stderr.write(
11858
+ `[memoraone-mcp][diag] heartbeat projectId=${pid ?? "unknown"} binding=${ctx.repositoryBindingId} credentialSource=${ctx.credentialSource ?? "keyring"} accessTokenFingerprint=${ctx.accessTokenFingerprint ?? "unknown"} ideType=${ctx.ideType ?? "unknown"}
11859
+ `
11860
+ );
11861
+ }
11862
+ if (!pid) {
11863
+ throw new Error("[memoraone-mcp] Cannot send heartbeat without an active project binding");
11864
+ }
11865
+ const body = {};
11866
+ if (ctx.ideType) body.ide_type = ctx.ideType;
11867
+ if (ctx.sessionId) body.session_id = ctx.sessionId;
11868
+ const raw = await client.post("/v1/local-mcp/heartbeat", body, { log: false });
11869
+ return { active: parseActiveFlag(raw) };
11870
+ } catch (err) {
11871
+ process.stderr.write(
11872
+ `[memoraone-mcp][info] heartbeat error (silent) ${redactSensitiveText(String(err))}
11873
+ `
11874
+ );
11875
+ return { active: null };
11876
+ }
11877
+ }
11878
+ function createDaemonHeartbeat(opts) {
11879
+ let interval = null;
11880
+ let runGeneration = 0;
11881
+ let client = null;
11882
+ let announced = false;
11883
+ let studioActive = null;
11884
+ let starting = false;
11885
+ let tickInFlight = false;
11886
+ let bindingAuthorityEnded = false;
11887
+ let waitingForIdeType = false;
11888
+ const sessionId = opts.sessionId ?? generateMcpSessionId();
11889
+ const packageVersion = opts.packageVersion ?? resolvePackageVersion();
11890
+ const ctx = {
11891
+ projectId: opts.binding.projectId,
11892
+ repositoryBindingId: opts.binding.repositoryBindingId,
11893
+ ideType: opts.ideType,
11894
+ sessionId,
11895
+ credentialSource: "keyring",
11896
+ accessTokenFingerprint: null
11897
+ };
11898
+ const log4 = opts.onLog ?? ((msg) => {
11899
+ process.stderr.write(`[memoraone-mcp][daemon-heartbeat] ${redactSensitiveText(msg)}
11900
+ `);
11901
+ });
11902
+ const validateBinding = opts.validateBinding ?? checkRuntimeBindingValidity;
11903
+ const endBindingAuthority = (reason) => {
11904
+ if (bindingAuthorityEnded) return;
11905
+ bindingAuthorityEnded = true;
11906
+ studioActive = false;
11907
+ runGeneration += 1;
11908
+ if (interval) {
11909
+ clearInterval(interval);
11910
+ interval = null;
11911
+ }
11912
+ log4(`heartbeat authority ended: ${redactSensitiveText(reason)}`);
11913
+ };
11914
+ const bindingIsValid = async () => {
11915
+ try {
11916
+ const result = await validateBinding(opts.binding);
11917
+ if (result.valid) return true;
11918
+ endBindingAuthority("reason" in result ? result.reason : "binding is no longer valid");
11919
+ return false;
11920
+ } catch (err) {
11921
+ endBindingAuthority(`binding validation failed: ${String(err)}`);
11922
+ return false;
11923
+ }
11924
+ };
11925
+ const ensureClient = async () => {
11926
+ if (client) return client;
11927
+ const creds = await readInstallationCredentials(opts.binding.repositoryBindingId);
11928
+ if (!creds?.accessToken) {
11929
+ log4("cannot start: no access token in keyring");
11930
+ return null;
11931
+ }
11932
+ ctx.accessTokenFingerprint = fingerprintAccessToken(creds.accessToken);
11933
+ client = opts.createClient?.(opts.binding) ?? new memoraClient_default(config2, {
11934
+ repositoryBindingId: opts.binding.repositoryBindingId,
11935
+ projectId: opts.binding.projectId
11936
+ });
11937
+ return client;
11938
+ };
11939
+ const ensureAnnounced = async (activeClient) => {
11940
+ if (announced) return true;
11941
+ if (!ctx.ideType) {
11942
+ waitingForIdeType = true;
11943
+ log4("waiting for ide type before session announce");
11944
+ return false;
11945
+ }
11946
+ waitingForIdeType = false;
11947
+ try {
11948
+ const result = await announceIdeSession(activeClient, {
11949
+ sessionId,
11950
+ ideType: ctx.ideType,
11951
+ packageVersion
11952
+ });
11953
+ announced = true;
11954
+ studioActive = result.active;
11955
+ log4(`session announced ideType=${ctx.ideType} active=${String(result.active)}`);
11956
+ return true;
11957
+ } catch (err) {
11958
+ studioActive = false;
11959
+ log4(`session announce failed: ${redactSensitiveText(String(err))}`);
11960
+ return false;
11961
+ }
11962
+ };
11963
+ const applyHeartbeatOutcome = (outcome) => {
11964
+ if (outcome.active === null) return;
11965
+ if (outcome.active === false && studioActive !== false) {
11966
+ studioActive = false;
11967
+ log4("session superseded (active=false); continuing to serve MCP without re-announce");
11968
+ return;
11969
+ }
11970
+ if (outcome.active === true) {
11971
+ studioActive = true;
11972
+ }
11973
+ };
11974
+ const tick = async (generation) => {
11975
+ if (!client || generation !== runGeneration || bindingAuthorityEnded || tickInFlight) return;
11976
+ tickInFlight = true;
11977
+ try {
11978
+ if (!await bindingIsValid()) return;
11979
+ if (generation !== runGeneration || !interval) return;
11980
+ const outcome = await sendProjectHeartbeat(client, ctx);
11981
+ if (generation !== runGeneration || !interval) return;
11982
+ applyHeartbeatOutcome(outcome);
11983
+ } finally {
11984
+ tickInFlight = false;
11985
+ }
11986
+ };
11987
+ const beginInterval = () => {
11988
+ if (interval) return;
11989
+ const intervalMs = resolveHeartbeatIntervalMs();
11990
+ const generation = runGeneration;
11991
+ log4(
11992
+ `daemon owns heartbeat for binding=${opts.binding.repositoryBindingId} project=${opts.binding.projectId} ideType=${ctx.ideType ?? "unknown"} interval=${intervalMs}ms`
11993
+ );
11994
+ interval = setInterval(() => {
11995
+ void tick(generation);
11996
+ }, intervalMs);
11997
+ interval.unref?.();
11998
+ void tick(generation);
11999
+ };
12000
+ const start = async () => {
12001
+ if (!config2.heartbeatEnabled) {
12002
+ log4("disabled by config");
12003
+ return;
12004
+ }
12005
+ if (interval) {
12006
+ log4("already running (skipped duplicate start)");
12007
+ return;
12008
+ }
12009
+ if (starting) {
12010
+ log4("already starting (skipped duplicate start)");
12011
+ return;
12012
+ }
12013
+ if (bindingAuthorityEnded) {
12014
+ log4("binding authority already ended; heartbeat will not restart");
12015
+ return;
12016
+ }
12017
+ starting = true;
12018
+ try {
12019
+ if (!await bindingIsValid()) return;
12020
+ const activeClient = await ensureClient();
12021
+ if (!activeClient) return;
12022
+ const ok = await ensureAnnounced(activeClient);
12023
+ if (!ok) {
12024
+ return;
12025
+ }
12026
+ beginInterval();
12027
+ } finally {
12028
+ starting = false;
12029
+ if (waitingForIdeType && !announced && !interval && ctx.ideType && config2.heartbeatEnabled) {
12030
+ waitingForIdeType = false;
12031
+ void start();
12032
+ }
12033
+ }
12034
+ };
12035
+ const stop = () => {
12036
+ runGeneration += 1;
12037
+ if (interval) {
12038
+ clearInterval(interval);
12039
+ interval = null;
12040
+ }
12041
+ log4(`daemon released heartbeat for binding=${opts.binding.repositoryBindingId}`);
12042
+ };
12043
+ const isRunning = () => interval !== null;
12044
+ const setIdeType = (ideType) => {
12045
+ const previous = ctx.ideType;
12046
+ if (previous === ideType) {
12047
+ return;
12048
+ }
12049
+ ctx.ideType = ideType;
12050
+ log4(`daemon heartbeat ideType updated to ${ideType}`);
12051
+ if (!announced) {
12052
+ void start();
12053
+ return;
12054
+ }
12055
+ if (client && interval) {
12056
+ void tick(runGeneration);
12057
+ }
12058
+ };
12059
+ const getIdeType = () => ctx.ideType;
12060
+ const getSessionId = () => sessionId;
12061
+ const isStudioActive = () => studioActive;
12062
+ const hasAnnounced = () => announced;
12063
+ return {
12064
+ start,
12065
+ stop,
12066
+ isRunning,
12067
+ setIdeType,
12068
+ getIdeType,
12069
+ getSessionId,
12070
+ isStudioActive,
12071
+ hasAnnounced
12072
+ };
12073
+ }
12074
+
11742
12075
  // src/memoraMcpServer.ts
11743
12076
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
11744
12077
  var import_types = require("@modelcontextprotocol/sdk/types.js");
11745
12078
 
11746
12079
  // src/pluginStartupBindingWait.ts
11747
- var path48 = __toESM(require("path"), 1);
12080
+ var path49 = __toESM(require("path"), 1);
11748
12081
  var MEMORAONE_PLUGIN_ENV = "MEMORAONE_PLUGIN";
11749
12082
  var MEMORAONE_CURSOR_PLUGIN = "cursor";
11750
12083
  var PLUGIN_STARTUP_BINDING_WAIT_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -11805,7 +12138,7 @@ function collectPluginStartupBindingRoots(options) {
11805
12138
  if (raw === void 0 || raw === null || raw.trim() === "") {
11806
12139
  return;
11807
12140
  }
11808
- const resolved = path48.resolve(raw);
12141
+ const resolved = path49.resolve(raw);
11809
12142
  if (seen.has(resolved)) {
11810
12143
  return;
11811
12144
  }
@@ -12098,11 +12431,11 @@ var LOCAL_MCP_TOOL_NAMES = [
12098
12431
 
12099
12432
  // src/tools/handlers/postEvent.ts
12100
12433
  var import_v415 = require("zod/v4");
12101
- var crypto5 = __toESM(require("crypto"), 1);
12434
+ var crypto6 = __toESM(require("crypto"), 1);
12102
12435
 
12103
12436
  // src/runContext.ts
12104
12437
  var import_node_async_hooks = require("async_hooks");
12105
- var crypto4 = __toESM(require("crypto"), 1);
12438
+ var crypto5 = __toESM(require("crypto"), 1);
12106
12439
  var sessionContextStorage = new import_node_async_hooks.AsyncLocalStorage();
12107
12440
  function createSessionRunContext(initial = {}) {
12108
12441
  return {
@@ -12149,7 +12482,7 @@ function resolveRunId(passed) {
12149
12482
  return getCurrentRunId();
12150
12483
  }
12151
12484
  function generateRunId() {
12152
- return crypto4.randomBytes(16).toString("hex");
12485
+ return crypto5.randomBytes(16).toString("hex");
12153
12486
  }
12154
12487
 
12155
12488
  // src/tools/handlers/postEvent.ts
@@ -12189,7 +12522,7 @@ function buildPostEventContentFields(content) {
12189
12522
  return { message: JSON.stringify(content) };
12190
12523
  }
12191
12524
  async function handlePostEvent(client, args) {
12192
- const nonce = crypto5.randomBytes(8).toString("hex");
12525
+ const nonce = crypto6.randomBytes(8).toString("hex");
12193
12526
  console.error(
12194
12527
  `[memoraone-mcp][debug] tool=memora_post_event toolCallId=unknown nonce=${nonce} stage=before_post`
12195
12528
  );
@@ -12899,19 +13232,6 @@ async function handleFinishSetup(options) {
12899
13232
  };
12900
13233
  }
12901
13234
 
12902
- // src/heartbeat.ts
12903
- var crypto6 = __toESM(require("crypto"), 1);
12904
- var fs37 = __toESM(require("fs"), 1);
12905
- var path49 = __toESM(require("path"), 1);
12906
-
12907
- // src/localState/mcpSessionId.ts
12908
- var import_node_crypto29 = require("crypto");
12909
-
12910
- // src/heartbeat.ts
12911
- function fingerprintAccessToken(accessToken) {
12912
- return crypto6.createHash("sha256").update(accessToken).digest("hex").slice(0, 12);
12913
- }
12914
-
12915
13235
  // src/ideType.ts
12916
13236
  var RELIABLE_CLIENT_INFO_NAMES = /* @__PURE__ */ new Map([
12917
13237
  // Existing
@@ -13653,6 +13973,66 @@ var log2 = (msg) => {
13653
13973
  `);
13654
13974
  };
13655
13975
  var sessionsById = /* @__PURE__ */ new Map();
13976
+ function isGatewayHeartbeatEnabled(env2) {
13977
+ if (!config2.heartbeatEnabled) return false;
13978
+ const raw = env2?.MEMORAONE_HEARTBEAT;
13979
+ if (raw === void 0) return true;
13980
+ const normalized = String(raw).trim().toLowerCase();
13981
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
13982
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
13983
+ return true;
13984
+ }
13985
+ function stopSessionHeartbeat(session) {
13986
+ if (!session.heartbeat) return;
13987
+ session.heartbeat.stop();
13988
+ session.heartbeat = null;
13989
+ }
13990
+ function releaseSession(session) {
13991
+ session.closed = true;
13992
+ stopSessionHeartbeat(session);
13993
+ }
13994
+ async function attachGatewayMcpSessionHeartbeat(session, handle, options = {}) {
13995
+ if (session.heartbeatAttach) {
13996
+ await session.heartbeatAttach;
13997
+ return;
13998
+ }
13999
+ session.heartbeatAttach = (async () => {
14000
+ let activeClient;
14001
+ try {
14002
+ activeClient = await handle.bindingReady;
14003
+ } catch {
14004
+ return;
14005
+ }
14006
+ if (session.closed) return;
14007
+ if (!activeClient) return;
14008
+ const boundClient = activeClient;
14009
+ if (!handle.runtime.authoritativeBinding) return;
14010
+ if (!isGatewayHeartbeatEnabled(options.env)) return;
14011
+ if (session.heartbeat) return;
14012
+ const heartbeat = createDaemonHeartbeat({
14013
+ binding: handle.runtime.authoritativeBinding,
14014
+ ideType: handle.runtime.ideType ?? config2.ideType,
14015
+ createClient: () => boundClient,
14016
+ validateBinding: (binding) => checkRuntimeBindingValidity(binding, { homeDir: options.homeDir }),
14017
+ onLog: (msg) => {
14018
+ log2(`[session-heartbeat] ${msg}`);
14019
+ }
14020
+ });
14021
+ session.heartbeat = heartbeat;
14022
+ if (session.closed) {
14023
+ stopSessionHeartbeat(session);
14024
+ return;
14025
+ }
14026
+ log2(
14027
+ `${handle.sessionLabel} owns heartbeat for project ${handle.runtime.projectId}`
14028
+ );
14029
+ await heartbeat.start();
14030
+ if (session.closed) {
14031
+ stopSessionHeartbeat(session);
14032
+ }
14033
+ })();
14034
+ await session.heartbeatAttach;
14035
+ }
13656
14036
  function sendJsonRpcHttpError(res, status, message) {
13657
14037
  if (res.headersSent) return;
13658
14038
  const body = JSON.stringify({
@@ -13744,6 +14124,7 @@ async function removeSession(sessionId) {
13744
14124
  const session = sessionsById.get(sessionId);
13745
14125
  if (!session) return;
13746
14126
  sessionsById.delete(sessionId);
14127
+ releaseSession(session);
13747
14128
  try {
13748
14129
  await session.transport.close();
13749
14130
  } catch {
@@ -13779,7 +14160,11 @@ async function createSession(binding, canonicalUrl, options) {
13779
14160
  sessionsById.set(sessionId, sessionRef);
13780
14161
  },
13781
14162
  onsessionclosed: async (sessionId) => {
13782
- sessionsById.delete(sessionId);
14163
+ const existing = sessionsById.get(sessionId);
14164
+ if (existing) {
14165
+ sessionsById.delete(sessionId);
14166
+ releaseSession(existing);
14167
+ }
13783
14168
  }
13784
14169
  });
13785
14170
  const handle = createMemoraMcpServer({
@@ -13789,15 +14174,24 @@ async function createSession(binding, canonicalUrl, options) {
13789
14174
  bindingResolveOptions: {
13790
14175
  homeDir: options.homeDir,
13791
14176
  identityDeps: options.identityDeps
14177
+ },
14178
+ onSessionIdeTypeKnown: (ideType) => {
14179
+ sessionRef?.heartbeat?.setIdeType(ideType);
13792
14180
  }
13793
14181
  });
13794
- void handle.bindingReady.catch(() => {
13795
- });
13796
14182
  await handle.server.connect(transport);
13797
14183
  transport.onclose = () => {
13798
14184
  const sid = transport.sessionId;
13799
14185
  if (sid) {
13800
- sessionsById.delete(sid);
14186
+ const existing = sessionsById.get(sid);
14187
+ if (existing) {
14188
+ sessionsById.delete(sid);
14189
+ releaseSession(existing);
14190
+ } else if (sessionRef && !sessionRef.closed) {
14191
+ releaseSession(sessionRef);
14192
+ }
14193
+ } else if (sessionRef) {
14194
+ releaseSession(sessionRef);
13801
14195
  }
13802
14196
  };
13803
14197
  sessionRef = {
@@ -13806,12 +14200,26 @@ async function createSession(binding, canonicalUrl, options) {
13806
14200
  canonicalUrl,
13807
14201
  binding,
13808
14202
  transport,
14203
+ heartbeat: null,
14204
+ heartbeatAttach: null,
14205
+ closed: false,
13809
14206
  close: async () => {
13810
14207
  const sid = transport.sessionId;
13811
- if (sid) sessionsById.delete(sid);
13812
- await transport.close();
14208
+ if (sid && sessionsById.get(sid) === sessionRef) {
14209
+ await removeSession(sid);
14210
+ return;
14211
+ }
14212
+ releaseSession(sessionRef);
14213
+ try {
14214
+ await transport.close();
14215
+ } catch {
14216
+ }
13813
14217
  }
13814
14218
  };
14219
+ void attachGatewayMcpSessionHeartbeat(sessionRef, handle, options).catch(
14220
+ () => {
14221
+ }
14222
+ );
13815
14223
  return sessionRef;
13816
14224
  }
13817
14225
  async function handleAuthenticatedGatewayMcpRequest(req, res, auth, options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memoraone/mcp",
3
- "version": "0.1.57",
3
+ "version": "0.1.58",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "exports": {