@memoraone/mcp 0.1.57 → 0.1.59
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 +460 -49
- 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 = {}) {
|
|
@@ -13914,6 +14322,9 @@ function sendPairingGate(res, body) {
|
|
|
13914
14322
|
});
|
|
13915
14323
|
res.end(body);
|
|
13916
14324
|
}
|
|
14325
|
+
function canWriteGatewayResponse(res) {
|
|
14326
|
+
return !res.writableEnded && !res.destroyed;
|
|
14327
|
+
}
|
|
13917
14328
|
async function handleGatewayMcpAuthRequest(req, res, url, options = {}) {
|
|
13918
14329
|
if (url.pathname !== GATEWAY_MCP_PATH) return false;
|
|
13919
14330
|
const env2 = options.env ?? process.env;
|
|
@@ -13985,8 +14396,7 @@ async function handleGatewayMcpAuthRequest(req, res, url, options = {}) {
|
|
|
13985
14396
|
return true;
|
|
13986
14397
|
}
|
|
13987
14398
|
const pairing = await coordinator.ensure(parsed2.workspaceRoot);
|
|
13988
|
-
if (
|
|
13989
|
-
sendPairingGate(res, "Pairing pending");
|
|
14399
|
+
if (!canWriteGatewayResponse(res)) {
|
|
13990
14400
|
return true;
|
|
13991
14401
|
}
|
|
13992
14402
|
if (pairing.status === "failed") {
|
|
@@ -14026,6 +14436,9 @@ async function handleGatewayMcpAuthRequest(req, res, url, options = {}) {
|
|
|
14026
14436
|
);
|
|
14027
14437
|
return true;
|
|
14028
14438
|
} catch {
|
|
14439
|
+
if (!canWriteGatewayResponse(res)) {
|
|
14440
|
+
return true;
|
|
14441
|
+
}
|
|
14029
14442
|
sendUnauthorized(res, env2, parsed2.workspaceRoot);
|
|
14030
14443
|
return true;
|
|
14031
14444
|
}
|
|
@@ -14078,13 +14491,19 @@ function createGatewayUnboundPairingCoordinator(options = {}) {
|
|
|
14078
14491
|
const openUrl = options.openUrl ?? ((url) => openUrlInBrowser(url));
|
|
14079
14492
|
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
|
|
14080
14493
|
const apiUrl = (options.apiUrl ?? config2.apiUrl).replace(/\/+$/, "");
|
|
14494
|
+
function settleLatch(key, entry, status) {
|
|
14495
|
+
if (entry.status !== "pending") return;
|
|
14496
|
+
entry.status = status;
|
|
14497
|
+
entry.settle(status);
|
|
14498
|
+
latches.delete(key);
|
|
14499
|
+
}
|
|
14081
14500
|
async function pollInBackground(key, session, entry) {
|
|
14082
14501
|
for (; ; ) {
|
|
14083
14502
|
let result;
|
|
14084
14503
|
try {
|
|
14085
14504
|
result = await session.poll();
|
|
14086
14505
|
} catch {
|
|
14087
|
-
entry
|
|
14506
|
+
settleLatch(key, entry, "failed");
|
|
14088
14507
|
return;
|
|
14089
14508
|
}
|
|
14090
14509
|
if (result.status === "pending") {
|
|
@@ -14092,15 +14511,14 @@ function createGatewayUnboundPairingCoordinator(options = {}) {
|
|
|
14092
14511
|
continue;
|
|
14093
14512
|
}
|
|
14094
14513
|
if (result.status === "ready") {
|
|
14095
|
-
entry
|
|
14096
|
-
latches.delete(key);
|
|
14514
|
+
settleLatch(key, entry, "ready");
|
|
14097
14515
|
return;
|
|
14098
14516
|
}
|
|
14099
14517
|
if (result.status === "backend_failure" && result.operation === "poll") {
|
|
14100
14518
|
await delay3(pollIntervalMs);
|
|
14101
14519
|
continue;
|
|
14102
14520
|
}
|
|
14103
|
-
entry
|
|
14521
|
+
settleLatch(key, entry, "failed");
|
|
14104
14522
|
return;
|
|
14105
14523
|
}
|
|
14106
14524
|
}
|
|
@@ -14122,18 +14540,16 @@ function createGatewayUnboundPairingCoordinator(options = {}) {
|
|
|
14122
14540
|
try {
|
|
14123
14541
|
await openUrl(authorizationUrl);
|
|
14124
14542
|
} catch {
|
|
14125
|
-
entry
|
|
14543
|
+
settleLatch(key, entry, "failed");
|
|
14126
14544
|
return;
|
|
14127
14545
|
}
|
|
14128
14546
|
} else {
|
|
14129
|
-
entry
|
|
14547
|
+
settleLatch(key, entry, "failed");
|
|
14130
14548
|
return;
|
|
14131
14549
|
}
|
|
14132
|
-
|
|
14133
|
-
void pollInBackground(key, session, entry);
|
|
14134
|
-
});
|
|
14550
|
+
void pollInBackground(key, session, entry);
|
|
14135
14551
|
} catch {
|
|
14136
|
-
entry
|
|
14552
|
+
settleLatch(key, entry, "failed");
|
|
14137
14553
|
}
|
|
14138
14554
|
}
|
|
14139
14555
|
return {
|
|
@@ -14141,21 +14557,16 @@ function createGatewayUnboundPairingCoordinator(options = {}) {
|
|
|
14141
14557
|
const key = path51.resolve(workspaceRoot);
|
|
14142
14558
|
const existing = latches.get(key);
|
|
14143
14559
|
if (existing) {
|
|
14144
|
-
await existing.
|
|
14145
|
-
return { status: existing.status };
|
|
14560
|
+
return { status: await existing.settled };
|
|
14146
14561
|
}
|
|
14147
|
-
let
|
|
14148
|
-
const
|
|
14149
|
-
|
|
14562
|
+
let settle;
|
|
14563
|
+
const settled = new Promise((resolve47) => {
|
|
14564
|
+
settle = resolve47;
|
|
14150
14565
|
});
|
|
14151
|
-
const entry = { status: "pending",
|
|
14566
|
+
const entry = { status: "pending", settled, settle };
|
|
14152
14567
|
latches.set(key, entry);
|
|
14153
|
-
|
|
14154
|
-
|
|
14155
|
-
} finally {
|
|
14156
|
-
resolveStart();
|
|
14157
|
-
}
|
|
14158
|
-
return { status: entry.status };
|
|
14568
|
+
void startPairing(key, entry);
|
|
14569
|
+
return { status: await entry.settled };
|
|
14159
14570
|
}
|
|
14160
14571
|
};
|
|
14161
14572
|
}
|