@memoraone/mcp 0.1.56 → 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.56",
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,
@@ -11218,8 +11233,9 @@ function delay(ms) {
11218
11233
  return new Promise((resolve47) => setTimeout(resolve47, ms));
11219
11234
  }
11220
11235
  function buildProtectedResourceMetadata(env2 = process.env, workspaceRoot) {
11236
+ const externalIssuer = Boolean(env2.MEMORAONE_OAUTH_ISSUER_URL?.trim());
11221
11237
  return {
11222
- resource: workspaceRoot === void 0 ? getGatewayResourceBaseUrl(env2) : buildGatewayMcpUrl(workspaceRoot, env2),
11238
+ resource: workspaceRoot === void 0 || externalIssuer ? getGatewayResourceBaseUrl(env2) : buildGatewayMcpUrl(workspaceRoot, env2),
11223
11239
  authorization_servers: [getGatewayAuthorizationServerIssuerUrl(env2)]
11224
11240
  };
11225
11241
  }
@@ -11350,17 +11366,17 @@ var GatewayOAuthSurface = class {
11350
11366
  */
11351
11367
  async handleProtectedResourceMetadata(res, url) {
11352
11368
  const wsOrError = getGatewayWsQueryParam(url);
11353
- if (wsOrError !== null && typeof wsOrError === "object" && wsOrError.ok === false) {
11369
+ if (wsOrError === null) {
11370
+ json(res, 200, buildProtectedResourceMetadata(this.env));
11371
+ return;
11372
+ }
11373
+ if (typeof wsOrError !== "string") {
11354
11374
  json(res, 404, {
11355
11375
  error: "invalid_request",
11356
11376
  error_description: "resource workspace is invalid"
11357
11377
  });
11358
11378
  return;
11359
11379
  }
11360
- if (wsOrError === null) {
11361
- json(res, 200, buildProtectedResourceMetadata(this.env));
11362
- return;
11363
- }
11364
11380
  const parsed2 = await parseGatewayWorkspaceRoot(wsOrError);
11365
11381
  if (!parsed2.ok) {
11366
11382
  json(res, 404, {
@@ -11738,12 +11754,330 @@ var path50 = __toESM(require("path"), 1);
11738
11754
  var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
11739
11755
  var import_types2 = require("@modelcontextprotocol/sdk/types.js");
11740
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
+
11741
12075
  // src/memoraMcpServer.ts
11742
12076
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
11743
12077
  var import_types = require("@modelcontextprotocol/sdk/types.js");
11744
12078
 
11745
12079
  // src/pluginStartupBindingWait.ts
11746
- var path48 = __toESM(require("path"), 1);
12080
+ var path49 = __toESM(require("path"), 1);
11747
12081
  var MEMORAONE_PLUGIN_ENV = "MEMORAONE_PLUGIN";
11748
12082
  var MEMORAONE_CURSOR_PLUGIN = "cursor";
11749
12083
  var PLUGIN_STARTUP_BINDING_WAIT_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -11804,7 +12138,7 @@ function collectPluginStartupBindingRoots(options) {
11804
12138
  if (raw === void 0 || raw === null || raw.trim() === "") {
11805
12139
  return;
11806
12140
  }
11807
- const resolved = path48.resolve(raw);
12141
+ const resolved = path49.resolve(raw);
11808
12142
  if (seen.has(resolved)) {
11809
12143
  return;
11810
12144
  }
@@ -12097,11 +12431,11 @@ var LOCAL_MCP_TOOL_NAMES = [
12097
12431
 
12098
12432
  // src/tools/handlers/postEvent.ts
12099
12433
  var import_v415 = require("zod/v4");
12100
- var crypto5 = __toESM(require("crypto"), 1);
12434
+ var crypto6 = __toESM(require("crypto"), 1);
12101
12435
 
12102
12436
  // src/runContext.ts
12103
12437
  var import_node_async_hooks = require("async_hooks");
12104
- var crypto4 = __toESM(require("crypto"), 1);
12438
+ var crypto5 = __toESM(require("crypto"), 1);
12105
12439
  var sessionContextStorage = new import_node_async_hooks.AsyncLocalStorage();
12106
12440
  function createSessionRunContext(initial = {}) {
12107
12441
  return {
@@ -12148,7 +12482,7 @@ function resolveRunId(passed) {
12148
12482
  return getCurrentRunId();
12149
12483
  }
12150
12484
  function generateRunId() {
12151
- return crypto4.randomBytes(16).toString("hex");
12485
+ return crypto5.randomBytes(16).toString("hex");
12152
12486
  }
12153
12487
 
12154
12488
  // src/tools/handlers/postEvent.ts
@@ -12188,7 +12522,7 @@ function buildPostEventContentFields(content) {
12188
12522
  return { message: JSON.stringify(content) };
12189
12523
  }
12190
12524
  async function handlePostEvent(client, args) {
12191
- const nonce = crypto5.randomBytes(8).toString("hex");
12525
+ const nonce = crypto6.randomBytes(8).toString("hex");
12192
12526
  console.error(
12193
12527
  `[memoraone-mcp][debug] tool=memora_post_event toolCallId=unknown nonce=${nonce} stage=before_post`
12194
12528
  );
@@ -12898,19 +13232,6 @@ async function handleFinishSetup(options) {
12898
13232
  };
12899
13233
  }
12900
13234
 
12901
- // src/heartbeat.ts
12902
- var crypto6 = __toESM(require("crypto"), 1);
12903
- var fs37 = __toESM(require("fs"), 1);
12904
- var path49 = __toESM(require("path"), 1);
12905
-
12906
- // src/localState/mcpSessionId.ts
12907
- var import_node_crypto29 = require("crypto");
12908
-
12909
- // src/heartbeat.ts
12910
- function fingerprintAccessToken(accessToken) {
12911
- return crypto6.createHash("sha256").update(accessToken).digest("hex").slice(0, 12);
12912
- }
12913
-
12914
13235
  // src/ideType.ts
12915
13236
  var RELIABLE_CLIENT_INFO_NAMES = /* @__PURE__ */ new Map([
12916
13237
  // Existing
@@ -13652,6 +13973,66 @@ var log2 = (msg) => {
13652
13973
  `);
13653
13974
  };
13654
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
+ }
13655
14036
  function sendJsonRpcHttpError(res, status, message) {
13656
14037
  if (res.headersSent) return;
13657
14038
  const body = JSON.stringify({
@@ -13743,6 +14124,7 @@ async function removeSession(sessionId) {
13743
14124
  const session = sessionsById.get(sessionId);
13744
14125
  if (!session) return;
13745
14126
  sessionsById.delete(sessionId);
14127
+ releaseSession(session);
13746
14128
  try {
13747
14129
  await session.transport.close();
13748
14130
  } catch {
@@ -13778,7 +14160,11 @@ async function createSession(binding, canonicalUrl, options) {
13778
14160
  sessionsById.set(sessionId, sessionRef);
13779
14161
  },
13780
14162
  onsessionclosed: async (sessionId) => {
13781
- sessionsById.delete(sessionId);
14163
+ const existing = sessionsById.get(sessionId);
14164
+ if (existing) {
14165
+ sessionsById.delete(sessionId);
14166
+ releaseSession(existing);
14167
+ }
13782
14168
  }
13783
14169
  });
13784
14170
  const handle = createMemoraMcpServer({
@@ -13788,15 +14174,24 @@ async function createSession(binding, canonicalUrl, options) {
13788
14174
  bindingResolveOptions: {
13789
14175
  homeDir: options.homeDir,
13790
14176
  identityDeps: options.identityDeps
14177
+ },
14178
+ onSessionIdeTypeKnown: (ideType) => {
14179
+ sessionRef?.heartbeat?.setIdeType(ideType);
13791
14180
  }
13792
14181
  });
13793
- void handle.bindingReady.catch(() => {
13794
- });
13795
14182
  await handle.server.connect(transport);
13796
14183
  transport.onclose = () => {
13797
14184
  const sid = transport.sessionId;
13798
14185
  if (sid) {
13799
- 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);
13800
14195
  }
13801
14196
  };
13802
14197
  sessionRef = {
@@ -13805,12 +14200,26 @@ async function createSession(binding, canonicalUrl, options) {
13805
14200
  canonicalUrl,
13806
14201
  binding,
13807
14202
  transport,
14203
+ heartbeat: null,
14204
+ heartbeatAttach: null,
14205
+ closed: false,
13808
14206
  close: async () => {
13809
14207
  const sid = transport.sessionId;
13810
- if (sid) sessionsById.delete(sid);
13811
- 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
+ }
13812
14217
  }
13813
14218
  };
14219
+ void attachGatewayMcpSessionHeartbeat(sessionRef, handle, options).catch(
14220
+ () => {
14221
+ }
14222
+ );
13814
14223
  return sessionRef;
13815
14224
  }
13816
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.56",
3
+ "version": "0.1.58",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "exports": {