refactor: MCP types and health check

This commit is contained in:
Aleksander Grygier 2026-01-12 18:12:08 +01:00
parent 0180becb8b
commit 0009c0c300
15 changed files with 331 additions and 60 deletions

View File

@ -37,7 +37,7 @@ import type {
ChatMessageToolCallTiming,
ChatMessageAgenticTurnStats
} from '$lib/types/chat';
import type { MCPToolCall } from '$lib/types/mcp';
import type { MCPToolCall } from '$lib/types';
import type { DatabaseMessage, DatabaseMessageExtra, McpServerOverride } from '$lib/types/database';
export interface AgenticFlowCallbacks {

View File

@ -18,7 +18,6 @@
// MCP Client
export { MCPClient, mcpClient } from './mcp.client';
export type { HealthCheckState, HealthCheckParams } from './mcp.client';
// Chat Client
export { ChatClient, chatClient } from './chat.client';

View File

@ -26,31 +26,61 @@
*/
import { browser } from '$app/environment';
import { MCPService, type MCPConnection } from '$lib/services/mcp.service';
import { MCPService } from '$lib/services/mcp.service';
import type {
MCPToolCall,
OpenAIToolDefinition,
ServerStatus,
ToolExecutionResult,
MCPClientConfig
} from '$lib/types/mcp';
MCPClientConfig,
MCPConnection,
HealthCheckState,
HealthCheckParams,
ServerCapabilities,
ClientCapabilities,
MCPCapabilitiesInfo,
MCPConnectionLog
} from '$lib/types';
import { MCPConnectionPhase, MCPLogLevel, HealthCheckStatus } from '$lib/enums';
import type { McpServerOverride } from '$lib/types/database';
import { MCPError } from '$lib/errors';
import { buildMcpClientConfig, detectMcpTransportFromUrl } from '$lib/utils/mcp';
import { config } from '$lib/stores/settings.svelte';
import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp';
export type HealthCheckState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'error'; message: string }
| { status: 'success'; tools: { name: string; description?: string }[] };
export interface HealthCheckParams {
id: string;
url: string;
requestTimeoutSeconds: number;
headers?: string;
/**
* Build capabilities info from server and client capabilities
*/
function buildCapabilitiesInfo(
serverCaps?: ServerCapabilities,
clientCaps?: ClientCapabilities
): MCPCapabilitiesInfo {
return {
server: {
tools: serverCaps?.tools ? { listChanged: serverCaps.tools.listChanged } : undefined,
prompts: serverCaps?.prompts ? { listChanged: serverCaps.prompts.listChanged } : undefined,
resources: serverCaps?.resources
? {
subscribe: serverCaps.resources.subscribe,
listChanged: serverCaps.resources.listChanged
}
: undefined,
logging: !!serverCaps?.logging,
completions: !!serverCaps?.completions,
tasks: !!serverCaps?.tasks
},
client: {
roots: clientCaps?.roots ? { listChanged: clientCaps.roots.listChanged } : undefined,
sampling: !!clientCaps?.sampling,
elicitation: clientCaps?.elicitation
? {
form: !!clientCaps.elicitation.form,
url: !!clientCaps.elicitation.url
}
: undefined,
tasks: !!clientCaps?.tasks
}
};
}
export class MCPClient {
@ -531,19 +561,28 @@ export class MCPClient {
/**
* Run health check for a specific server configuration.
* Creates a temporary connection to test connectivity and list tools.
* Tracks connection phases and collects detailed connection info.
*/
async runHealthCheck(server: HealthCheckParams): Promise<void> {
const trimmedUrl = server.url.trim();
const logs: MCPConnectionLog[] = [];
let currentPhase: MCPConnectionPhase = MCPConnectionPhase.Idle;
if (!trimmedUrl) {
this.notifyHealthCheck(server.id, {
status: 'error',
message: 'Please enter a server URL first.'
status: HealthCheckStatus.Error,
message: 'Please enter a server URL first.',
logs: []
});
return;
}
this.notifyHealthCheck(server.id, { status: 'loading' });
// Initial connecting state
this.notifyHealthCheck(server.id, {
status: HealthCheckStatus.Connecting,
phase: MCPConnectionPhase.TransportCreating,
logs: []
});
const timeoutMs = Math.round(server.requestTimeoutSeconds * 1000);
const headers = this.parseHeaders(server.headers);
@ -559,19 +598,57 @@ export class MCPClient {
headers
},
DEFAULT_MCP_CONFIG.clientInfo,
DEFAULT_MCP_CONFIG.capabilities
DEFAULT_MCP_CONFIG.capabilities,
// Phase callback for tracking progress
(phase, log) => {
currentPhase = phase;
logs.push(log);
this.notifyHealthCheck(server.id, {
status: HealthCheckStatus.Connecting,
phase,
logs: [...logs]
});
}
);
const tools = connection.tools.map((tool) => ({
name: tool.name,
description: tool.description
description: tool.description,
title: tool.title
}));
this.notifyHealthCheck(server.id, { status: 'success', tools });
const capabilities = buildCapabilitiesInfo(
connection.serverCapabilities,
connection.clientCapabilities
);
this.notifyHealthCheck(server.id, {
status: HealthCheckStatus.Success,
tools,
serverInfo: connection.serverInfo,
capabilities,
transportType: connection.transportType,
protocolVersion: connection.protocolVersion,
instructions: connection.instructions,
connectionTimeMs: connection.connectionTimeMs,
logs
});
await MCPService.disconnect(connection);
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error occurred';
this.notifyHealthCheck(server.id, { status: 'error', message });
logs.push({
timestamp: new Date(),
phase: MCPConnectionPhase.Error,
message: `Connection failed: ${message}`,
level: MCPLogLevel.Error
});
this.notifyHealthCheck(server.id, {
status: HealthCheckStatus.Error,
message,
phase: currentPhase,
logs
});
}
}

View File

@ -9,7 +9,8 @@
import { settingsStore } from '$lib/stores/settings.svelte';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { parseMcpServerSettings, getServerDisplayName, getFaviconUrl } from '$lib/utils/mcp';
import type { MCPServerSettingsEntry } from '$lib/types/mcp';
import type { MCPServerSettingsEntry } from '$lib/types';
import { HealthCheckStatus } from '$lib/enums';
import { mcpStore } from '$lib/stores/mcp.svelte';
import { mcpClient } from '$lib/clients/mcp.client';
@ -156,7 +157,7 @@
{#each filteredMcpServers() as server (server.id)}
{@const healthState = mcpStore.getHealthCheckState(server.id)}
{@const hasError = healthState.status === 'error'}
{@const hasError = healthState.status === HealthCheckStatus.Error}
{@const isEnabledForChat = isServerEnabledForChat(server)}
{@const hasOverride = hasPerChatOverride(server.id)}

View File

@ -1,8 +1,8 @@
<script lang="ts">
import { onMount } from 'svelte';
import * as Card from '$lib/components/ui/card';
import type { MCPServerSettingsEntry } from '$lib/types/mcp';
import { mcpStore, type HealthCheckState } from '$lib/stores/mcp.svelte';
import type { MCPServerSettingsEntry, HealthCheckState } from '$lib/types';
import { MCPConnectionPhase } from '$lib/enums';
import { mcpClient } from '$lib/clients/mcp.client';
import McpServerCardHeader from './McpServerCardHeader.svelte';
import McpServerCardActions from './McpServerCardActions.svelte';

View File

@ -3,7 +3,7 @@
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import { getServerDisplayName, getFaviconUrl } from '$lib/utils/mcp';
import type { MCPServerSettingsEntry } from '$lib/types/mcp';
import type { MCPServerSettingsEntry } from '$lib/types';
import { mcpStore } from '$lib/stores/mcp.svelte';
import { McpServerCard } from '$lib/components/app/mcp/McpServerCard';
import McpServerForm from './McpServerForm.svelte';

View File

@ -1,4 +1,4 @@
import type { ClientCapabilities, Implementation } from '$lib/types/mcp';
import type { ClientCapabilities, Implementation } from '$lib/types';
export const DEFAULT_MCP_CONFIG = {
protocolVersion: '2025-06-18',

View File

@ -20,6 +20,8 @@ export {
MimeTypeText
} from './files';
export { MCPConnectionPhase, MCPLogLevel, MCPTransportType, HealthCheckStatus } from './mcp';
export { ModelModality } from './model';
export { ServerRole, ServerModelStatus } from './server';

View File

@ -0,0 +1,42 @@
/**
* Connection lifecycle phases for MCP protocol
*/
export enum MCPConnectionPhase {
Idle = 'idle',
TransportCreating = 'transport_creating',
TransportReady = 'transport_ready',
Initializing = 'initializing',
CapabilitiesExchanged = 'capabilities_exchanged',
ListingTools = 'listing_tools',
Connected = 'connected',
Error = 'error',
Disconnected = 'disconnected'
}
/**
* Log level for connection events
*/
export enum MCPLogLevel {
Info = 'info',
Warn = 'warn',
Error = 'error'
}
/**
* Transport types for MCP connections
*/
export enum MCPTransportType {
Websocket = 'websocket',
StreamableHttp = 'streamable_http',
SSE = 'sse'
}
/**
* Health check status for MCP servers
*/
export enum HealthCheckStatus {
Idle = 'idle',
Connecting = 'connecting',
Success = 'success',
Error = 'error'
}

View File

@ -3,4 +3,4 @@ export { DatabaseService } from './database.service';
export { ModelsService } from './models.service';
export { PropsService } from './props.service';
export { ParameterSyncService } from './parameter-sync.service';
export { MCPService, type MCPConnection } from './mcp.service';
export { MCPService } from './mcp.service';

View File

@ -24,26 +24,16 @@ import type {
ToolCallParams,
ToolExecutionResult,
Implementation,
ClientCapabilities
} from '$lib/types/mcp';
ClientCapabilities,
MCPConnection,
MCPPhaseCallback,
MCPConnectionLog,
MCPServerInfo
} from '$lib/types';
import { MCPConnectionPhase, MCPLogLevel, MCPTransportType } from '$lib/enums';
import { MCPError } from '$lib/errors';
import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp';
/**
* Represents an active MCP server connection.
* Returned by MCPService.connect() and used for subsequent operations.
*/
export interface MCPConnection {
/** MCP SDK Client instance */
client: Client;
/** Active transport */
transport: Transport;
/** Discovered tools from this server */
tools: Tool[];
/** Server identifier */
serverName: string;
}
interface ToolResultContentItem {
type: string;
text?: string;

View File

@ -20,8 +20,8 @@
*/
import { browser } from '$app/environment';
import { mcpClient, type HealthCheckState } from '$lib/clients';
import type { MCPServerSettingsEntry, McpServerUsageStats } from '$lib/types/mcp';
import { mcpClient } from '$lib/clients/mcp.client';
import type { HealthCheckState, MCPServerSettingsEntry, McpServerUsageStats } from '$lib/types';
import type { McpServerOverride } from '$lib/types/database';
import { buildMcpClientConfig, parseMcpServerSettings } from '$lib/utils/mcp';
import { config, settingsStore } from '$lib/stores/settings.svelte';
@ -52,8 +52,6 @@ function parseMcpServerUsageStats(rawStats: unknown): McpServerUsageStats {
return {};
}
export type { HealthCheckState };
class MCPStore {
private _isInitializing = $state(false);
private _error = $state<string | null>(null);

View File

@ -69,3 +69,28 @@ export type {
// Common types
export type { KeyValuePair } from './common';
// MCP types
export type {
ClientCapabilities,
ServerCapabilities,
Implementation,
MCPConnectionLog,
MCPServerInfo,
MCPCapabilitiesInfo,
MCPToolInfo,
MCPConnectionDetails,
MCPPhaseCallback,
MCPConnection,
HealthCheckState,
HealthCheckParams,
MCPServerConfig,
MCPClientConfig,
MCPServerSettingsEntry,
McpServerUsageStats,
MCPToolCall,
OpenAIToolDefinition,
ServerStatus,
ToolCallParams,
ToolExecutionResult
} from './mcp';

View File

@ -1,5 +1,7 @@
import type { MCPConnectionPhase, MCPLogLevel } from '$lib/enums/mcp';
import type {
ClientCapabilities as SDKClientCapabilities,
ServerCapabilities as SDKServerCapabilities,
Implementation as SDKImplementation,
Tool,
CallToolResult
@ -7,9 +9,148 @@ import type {
export type { Tool, CallToolResult };
export type ClientCapabilities = SDKClientCapabilities;
export type ServerCapabilities = SDKServerCapabilities;
export type Implementation = SDKImplementation;
export type MCPTransportType = 'websocket' | 'streamable_http';
/**
* Log entry for connection events
*/
export interface MCPConnectionLog {
timestamp: Date;
phase: MCPConnectionPhase;
message: string;
details?: unknown;
level: MCPLogLevel;
}
/**
* Server information returned after initialization
*/
export interface MCPServerInfo {
name: string;
version: string;
title?: string;
description?: string;
websiteUrl?: string;
icons?: Array<{ src: string; mimeType?: string; sizes?: string[] }>;
}
/**
* Detailed capabilities information
*/
export interface MCPCapabilitiesInfo {
server: {
tools?: { listChanged?: boolean };
prompts?: { listChanged?: boolean };
resources?: { subscribe?: boolean; listChanged?: boolean };
logging?: boolean;
completions?: boolean;
tasks?: boolean;
};
client: {
roots?: { listChanged?: boolean };
sampling?: boolean;
elicitation?: { form?: boolean; url?: boolean };
tasks?: boolean;
};
}
/**
* Tool information for display
*/
export interface MCPToolInfo {
name: string;
description?: string;
title?: string;
}
/**
* Full connection details for visualization
*/
export interface MCPConnectionDetails {
phase: MCPConnectionPhase;
transportType?: MCPTransportType;
protocolVersion?: string;
serverInfo?: MCPServerInfo;
capabilities?: MCPCapabilitiesInfo;
instructions?: string;
tools: MCPToolInfo[];
connectionTimeMs?: number;
error?: string;
logs: MCPConnectionLog[];
}
/**
* Callback for connection phase changes
*/
export type MCPPhaseCallback = (
phase: MCPConnectionPhase,
log: MCPConnectionLog,
details?: {
transportType?: MCPTransportType;
serverInfo?: MCPServerInfo;
serverCapabilities?: ServerCapabilities;
clientCapabilities?: ClientCapabilities;
protocolVersion?: string;
instructions?: string;
}
) => void;
/**
* Represents an active MCP server connection.
* Returned by MCPService.connect() and used for subsequent operations.
*/
export interface MCPConnection {
client: import('@modelcontextprotocol/sdk/client').Client;
transport: import('@modelcontextprotocol/sdk/shared/transport.js').Transport;
tools: import('@modelcontextprotocol/sdk/types.js').Tool[];
serverName: string;
transportType: MCPTransportType;
serverInfo?: MCPServerInfo;
serverCapabilities?: ServerCapabilities;
clientCapabilities?: ClientCapabilities;
protocolVersion?: string;
instructions?: string;
connectionTimeMs: number;
}
/**
* Extended health check state with detailed connection info
*/
export type HealthCheckState =
| { status: import('$lib/enums/mcp').HealthCheckStatus.Idle }
| {
status: import('$lib/enums/mcp').HealthCheckStatus.Connecting;
phase: MCPConnectionPhase;
logs: MCPConnectionLog[];
}
| {
status: import('$lib/enums/mcp').HealthCheckStatus.Error;
message: string;
phase?: MCPConnectionPhase;
logs: MCPConnectionLog[];
}
| {
status: import('$lib/enums/mcp').HealthCheckStatus.Success;
tools: MCPToolInfo[];
serverInfo?: MCPServerInfo;
capabilities?: MCPCapabilitiesInfo;
transportType?: MCPTransportType;
protocolVersion?: string;
instructions?: string;
connectionTimeMs?: number;
logs: MCPConnectionLog[];
};
/**
* Health check parameters
*/
export interface HealthCheckParams {
id: string;
url: string;
requestTimeoutSeconds: number;
headers?: string;
}
export type MCPServerConfig = {
transport?: MCPTransportType;

View File

@ -1,11 +1,7 @@
import type {
MCPTransportType,
MCPClientConfig,
MCPServerConfig,
MCPServerSettingsEntry
} from '$lib/types/mcp';
import type { MCPClientConfig, MCPServerConfig, MCPServerSettingsEntry } from '$lib/types';
import type { SettingsConfigType } from '$lib/types/settings';
import type { McpServerOverride } from '$lib/types/database';
import { MCPTransportType } from '$lib/enums';
import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp';
import { normalizePositiveNumber } from '$lib/utils/number';
@ -17,8 +13,8 @@ export function detectMcpTransportFromUrl(url: string): MCPTransportType {
const normalized = url.trim().toLowerCase();
return normalized.startsWith('ws://') || normalized.startsWith('wss://')
? 'websocket'
: 'streamable_http';
? MCPTransportType.Websocket
: MCPTransportType.StreamableHttp;
}
/**