feat(mcp): reconnect failed MCP servers in managed OpenCode

OpenCode marks an MCP server failed when it does not come up at startup
or when a live connection drops, and never retries. A new managed plugin
reconnects those servers with a per-server backoff (1s doubling to a 30s
cap), wakes early on mcp.tools.changed, and stops on dispose. Only the
failed state is retried; disabled and auth states stay untouched.

The OPENCODE_CONFIG_CONTENT merge that agent-tool and system-prompt each
carried is now one shared helper so the three managed plugins compose.

Claude-Session: https://claude.ai/code/session_01VqV56Hez25hTxXH4ipJfzH
This commit is contained in:
Bohdan Triapitsyn
2026-09-04 22:56:27 +03:00
parent caac24ac3d
commit ce5c0c068e
8 changed files with 375 additions and 45 deletions
+15 -4
View File
@@ -111,6 +111,7 @@ import { createDevServerScanner } from './lib/dev-servers/routes.js';
import { createDevTunnelRuntime } from './lib/dev-tunnel/runtime.js';
import { registerBrowserControlRoutes } from './lib/browser-control/routes.js';
import { createSystemPromptRuntime } from './lib/system-prompt/runtime.js';
import { createMcpReconnectRuntime } from './lib/mcp-reconnect/runtime.js';
import { createOpenChamberSessionService } from './lib/openchamber-sessions/routes.js';
import { createScheduledTaskService } from './lib/scheduled-tasks/service.js';
import { createOpenChamberControlService } from './lib/openchamber-control/service.js';
@@ -285,6 +286,7 @@ const readCustomThemesFromDisk = (...args) => themeRuntime.readCustomThemesFromD
let notificationTemplateRuntime = null;
let agentToolRuntime = null;
let systemPromptRuntime = null;
let mcpReconnectRuntime = null;
const createTimeoutSignal = (...args) => notificationTemplateRuntime.createTimeoutSignal(...args);
const formatProjectLabel = (...args) => notificationTemplateRuntime.formatProjectLabel(...args);
@@ -1222,11 +1224,15 @@ const openCodeLifecycleRuntime = createOpenCodeLifecycleRuntime({
const managedEnv = includeControl || includeWeb || includeMemory
? await (agentToolRuntime?.prepareManagedOpenCodeEnv({ includeControl, includeWeb, includeMemory }) || {})
: {};
if (settings?.optimizeSystemPrompt !== true) return managedEnv;
const configContent = managedEnv.OPENCODE_CONFIG_CONTENT ?? process.env.OPENCODE_CONFIG_CONTENT;
const systemPromptEnv = await systemPromptRuntime.prepareManagedOpenCodeEnv(configContent);
return { ...managedEnv, ...systemPromptEnv };
// Each managed plugin appends itself to the config the previous one produced.
let configContent = managedEnv.OPENCODE_CONFIG_CONTENT ?? process.env.OPENCODE_CONFIG_CONTENT;
if (settings?.optimizeSystemPrompt === true) {
({ OPENCODE_CONFIG_CONTENT: configContent } = await systemPromptRuntime.prepareManagedOpenCodeEnv(configContent));
}
// Always on for managed OpenCode: it only retries servers OpenCode gave up on.
const mcpReconnectEnv = await mcpReconnectRuntime.prepareManagedOpenCodeEnv(configContent);
return { ...managedEnv, ...mcpReconnectEnv };
},
});
@@ -1500,6 +1506,11 @@ async function main(options = {}) {
path,
dataDir: OPENCHAMBER_DATA_DIR,
});
mcpReconnectRuntime = createMcpReconnectRuntime({
fsPromises,
path,
dataDir: OPENCHAMBER_DATA_DIR,
});
// Pairing transports advertised to the create-device dialog. LAN reachability is
// derived from the SERVER's actual bind (a wildcard bind → the machine's LAN IP;