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
@@ -1,5 +1,5 @@
import { parse as parseJsonc } from 'jsonc-parser';
import { pathToFileURL } from 'node:url';
import { appendManagedPlugin } from '../opencode/managed-plugin-config.js';
const PROVIDER_PROMPT_BOUNDARY = 'You are powered by the model named';
const MINIMAL_IDENTITY = 'You are OpenCode, a coding agent.';
@@ -33,25 +33,6 @@ export const OpenChamberSystemPromptPlugin = async () => ({
})
`;
const mergePluginConfig = (rawConfig, pluginUrl) => {
const errors = [];
const parsed = typeof rawConfig === 'string' && rawConfig.trim()
? parseJsonc(rawConfig, errors, { allowTrailingComma: true })
: {};
if (errors.length > 0 || !parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('OPENCODE_CONFIG_CONTENT must contain a valid JSON object before OpenChamber can inject its system prompt optimizer');
}
if (parsed.plugin !== undefined && !Array.isArray(parsed.plugin)) {
throw new Error('OPENCODE_CONFIG_CONTENT plugin must be an array before OpenChamber can inject its system prompt optimizer');
}
const configured = Array.isArray(parsed.plugin) ? parsed.plugin : [];
parsed.plugin = [
...configured.filter((value) => value !== pluginUrl && (!Array.isArray(value) || value[0] !== pluginUrl)),
pluginUrl,
];
return JSON.stringify(parsed);
};
export const createSystemPromptRuntime = ({ fsPromises, path, dataDir }) => {
const pluginDirectory = path.join(dataDir, 'system-prompt');
const pluginPath = path.join(pluginDirectory, 'openchamber-system-prompt-plugin.js');
@@ -60,7 +41,7 @@ export const createSystemPromptRuntime = ({ fsPromises, path, dataDir }) => {
await fsPromises.mkdir(pluginDirectory, { recursive: true });
await fsPromises.writeFile(pluginPath, createPluginSource(), { mode: 0o600 });
return {
OPENCODE_CONFIG_CONTENT: mergePluginConfig(rawConfig, pathToFileURL(pluginPath).href),
OPENCODE_CONFIG_CONTENT: appendManagedPlugin(rawConfig, pathToFileURL(pluginPath).href, 'system prompt optimizer'),
};
};