import type { McpExtensionState } from './_state.ts'; import { MCP_STATUS_EVENT, MCP_STATUS_SNAPSHOT_VERSION, type McpServerStatusSnapshot, type McpStatusSnapshot, } from './_types.ts'; const FAILURE_BACKOFF_MS = 60 * 1000; export interface McpStatusEventBus { emit(channel: string, data: unknown): void; } function getActiveFailureAgeSeconds(state: McpExtensionState, serverName: string): number | undefined { const failedAt = state.failureTracker.get(serverName); if (!failedAt) { return undefined; } const ageMs = Date.now() - failedAt; if (ageMs > FAILURE_BACKOFF_MS) { return undefined; } return Math.round(ageMs / 1000); } /** Build a sanitized snapshot without connecting or querying any MCP server. */ export function createMcpStatusSnapshot(state: McpExtensionState): McpStatusSnapshot { const servers: McpServerStatusSnapshot[] = []; let totalTools = 0; let totalResources = 0; let connectedCount = 0; let disabledCount = 0; for (const name of Object.keys(state.config.mcpServers)) { const definition = state.config.mcpServers[name]; const disabled = definition?.disabled === true; const connection = disabled ? undefined : state.manager.getConnection(name); const metadata = disabled ? undefined : state.toolMetadata.get(name); const toolCount = metadata?.length ?? (connection?.status === 'connected' ? connection.tools.length : 0); const resourceCount = disabled ? undefined : (state.resourceCounts?.get(name) ?? (connection?.status === 'connected' ? connection.resources.length : undefined)); const failedAgoSeconds = disabled ? undefined : getActiveFailureAgeSeconds(state, name); let status: McpServerStatusSnapshot['status'] = 'not-connected'; if (disabled) { status = 'disabled'; disabledCount++; } else if (connection?.status === 'connected') { status = 'connected'; connectedCount++; } else if (connection?.status === 'needs-auth') { status = 'needs-auth'; } else if (failedAgoSeconds !== undefined) { status = 'failed'; } else if (metadata !== undefined) { status = 'cached'; } totalTools += disabled ? 0 : toolCount; if (!disabled && resourceCount !== undefined) { totalResources += resourceCount; } servers.push({ name, status, toolCount, ...(resourceCount !== undefined ? { resourceCount } : {}), ...(status === 'failed' && failedAgoSeconds !== undefined ? { failedAgoSeconds } : {}), disabled, }); } return { version: MCP_STATUS_SNAPSHOT_VERSION, servers, totalTools, totalResources, connectedCount, disabledCount, }; } export function publishMcpStatusSnapshot(state: McpExtensionState, snapshot?: McpStatusSnapshot): void { const events = state.statusEvents; if (!events) { return; } try { events.emit(MCP_STATUS_EVENT, snapshot ?? createMcpStatusSnapshot(state)); } catch { // Event consumers must not be able to interrupt MCP operations. } } export function publishMcpStatusShutdown(events: McpStatusEventBus | undefined): void { if (!events) { return; } try { events.emit(MCP_STATUS_EVENT, { version: MCP_STATUS_SNAPSHOT_VERSION, servers: [], totalTools: 0, totalResources: 0, connectedCount: 0, disabledCount: 0, } satisfies McpStatusSnapshot); } catch { // Event consumers must not be able to interrupt MCP shutdown. } } export type { McpServerStatusSnapshot, McpStatusSnapshot } from './_types.ts'; export { MCP_STATUS_EVENT, MCP_STATUS_SNAPSHOT_VERSION } from './_types.ts';