fix: handle MCP WebSocket disconnections with auto-reconnect (SDK provides no native reconnection)

This commit is contained in:
Pascal 2026-02-04 20:56:06 +01:00
parent 58e3a1890b
commit bba8f64e0f
3 changed files with 105 additions and 10 deletions

View File

@ -11,3 +11,7 @@ export const DEFAULT_MCP_CONFIG = {
export const MCP_SERVER_ID_PREFIX = 'LlamaCpp-WebUI-MCP-Server-';
export const DEFAULT_CLIENT_VERSION = '1.0.0';
export const DEFAULT_IMAGE_MIME_TYPE = 'image/png';
export const MCP_RECONNECT_INITIAL_DELAY = 1000;
export const MCP_RECONNECT_BACKOFF_MULTIPLIER = 2;
export const MCP_RECONNECT_MAX_DELAY = 30000;

View File

@ -215,6 +215,17 @@ export class MCPService {
}
const { transport, type: transportType } = this.createTransport(serverConfig);
// Setup WebSocket reconnection handler
if (transportType === MCPTransportType.WEBSOCKET) {
transport.onclose = () => {
console.log(`[MCPService][${serverName}] WebSocket closed, notifying for reconnection`);
onPhase?.(
MCPConnectionPhase.DISCONNECTED,
this.createLog(MCPConnectionPhase.DISCONNECTED, 'WebSocket connection closed')
);
};
}
// Phase: Transport ready
onPhase?.(
MCPConnectionPhase.TRANSPORT_READY,
@ -319,6 +330,10 @@ export class MCPService {
static async disconnect(connection: MCPConnection): Promise<void> {
console.log(`[MCPService][${connection.serverName}] Disconnecting...`);
try {
// Prevent reconnection on voluntary disconnect
if (connection.transport.onclose) {
connection.transport.onclose = undefined;
}
await connection.client.close();
} catch (error) {
console.warn(`[MCPService][${connection.serverName}] Error during disconnect:`, error);

View File

@ -25,7 +25,13 @@ import { config, settingsStore } from '$lib/stores/settings.svelte';
import { mcpResourceStore } from '$lib/stores/mcp-resources.svelte';
import { parseMcpServerSettings, detectMcpTransportFromUrl } from '$lib/utils';
import { MCPConnectionPhase, MCPLogLevel, HealthCheckStatus, MCPRefType } from '$lib/enums';
import { DEFAULT_MCP_CONFIG, MCP_SERVER_ID_PREFIX } from '$lib/constants/mcp';
import {
DEFAULT_MCP_CONFIG,
MCP_SERVER_ID_PREFIX,
MCP_RECONNECT_INITIAL_DELAY,
MCP_RECONNECT_BACKOFF_MULTIPLIER,
MCP_RECONNECT_MAX_DELAY
} from '$lib/constants/mcp';
import type {
MCPToolCall,
OpenAIToolDefinition,
@ -189,6 +195,7 @@ class MCPStore {
private connections = new Map<string, MCPConnection>();
private toolsIndex = new Map<string, string>();
private serverConfigs = new Map<string, MCPServerConfig>(); // Store configs for reconnection
private configSignature: string | null = null;
private initPromise: Promise<boolean> | null = null;
private activeFlowCount = 0;
@ -405,13 +412,22 @@ class MCPStore {
const capabilities = mcpConfig.capabilities ?? DEFAULT_MCP_CONFIG.capabilities;
const results = await Promise.allSettled(
serverEntries.map(async ([name, serverConfig]) => {
// Store config for reconnection
this.serverConfigs.set(name, serverConfig);
const listChangedHandlers = this.createListChangedHandlers(name);
const connection = await MCPService.connect(
name,
serverConfig,
clientInfo,
capabilities,
undefined,
(phase) => {
// Handle WebSocket disconnection
if (phase === MCPConnectionPhase.DISCONNECTED) {
console.log(`[MCPStore][${name}] Connection lost, starting auto-reconnect`);
this.autoReconnect(name);
}
},
listChangedHandlers
);
return { name, connection };
@ -534,10 +550,57 @@ class MCPStore {
);
this.connections.clear();
this.toolsIndex.clear();
this.serverConfigs.clear();
this.configSignature = null;
this.updateState({ isInitializing: false, error: null, toolCount: 0, connectedServers: [] });
}
/**
* Auto-reconnect to a server with exponential backoff.
* Continues indefinitely until successful.
*/
private async autoReconnect(serverName: string): Promise<void> {
const serverConfig = this.serverConfigs.get(serverName);
if (!serverConfig) {
console.error(`[MCPStore] No config found for ${serverName}, cannot reconnect`);
return;
}
let backoff = MCP_RECONNECT_INITIAL_DELAY;
while (true) {
await new Promise((resolve) => setTimeout(resolve, backoff));
console.log(`[MCPStore][${serverName}] Auto-reconnecting...`);
try {
const listChangedHandlers = this.createListChangedHandlers(serverName);
const connection = await MCPService.connect(
serverName,
serverConfig,
DEFAULT_MCP_CONFIG.clientInfo,
DEFAULT_MCP_CONFIG.capabilities,
undefined,
listChangedHandlers
);
// Replace old connection with new one
this.connections.set(serverName, connection);
// Rebuild tool index for this server
for (const tool of connection.tools) {
this.toolsIndex.set(tool.name, serverName);
}
console.log(`[MCPStore][${serverName}] Reconnected successfully`);
break;
} catch (error) {
console.warn(`[MCPStore][${serverName}] Reconnection failed:`, error);
backoff = Math.min(backoff * MCP_RECONNECT_BACKOFF_MULTIPLIER, MCP_RECONNECT_MAX_DELAY);
}
}
}
getToolDefinitionsForLLM(): OpenAIToolDefinition[] {
const tools: OpenAIToolDefinition[] = [];
for (const connection of this.connections.values()) {
@ -865,16 +928,21 @@ class MCPStore {
const headers = this.parseHeaders(server.headers);
try {
const serverConfig: MCPServerConfig = {
url: trimmedUrl,
transport: detectMcpTransportFromUrl(trimmedUrl),
handshakeTimeoutMs: DEFAULT_MCP_CONFIG.connectionTimeoutMs,
requestTimeoutMs: timeoutMs,
headers,
useProxy: server.useProxy
};
// Store config for reconnection
this.serverConfigs.set(server.id, serverConfig);
const connection = await MCPService.connect(
server.id,
{
url: trimmedUrl,
transport: detectMcpTransportFromUrl(trimmedUrl),
handshakeTimeoutMs: DEFAULT_MCP_CONFIG.connectionTimeoutMs,
requestTimeoutMs: timeoutMs,
headers,
useProxy: server.useProxy
},
serverConfig,
DEFAULT_MCP_CONFIG.clientInfo,
DEFAULT_MCP_CONFIG.capabilities,
(phase, log) => {
@ -885,6 +953,14 @@ class MCPStore {
phase,
logs: [...logs]
});
// Handle WebSocket disconnection
if (phase === MCPConnectionPhase.DISCONNECTED && promoteToActive) {
console.log(
`[MCPStore][${server.id}] Connection lost during health check, starting auto-reconnect`
);
this.autoReconnect(server.id);
}
}
);
const tools = connection.tools.map((tool) => ({