@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 +1 -1
- package/dist/gateway.cjs +433 -25
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
|
13812
|
-
|
|
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 = {}) {
|