From 5407b2efab746b6e506e8c95c292735673a0e697 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Mon, 12 Jan 2026 18:26:48 +0100 Subject: [PATCH] feat: MCP connection details WIP --- .../mcp/McpServerCard/McpServerCard.svelte | 41 ++++- .../webui/src/lib/services/mcp.service.ts | 166 ++++++++++++++++-- 2 files changed, 186 insertions(+), 21 deletions(-) diff --git a/tools/server/webui/src/lib/components/app/mcp/McpServerCard/McpServerCard.svelte b/tools/server/webui/src/lib/components/app/mcp/McpServerCard/McpServerCard.svelte index cd7b1a91ea..5e59cedf55 100644 --- a/tools/server/webui/src/lib/components/app/mcp/McpServerCard/McpServerCard.svelte +++ b/tools/server/webui/src/lib/components/app/mcp/McpServerCard/McpServerCard.svelte @@ -2,7 +2,8 @@ import { onMount } from 'svelte'; import * as Card from '$lib/components/ui/card'; import type { MCPServerSettingsEntry, HealthCheckState } from '$lib/types'; - import { MCPConnectionPhase } from '$lib/enums'; + import { HealthCheckStatus } from '$lib/enums'; + import { mcpStore } from '$lib/stores/mcp.svelte'; import { mcpClient } from '$lib/clients/mcp.client'; import McpServerCardHeader from './McpServerCardHeader.svelte'; import McpServerCardActions from './McpServerCardActions.svelte'; @@ -22,11 +23,39 @@ let { server, displayName, faviconUrl, onToggle, onUpdate, onDelete }: Props = $props(); let healthState = $derived(mcpStore.getHealthCheckState(server.id)); - let isHealthChecking = $derived(healthState.status === 'loading'); - let isConnected = $derived(healthState.status === 'success'); - let isError = $derived(healthState.status === 'error'); - let errorMessage = $derived(healthState.status === 'error' ? healthState.message : undefined); - let tools = $derived(healthState.status === 'success' ? healthState.tools : []); + let isHealthChecking = $derived(healthState.status === HealthCheckStatus.Connecting); + let isConnected = $derived(healthState.status === HealthCheckStatus.Success); + let isError = $derived(healthState.status === HealthCheckStatus.Error); + let errorMessage = $derived( + healthState.status === HealthCheckStatus.Error ? healthState.message : undefined + ); + let tools = $derived(healthState.status === HealthCheckStatus.Success ? healthState.tools : []); + + let connectionLogs = $derived( + healthState.status === HealthCheckStatus.Connecting || + healthState.status === HealthCheckStatus.Success || + healthState.status === HealthCheckStatus.Error + ? healthState.logs + : [] + ); + let serverInfo = $derived( + healthState.status === HealthCheckStatus.Success ? healthState.serverInfo : undefined + ); + let capabilities = $derived( + healthState.status === HealthCheckStatus.Success ? healthState.capabilities : undefined + ); + let transportType = $derived( + healthState.status === HealthCheckStatus.Success ? healthState.transportType : undefined + ); + let protocolVersion = $derived( + healthState.status === HealthCheckStatus.Success ? healthState.protocolVersion : undefined + ); + let connectionTimeMs = $derived( + healthState.status === HealthCheckStatus.Success ? healthState.connectionTimeMs : undefined + ); + let instructions = $derived( + healthState.status === HealthCheckStatus.Success ? healthState.instructions : undefined + ); let isEditing = $state(!server.url.trim()); let showDeleteDialog = $state(false); diff --git a/tools/server/webui/src/lib/services/mcp.service.ts b/tools/server/webui/src/lib/services/mcp.service.ts index 4ddd7e96f2..42500cdf76 100644 --- a/tools/server/webui/src/lib/services/mcp.service.ts +++ b/tools/server/webui/src/lib/services/mcp.service.ts @@ -49,11 +49,33 @@ interface ToolCallResult { } export class MCPService { + /** + * Create a connection log entry + */ + private static createLog( + phase: MCPConnectionPhase, + message: string, + level: MCPLogLevel = MCPLogLevel.Info, + details?: unknown + ): MCPConnectionLog { + return { + timestamp: new Date(), + phase, + message, + level, + details + }; + } + /** * Create transport based on server configuration. * Supports WebSocket, StreamableHTTP (modern), and SSE (legacy) transports. + * Returns both transport and the type used. */ - static createTransport(config: MCPServerConfig): Transport { + static createTransport(config: MCPServerConfig): { + transport: Transport; + type: MCPTransportType; + } { if (!config.url) { throw new Error('MCP server configuration is missing url'); } @@ -64,26 +86,38 @@ export class MCPService { if (config.headers) { requestInit.headers = config.headers; } + if (config.credentials) { requestInit.credentials = config.credentials; } if (config.transport === 'websocket') { console.log(`[MCPService] Creating WebSocket transport for ${url.href}`); - return new WebSocketClientTransport(url); + + return { + transport: new WebSocketClientTransport(url), + type: MCPTransportType.Websocket + }; } try { console.log(`[MCPService] Creating StreamableHTTP transport for ${url.href}`); - return new StreamableHTTPClientTransport(url, { - requestInit, - sessionId: config.sessionId - }); + + return { + transport: new StreamableHTTPClientTransport(url, { + requestInit, + sessionId: config.sessionId + }), + type: MCPTransportType.StreamableHttp + }; } catch (httpError) { console.warn(`[MCPService] StreamableHTTP failed, trying SSE transport...`, httpError); try { - return new SSEClientTransport(url, { requestInit }); + return { + transport: new SSEClientTransport(url, { requestInit }), + type: MCPTransportType.SSE + }; } catch (sseError) { const httpMsg = httpError instanceof Error ? httpError.message : String(httpError); const sseMsg = sseError instanceof Error ? sseError.message : String(sseError); @@ -93,20 +127,58 @@ export class MCPService { } /** - * Connect to a single MCP server. - * Returns connection object with client, transport, and discovered tools. + * Extract server info from SDK Implementation type + */ + private static extractServerInfo(impl: Implementation | undefined): MCPServerInfo | undefined { + if (!impl) return undefined; + return { + name: impl.name, + version: impl.version, + title: impl.title, + description: impl.description, + websiteUrl: impl.websiteUrl, + icons: impl.icons?.map((icon) => ({ + src: icon.src, + mimeType: icon.mimeType, + sizes: icon.sizes + })) + }; + } + + /** + * Connect to a single MCP server with detailed phase tracking. + * Returns connection object with client, transport, discovered tools, and connection details. + * @param onPhase - Optional callback for connection phase changes */ static async connect( serverName: string, serverConfig: MCPServerConfig, clientInfo?: Implementation, - capabilities?: ClientCapabilities + capabilities?: ClientCapabilities, + onPhase?: MCPPhaseCallback ): Promise { + const startTime = performance.now(); const effectiveClientInfo = clientInfo ?? DEFAULT_MCP_CONFIG.clientInfo; const effectiveCapabilities = capabilities ?? DEFAULT_MCP_CONFIG.capabilities; + // Phase: Creating transport + onPhase?.( + MCPConnectionPhase.TransportCreating, + this.createLog( + MCPConnectionPhase.TransportCreating, + `Creating transport for ${serverConfig.url}` + ) + ); + console.log(`[MCPService][${serverName}] Creating transport...`); - const transport = this.createTransport(serverConfig); + const { transport, type: transportType } = this.createTransport(serverConfig); + + // Phase: Transport ready + onPhase?.( + MCPConnectionPhase.TransportReady, + this.createLog(MCPConnectionPhase.TransportReady, `Transport ready (${transportType})`), + { transportType } + ); const client = new Client( { @@ -116,19 +188,83 @@ export class MCPService { { capabilities: effectiveCapabilities } ); + // Phase: Initializing + onPhase?.( + MCPConnectionPhase.Initializing, + this.createLog(MCPConnectionPhase.Initializing, 'Sending initialize request...') + ); + console.log(`[MCPService][${serverName}] Connecting to server...`); await client.connect(transport); - console.log(`[MCPService][${serverName}] Connected, listing tools...`); - const tools = await this.listTools({ client, transport, tools: [], serverName }); + const serverVersion = client.getServerVersion(); + const serverCapabilities = client.getServerCapabilities(); + const instructions = client.getInstructions(); + const serverInfo = this.extractServerInfo(serverVersion); - console.log(`[MCPService][${serverName}] Initialization complete with ${tools.length} tools`); + // Phase: Capabilities exchanged + onPhase?.( + MCPConnectionPhase.CapabilitiesExchanged, + this.createLog( + MCPConnectionPhase.CapabilitiesExchanged, + 'Capabilities exchanged successfully', + MCPLogLevel.Info, + { + serverCapabilities, + serverInfo + } + ), + { + serverInfo, + serverCapabilities, + clientCapabilities: effectiveCapabilities, + instructions + } + ); + + // Phase: Listing tools + onPhase?.( + MCPConnectionPhase.ListingTools, + this.createLog(MCPConnectionPhase.ListingTools, 'Listing available tools...') + ); + + console.log(`[MCPService][${serverName}] Connected, listing tools...`); + const tools = await this.listTools({ + client, + transport, + tools: [], + serverName, + transportType, + connectionTimeMs: 0 + }); + + const connectionTimeMs = Math.round(performance.now() - startTime); + + // Phase: Connected + onPhase?.( + MCPConnectionPhase.Connected, + this.createLog( + MCPConnectionPhase.Connected, + `Connection established with ${tools.length} tools (${connectionTimeMs}ms)` + ) + ); + + console.log( + `[MCPService][${serverName}] Initialization complete with ${tools.length} tools in ${connectionTimeMs}ms` + ); return { client, transport, tools, - serverName + serverName, + transportType, + serverInfo, + serverCapabilities, + clientCapabilities: effectiveCapabilities, + protocolVersion: DEFAULT_MCP_CONFIG.protocolVersion, + instructions, + connectionTimeMs }; }