584 lines
18 KiB
JavaScript
584 lines
18 KiB
JavaScript
|
|
"use strict";
|
||
|
|
|
||
|
|
const crypto = require("node:crypto");
|
||
|
|
|
||
|
|
const {
|
||
|
|
getExternalMcpDiscoveryFilePath,
|
||
|
|
getExternalMcpLauncherPath,
|
||
|
|
EXTERNAL_MCP_CHAT_SESSION_ID,
|
||
|
|
buildDiscoveryEnv,
|
||
|
|
} = require("../cli/externalMcpDiscoveryPath.cjs");
|
||
|
|
const {
|
||
|
|
writeExternalDiscovery,
|
||
|
|
removeExternalDiscovery,
|
||
|
|
} = require("../cli/externalMcpDiscovery.cjs");
|
||
|
|
const { createExternalMcpCodexSetup } = require("./externalMcp/codexSetup.cjs");
|
||
|
|
const { createExternalMcpClaudeSetup } = require("./externalMcp/claudeSetup.cjs");
|
||
|
|
const { createExternalMcpGrokSetup } = require("./externalMcp/grokSetup.cjs");
|
||
|
|
const {
|
||
|
|
DEFAULT_SESSION_IDLE_TIMEOUT_MINUTES,
|
||
|
|
normalizeSessionIdleTimeoutMinutes,
|
||
|
|
} = require("./mcpServerBridge/sessionIdleManager.cjs");
|
||
|
|
|
||
|
|
const EXTERNAL_MCP_MODE_TEMPORARY = "temporary";
|
||
|
|
const EXTERNAL_MCP_MODE_PERSISTENT = "persistent";
|
||
|
|
const DEFAULT_IDLE_TIMEOUT_MINUTES = 10;
|
||
|
|
const MIN_IDLE_TIMEOUT_MINUTES = 1;
|
||
|
|
const MAX_IDLE_TIMEOUT_MINUTES = 24 * 60;
|
||
|
|
|
||
|
|
function normalizeMode(value) {
|
||
|
|
return value === EXTERNAL_MCP_MODE_PERSISTENT
|
||
|
|
? EXTERNAL_MCP_MODE_PERSISTENT
|
||
|
|
: EXTERNAL_MCP_MODE_TEMPORARY;
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeIdleTimeoutMinutes(value) {
|
||
|
|
const parsed = Number(value);
|
||
|
|
if (!Number.isFinite(parsed)) return DEFAULT_IDLE_TIMEOUT_MINUTES;
|
||
|
|
return Math.min(
|
||
|
|
MAX_IDLE_TIMEOUT_MINUTES,
|
||
|
|
Math.max(MIN_IDLE_TIMEOUT_MINUTES, Math.round(parsed)),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function createExternalMcpController(options = {}) {
|
||
|
|
const deps = {
|
||
|
|
mcpServerBridge: options.mcpServerBridge || null,
|
||
|
|
getDiscoveryFilePath: options.getDiscoveryFilePath || getExternalMcpDiscoveryFilePath,
|
||
|
|
getLauncherPath: options.getLauncherPath || getExternalMcpLauncherPath,
|
||
|
|
writeDiscovery: options.writeDiscovery || writeExternalDiscovery,
|
||
|
|
removeDiscovery: options.removeDiscovery || removeExternalDiscovery,
|
||
|
|
createCodexSetup: options.createCodexSetup || createExternalMcpCodexSetup,
|
||
|
|
createClaudeSetup: options.createClaudeSetup || createExternalMcpClaudeSetup,
|
||
|
|
createGrokSetup: options.createGrokSetup || createExternalMcpGrokSetup,
|
||
|
|
randomBytes: options.randomBytes || ((size) => crypto.randomBytes(size)),
|
||
|
|
Date: options.Date || Date,
|
||
|
|
setTimeout: options.setTimeout || setTimeout,
|
||
|
|
clearTimeout: options.clearTimeout || clearTimeout,
|
||
|
|
chatSessionId: options.chatSessionId || EXTERNAL_MCP_CHAT_SESSION_ID,
|
||
|
|
};
|
||
|
|
|
||
|
|
let discoveryFilePath = null;
|
||
|
|
let enabled = false;
|
||
|
|
let state = "disabled";
|
||
|
|
let error = null;
|
||
|
|
let mode = EXTERNAL_MCP_MODE_TEMPORARY;
|
||
|
|
let idleTimeoutMinutes = DEFAULT_IDLE_TIMEOUT_MINUTES;
|
||
|
|
let sessionIdleTimeoutMinutes = DEFAULT_SESSION_IDLE_TIMEOUT_MINUTES;
|
||
|
|
let lastActivityAt = null;
|
||
|
|
let idleExpiresAt = null;
|
||
|
|
let idleTimer = null;
|
||
|
|
let startPromise = null;
|
||
|
|
let stopPromise = null;
|
||
|
|
let setEnabledChain = Promise.resolve();
|
||
|
|
let startGeneration = 0;
|
||
|
|
let lastKnownPort = null;
|
||
|
|
let lastKnownToken = null;
|
||
|
|
let sessionSyncHandler = null;
|
||
|
|
let codexSetup = null;
|
||
|
|
let claudeSetup = null;
|
||
|
|
let grokSetup = null;
|
||
|
|
|
||
|
|
function getNow() {
|
||
|
|
return deps.Date.now();
|
||
|
|
}
|
||
|
|
|
||
|
|
function clearIdleTimer() {
|
||
|
|
if (!idleTimer) return;
|
||
|
|
deps.clearTimeout(idleTimer);
|
||
|
|
idleTimer = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function scheduleIdleShutdown() {
|
||
|
|
clearIdleTimer();
|
||
|
|
if (!enabled || state !== "running" || mode !== EXTERNAL_MCP_MODE_TEMPORARY) {
|
||
|
|
idleExpiresAt = null;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const now = getNow();
|
||
|
|
if (!lastActivityAt) lastActivityAt = now;
|
||
|
|
idleExpiresAt = lastActivityAt + idleTimeoutMinutes * 60 * 1000;
|
||
|
|
const delayMs = Math.max(0, idleExpiresAt - now);
|
||
|
|
idleTimer = deps.setTimeout(() => {
|
||
|
|
idleTimer = null;
|
||
|
|
void setEnabled(false);
|
||
|
|
}, delayMs);
|
||
|
|
}
|
||
|
|
|
||
|
|
function recordActivity() {
|
||
|
|
if (!enabled) return;
|
||
|
|
lastActivityAt = getNow();
|
||
|
|
scheduleIdleShutdown();
|
||
|
|
}
|
||
|
|
|
||
|
|
function ensureClientSetup() {
|
||
|
|
const launcherPath = deps.getLauncherPath() || null;
|
||
|
|
const discoveryEnv = buildDiscoveryEnv(discoveryFilePath);
|
||
|
|
if (!codexSetup) {
|
||
|
|
codexSetup = deps.createCodexSetup({ launcherPath, discoveryEnv });
|
||
|
|
}
|
||
|
|
if (!claudeSetup) {
|
||
|
|
claudeSetup = deps.createClaudeSetup({ launcherPath, discoveryEnv });
|
||
|
|
}
|
||
|
|
if (!grokSetup) {
|
||
|
|
grokSetup = deps.createGrokSetup({ launcherPath, discoveryEnv });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function getBridge() {
|
||
|
|
return deps.mcpServerBridge;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getExposedSessionCount() {
|
||
|
|
const bridge = getBridge();
|
||
|
|
if (!bridge?.getScopedSessionIds) return 0;
|
||
|
|
return bridge.getScopedSessionIds(deps.chatSessionId).length;
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildStatus() {
|
||
|
|
const bridge = getBridge();
|
||
|
|
const hostRunning = Boolean(bridge && typeof bridge.getOrCreateHost === "function" && lastKnownPort);
|
||
|
|
return {
|
||
|
|
ok: true,
|
||
|
|
enabled,
|
||
|
|
state,
|
||
|
|
host: "127.0.0.1",
|
||
|
|
port: lastKnownPort,
|
||
|
|
discoveryPath: discoveryFilePath || null,
|
||
|
|
launcherPath: deps.getLauncherPath() || null,
|
||
|
|
chatSessionId: deps.chatSessionId,
|
||
|
|
exposedSessionCount: getExposedSessionCount(),
|
||
|
|
mode,
|
||
|
|
idleTimeoutMinutes,
|
||
|
|
sessionIdleTimeoutMinutes,
|
||
|
|
lastActivityAt,
|
||
|
|
idleExpiresAt,
|
||
|
|
permissionMode: bridge?.getPermissionMode?.() || "confirm",
|
||
|
|
hostRunning,
|
||
|
|
error,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function writeDiscoveryFromBridge() {
|
||
|
|
const bridge = getBridge();
|
||
|
|
if (!discoveryFilePath || !bridge) return null;
|
||
|
|
// Prefer live host credentials from bridge module state via buildMcpServerConfig side channel.
|
||
|
|
// getOrCreateHost already started; read port/token from discovery write inputs we track.
|
||
|
|
if (lastKnownPort == null || !lastKnownToken) return null;
|
||
|
|
return deps.writeDiscovery(discoveryFilePath, {
|
||
|
|
host: "127.0.0.1",
|
||
|
|
port: lastKnownPort,
|
||
|
|
token: lastKnownToken,
|
||
|
|
pid: process.pid,
|
||
|
|
permissionMode: bridge.getPermissionMode?.() || "confirm",
|
||
|
|
chatSessionId: deps.chatSessionId,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async function resolveHostCredentials() {
|
||
|
|
const bridge = getBridge();
|
||
|
|
if (!bridge?.getOrCreateHost) {
|
||
|
|
throw new Error("MCP bridge is unavailable.");
|
||
|
|
}
|
||
|
|
const port = await bridge.getOrCreateHost();
|
||
|
|
// External MCP uses a dedicated token (rotated on enable) so disable can
|
||
|
|
// invalidate discovery without rotating the shared Catty/CLI TCP token.
|
||
|
|
const token = typeof bridge.issueExternalMcpAuthToken === "function"
|
||
|
|
? bridge.issueExternalMcpAuthToken()
|
||
|
|
: (typeof bridge.getExternalMcpAuthToken === "function" ? bridge.getExternalMcpAuthToken() : "");
|
||
|
|
if (!port || !token) {
|
||
|
|
throw new Error("Failed to resolve MCP bridge port/token for external mode.");
|
||
|
|
}
|
||
|
|
lastKnownPort = port;
|
||
|
|
lastKnownToken = token;
|
||
|
|
return { port, token };
|
||
|
|
}
|
||
|
|
|
||
|
|
async function startRuntime() {
|
||
|
|
if (!discoveryFilePath) {
|
||
|
|
throw new Error("External MCP discovery path is not configured.");
|
||
|
|
}
|
||
|
|
await resolveHostCredentials();
|
||
|
|
if (!enabled) {
|
||
|
|
throw new Error("External MCP was disabled during startup.");
|
||
|
|
}
|
||
|
|
writeDiscoveryFromBridge();
|
||
|
|
lastActivityAt = getNow();
|
||
|
|
if (typeof sessionSyncHandler === "function") {
|
||
|
|
await sessionSyncHandler();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function stopActiveRuntime() {
|
||
|
|
clearIdleTimer();
|
||
|
|
idleExpiresAt = null;
|
||
|
|
lastKnownPort = null;
|
||
|
|
lastKnownToken = null;
|
||
|
|
if (discoveryFilePath) {
|
||
|
|
deps.removeDiscovery(discoveryFilePath);
|
||
|
|
}
|
||
|
|
const bridge = getBridge();
|
||
|
|
// Soft revoke: rotate/clear the external token and reject reserved-scope
|
||
|
|
// RPCs while disabled. Keep TCP sockets alive so long-lived stdio clients
|
||
|
|
// can resume after re-enable without restarting Codex/Claude/Grok.
|
||
|
|
if (typeof bridge?.revokeExternalMcpAuthToken === "function") {
|
||
|
|
try {
|
||
|
|
bridge.revokeExternalMcpAuthToken();
|
||
|
|
} catch {
|
||
|
|
// Best-effort token revoke.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (typeof bridge?.cancelPtyExecsForSession === "function") {
|
||
|
|
try {
|
||
|
|
bridge.cancelPtyExecsForSession(deps.chatSessionId);
|
||
|
|
} catch {
|
||
|
|
// Best-effort exec cancel.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (typeof bridge?.cancelBackgroundJobsForSession === "function") {
|
||
|
|
try {
|
||
|
|
bridge.cancelBackgroundJobsForSession(deps.chatSessionId);
|
||
|
|
} catch {
|
||
|
|
// Best-effort background job cancel.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (typeof bridge?.cancelWorkerBackgroundJobsForSession === "function") {
|
||
|
|
try {
|
||
|
|
bridge.cancelWorkerBackgroundJobsForSession(deps.chatSessionId);
|
||
|
|
} catch {
|
||
|
|
// Best-effort worker job cancel.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (typeof bridge?.cancelSftpOpsForSession === "function") {
|
||
|
|
try {
|
||
|
|
await bridge.cancelSftpOpsForSession(deps.chatSessionId);
|
||
|
|
} catch {
|
||
|
|
// Best-effort SFTP cancel.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (typeof bridge?.setChatSessionCancelled === "function") {
|
||
|
|
try {
|
||
|
|
bridge.setChatSessionCancelled(deps.chatSessionId, true);
|
||
|
|
} catch {
|
||
|
|
// Best-effort cancel flag.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (typeof bridge?.clearPendingApprovals === "function") {
|
||
|
|
try {
|
||
|
|
bridge.clearPendingApprovals(deps.chatSessionId);
|
||
|
|
} catch {
|
||
|
|
// Best-effort approval revoke.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (bridge?.cleanupScopedMetadata) {
|
||
|
|
await bridge.cleanupScopedMetadata(deps.chatSessionId);
|
||
|
|
}
|
||
|
|
// cleanupScopedMetadata clears cancelledChatSessions; re-assert cancel so
|
||
|
|
// soft-revoke still blocks in-flight external work after metadata wipe.
|
||
|
|
if (typeof bridge?.setChatSessionCancelled === "function") {
|
||
|
|
try {
|
||
|
|
bridge.setChatSessionCancelled(deps.chatSessionId, true);
|
||
|
|
} catch {
|
||
|
|
// Best-effort cancel flag.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function setEnabledUnlocked(nextEnabled) {
|
||
|
|
const wantEnabled = Boolean(nextEnabled);
|
||
|
|
enabled = wantEnabled;
|
||
|
|
|
||
|
|
if (!wantEnabled) {
|
||
|
|
startGeneration += 1;
|
||
|
|
if (startPromise) {
|
||
|
|
state = "disabled";
|
||
|
|
error = null;
|
||
|
|
await startPromise.catch(() => {});
|
||
|
|
}
|
||
|
|
if (stopPromise) {
|
||
|
|
await stopPromise;
|
||
|
|
}
|
||
|
|
state = "disabled";
|
||
|
|
error = null;
|
||
|
|
stopPromise = stopActiveRuntime().finally(() => {
|
||
|
|
stopPromise = null;
|
||
|
|
});
|
||
|
|
await stopPromise;
|
||
|
|
return buildStatus();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (state === "running" && lastKnownPort != null && lastKnownToken) {
|
||
|
|
writeDiscoveryFromBridge();
|
||
|
|
scheduleIdleShutdown();
|
||
|
|
return buildStatus();
|
||
|
|
}
|
||
|
|
if (stopPromise) {
|
||
|
|
await stopPromise;
|
||
|
|
}
|
||
|
|
if (!enabled) {
|
||
|
|
return buildStatus();
|
||
|
|
}
|
||
|
|
if (startPromise) {
|
||
|
|
await startPromise.catch(() => {});
|
||
|
|
if (!enabled) {
|
||
|
|
return buildStatus();
|
||
|
|
}
|
||
|
|
if (state === "running" && lastKnownPort != null && lastKnownToken) {
|
||
|
|
writeDiscoveryFromBridge();
|
||
|
|
scheduleIdleShutdown();
|
||
|
|
return buildStatus();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const generation = ++startGeneration;
|
||
|
|
state = "starting";
|
||
|
|
error = null;
|
||
|
|
const bridgeForEnable = getBridge();
|
||
|
|
if (typeof bridgeForEnable?.setChatSessionCancelled === "function") {
|
||
|
|
try {
|
||
|
|
bridgeForEnable.setChatSessionCancelled(deps.chatSessionId, false);
|
||
|
|
} catch {
|
||
|
|
// Best-effort clear of a previous disable cancel flag.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
startPromise = startRuntime()
|
||
|
|
.then(() => {
|
||
|
|
if (!enabled || generation !== startGeneration) {
|
||
|
|
state = "disabled";
|
||
|
|
error = null;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
state = "running";
|
||
|
|
scheduleIdleShutdown();
|
||
|
|
})
|
||
|
|
.catch(async (startError) => {
|
||
|
|
if (generation !== startGeneration) return;
|
||
|
|
error = startError?.message || String(startError);
|
||
|
|
enabled = false;
|
||
|
|
state = "disabled";
|
||
|
|
try {
|
||
|
|
await stopActiveRuntime();
|
||
|
|
} catch {
|
||
|
|
// Ignore cleanup failures while surfacing the startup error.
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.finally(() => {
|
||
|
|
if (generation === startGeneration) {
|
||
|
|
startPromise = null;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
await startPromise;
|
||
|
|
return buildStatus();
|
||
|
|
}
|
||
|
|
|
||
|
|
function setEnabled(nextEnabled) {
|
||
|
|
const run = () => setEnabledUnlocked(nextEnabled);
|
||
|
|
const result = setEnabledChain.then(run, run);
|
||
|
|
setEnabledChain = result.then(() => {}, () => {});
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
function setConfig(config = {}) {
|
||
|
|
const nextMode = config.mode == null ? mode : normalizeMode(config.mode);
|
||
|
|
const nextIdleTimeoutMinutes = config.idleTimeoutMinutes == null
|
||
|
|
? idleTimeoutMinutes
|
||
|
|
: normalizeIdleTimeoutMinutes(config.idleTimeoutMinutes);
|
||
|
|
const nextSessionIdleTimeoutMinutes = config.sessionIdleTimeoutMinutes == null
|
||
|
|
? sessionIdleTimeoutMinutes
|
||
|
|
: normalizeSessionIdleTimeoutMinutes(config.sessionIdleTimeoutMinutes);
|
||
|
|
const modeChanged = nextMode !== mode;
|
||
|
|
const timeoutChanged = nextIdleTimeoutMinutes !== idleTimeoutMinutes;
|
||
|
|
|
||
|
|
mode = nextMode;
|
||
|
|
idleTimeoutMinutes = nextIdleTimeoutMinutes;
|
||
|
|
sessionIdleTimeoutMinutes = nextSessionIdleTimeoutMinutes;
|
||
|
|
getBridge()?.setSessionIdleTimeoutMinutes?.(sessionIdleTimeoutMinutes);
|
||
|
|
|
||
|
|
if ((modeChanged || timeoutChanged) && enabled && state === "running") {
|
||
|
|
lastActivityAt = getNow();
|
||
|
|
scheduleIdleShutdown();
|
||
|
|
}
|
||
|
|
|
||
|
|
return buildStatus();
|
||
|
|
}
|
||
|
|
|
||
|
|
function getStatus() {
|
||
|
|
return buildStatus();
|
||
|
|
}
|
||
|
|
|
||
|
|
function isEnabled() {
|
||
|
|
// Accept clients while starting/running as soon as the switch is on.
|
||
|
|
// Discovery may be written before state flips to "running". Do not treat
|
||
|
|
// error/disabled as enabled after cleanup revoked discovery/sockets.
|
||
|
|
return enabled && (state === "starting" || state === "running");
|
||
|
|
}
|
||
|
|
|
||
|
|
function getChatSessionId() {
|
||
|
|
return deps.chatSessionId;
|
||
|
|
}
|
||
|
|
|
||
|
|
function setSessionSyncHandler(handler) {
|
||
|
|
sessionSyncHandler = typeof handler === "function" ? handler : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function onBridgeHostReady({ port }) {
|
||
|
|
if (!isEnabled()) return;
|
||
|
|
// Host-ready reports the shared Catty/CLI TCP token — never write that into
|
||
|
|
// External discovery. Only refresh with an already-issued external token;
|
||
|
|
// do not issue here or cold-start enable double-rotates before
|
||
|
|
// resolveHostCredentials runs.
|
||
|
|
if (port) lastKnownPort = port;
|
||
|
|
const bridge = getBridge();
|
||
|
|
const externalToken = typeof bridge?.getExternalMcpAuthToken === "function"
|
||
|
|
? bridge.getExternalMcpAuthToken()
|
||
|
|
: null;
|
||
|
|
if (!externalToken) return;
|
||
|
|
lastKnownToken = externalToken;
|
||
|
|
if (discoveryFilePath && lastKnownPort && lastKnownToken) {
|
||
|
|
writeDiscoveryFromBridge();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function onPermissionModeChanged() {
|
||
|
|
if (isEnabled() && state === "running") {
|
||
|
|
writeDiscoveryFromBridge();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function init(initOptions = {}) {
|
||
|
|
if (initOptions.mcpServerBridge) {
|
||
|
|
deps.mcpServerBridge = initOptions.mcpServerBridge;
|
||
|
|
}
|
||
|
|
discoveryFilePath = initOptions.discoveryFilePath
|
||
|
|
|| deps.getDiscoveryFilePath(
|
||
|
|
initOptions.userDataDir ? { userDataDir: initOptions.userDataDir } : {},
|
||
|
|
);
|
||
|
|
// Rotate any stale discovery from a previous process.
|
||
|
|
if (discoveryFilePath) {
|
||
|
|
deps.removeDiscovery(discoveryFilePath);
|
||
|
|
}
|
||
|
|
// Recreate client setup helpers so they pick up the resolved discovery path.
|
||
|
|
codexSetup = null;
|
||
|
|
claudeSetup = null;
|
||
|
|
grokSetup = null;
|
||
|
|
ensureClientSetup();
|
||
|
|
}
|
||
|
|
|
||
|
|
function cleanup() {
|
||
|
|
clearIdleTimer();
|
||
|
|
if (discoveryFilePath) {
|
||
|
|
deps.removeDiscovery(discoveryFilePath);
|
||
|
|
}
|
||
|
|
enabled = false;
|
||
|
|
state = "disabled";
|
||
|
|
lastKnownPort = null;
|
||
|
|
lastKnownToken = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function getCodexStatus() {
|
||
|
|
ensureClientSetup();
|
||
|
|
return await codexSetup.getStatus();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function addToCodex() {
|
||
|
|
ensureClientSetup();
|
||
|
|
return await codexSetup.addToCodex();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function getClaudeStatus() {
|
||
|
|
ensureClientSetup();
|
||
|
|
return await claudeSetup.getStatus();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function addToClaude() {
|
||
|
|
ensureClientSetup();
|
||
|
|
return await claudeSetup.addToClaude();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function getGrokStatus() {
|
||
|
|
ensureClientSetup();
|
||
|
|
return await grokSetup.getStatus();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function addToGrok() {
|
||
|
|
ensureClientSetup();
|
||
|
|
return await grokSetup.addToGrok();
|
||
|
|
}
|
||
|
|
|
||
|
|
function registerHandlers(ipcMain, validateSender) {
|
||
|
|
const guard = typeof validateSender === "function"
|
||
|
|
? validateSender
|
||
|
|
: () => true;
|
||
|
|
|
||
|
|
ipcMain.handle("netcatty:external-mcp:get-status", async (event) => {
|
||
|
|
if (!guard(event)) return { ok: false, error: "Unauthorized IPC sender" };
|
||
|
|
return getStatus();
|
||
|
|
});
|
||
|
|
ipcMain.handle("netcatty:external-mcp:set-enabled", async (event, payload) => {
|
||
|
|
if (!guard(event)) return { ok: false, error: "Unauthorized IPC sender" };
|
||
|
|
return await setEnabled(Boolean(payload?.enabled));
|
||
|
|
});
|
||
|
|
ipcMain.handle("netcatty:external-mcp:set-config", async (event, payload) => {
|
||
|
|
if (!guard(event)) return { ok: false, error: "Unauthorized IPC sender" };
|
||
|
|
return setConfig(payload || {});
|
||
|
|
});
|
||
|
|
ipcMain.handle("netcatty:external-mcp:codex:get-status", async (event) => {
|
||
|
|
if (!guard(event)) return { ok: false, error: "Unauthorized IPC sender" };
|
||
|
|
return await getCodexStatus();
|
||
|
|
});
|
||
|
|
ipcMain.handle("netcatty:external-mcp:codex:add", async (event) => {
|
||
|
|
if (!guard(event)) return { ok: false, error: "Unauthorized IPC sender" };
|
||
|
|
return await addToCodex();
|
||
|
|
});
|
||
|
|
ipcMain.handle("netcatty:external-mcp:claude:get-status", async (event) => {
|
||
|
|
if (!guard(event)) return { ok: false, error: "Unauthorized IPC sender" };
|
||
|
|
return await getClaudeStatus();
|
||
|
|
});
|
||
|
|
ipcMain.handle("netcatty:external-mcp:claude:add", async (event) => {
|
||
|
|
if (!guard(event)) return { ok: false, error: "Unauthorized IPC sender" };
|
||
|
|
return await addToClaude();
|
||
|
|
});
|
||
|
|
ipcMain.handle("netcatty:external-mcp:grok:get-status", async (event) => {
|
||
|
|
if (!guard(event)) return { ok: false, error: "Unauthorized IPC sender" };
|
||
|
|
return await getGrokStatus();
|
||
|
|
});
|
||
|
|
ipcMain.handle("netcatty:external-mcp:grok:add", async (event) => {
|
||
|
|
if (!guard(event)) return { ok: false, error: "Unauthorized IPC sender" };
|
||
|
|
return await addToGrok();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
init,
|
||
|
|
cleanup,
|
||
|
|
setEnabled,
|
||
|
|
setConfig,
|
||
|
|
getStatus,
|
||
|
|
isEnabled,
|
||
|
|
getChatSessionId,
|
||
|
|
setSessionSyncHandler,
|
||
|
|
recordActivity,
|
||
|
|
onBridgeHostReady,
|
||
|
|
onPermissionModeChanged,
|
||
|
|
registerHandlers,
|
||
|
|
getCodexStatus,
|
||
|
|
addToCodex,
|
||
|
|
getClaudeStatus,
|
||
|
|
addToClaude,
|
||
|
|
getGrokStatus,
|
||
|
|
addToGrok,
|
||
|
|
EXTERNAL_MCP_CHAT_SESSION_ID: deps.chatSessionId,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = {
|
||
|
|
createExternalMcpController,
|
||
|
|
normalizeMode,
|
||
|
|
normalizeIdleTimeoutMinutes,
|
||
|
|
EXTERNAL_MCP_MODE_TEMPORARY,
|
||
|
|
EXTERNAL_MCP_MODE_PERSISTENT,
|
||
|
|
DEFAULT_IDLE_TIMEOUT_MINUTES,
|
||
|
|
EXTERNAL_MCP_CHAT_SESSION_ID,
|
||
|
|
};
|