From 763742ed307040c2496a8b685d500eed2fb6da7e Mon Sep 17 00:00:00 2001 From: Jerome Date: Thu, 12 Jun 2025 14:02:00 +0100 Subject: [PATCH] Implement improved auto-reconnect logic and fix TypeScript errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Features - Add proactive health check monitoring for MCP connections (30s intervals) - Implement automatic reconnection on health check failures - Add proper cleanup of health check intervals on disconnect ## Fixes - Resolve OAuth type compatibility issues with MCP SDK - Fix naming collision between InferenceProvider class and React component - Remove unused React imports across all components (new JSX transform) - Clean up unused type imports and variables - Remove unused handleOAuthAuthentication function ## Technical Details - Health checks use lightweight listTools() calls to verify connection state - Automatic reconnection triggers when health checks fail - Maintains existing exponential backoff retry logic - All TypeScript compilation errors resolved 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/App.tsx | 8 +- src/components/ChatInterface.tsx | 4 +- src/components/ConversationApp.tsx | 2 +- src/components/ConversationSidebar.tsx | 10 +- src/components/InferenceLogin.tsx | 2 +- src/components/InferenceTest.tsx | 2 +- src/components/MCPStatus.tsx | 1 - src/components/MCPTest.tsx | 2 +- src/components/MessageList.tsx | 3 +- src/components/OAuthCallback.tsx | 2 +- src/contexts/ConversationContext.tsx | 3 +- src/contexts/InferenceContext.tsx | 6 +- src/contexts/MCPContext.tsx | 11 +-- src/hooks/useAgentLoop.ts | 1 - src/mcp/connection.ts | 129 ++++++++++++++++++------- src/providers/openrouter/client.ts | 5 +- tsconfig.json | 2 +- 17 files changed, 122 insertions(+), 71 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index d1da0da..0d90bd1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react' -import { InferenceProvider } from '@/contexts/InferenceContext' +import { useState } from 'react' +import { InferenceContextProvider } from '@/contexts/InferenceContext' import { MCPProvider } from '@/contexts/MCPContext' import { InferenceTest } from '@/components/InferenceTest' import { MCPTest } from '@/components/MCPTest' @@ -23,7 +23,7 @@ function App() { const [activeTab, setActiveTab] = useState<'conversations' | 'inference' | 'mcp'>('conversations'); return ( - +
{/* Tab Navigation */} @@ -70,7 +70,7 @@ function App() { {activeTab === 'mcp' && }
-
+ ) } diff --git a/src/components/ChatInterface.tsx b/src/components/ChatInterface.tsx index caf19ec..74b7af5 100644 --- a/src/components/ChatInterface.tsx +++ b/src/components/ChatInterface.tsx @@ -1,6 +1,6 @@ // Main chat interface with message display and input -import React, { useState, useRef, useEffect } from 'react'; +import { useState, useRef, useEffect } from 'react'; import { useConversation } from '@/contexts/ConversationContext'; import { useInference } from '@/contexts/InferenceContext'; import { MessageList } from './MessageList'; @@ -16,7 +16,7 @@ export function ChatInterface() { getAgentLoopState, } = useConversation(); - const { provider: currentProvider, isAuthenticated } = useInference(); + const { provider: currentProvider } = useInference(); const [isLoading, setIsLoading] = useState(false); const messagesEndRef = useRef(null); diff --git a/src/components/ConversationApp.tsx b/src/components/ConversationApp.tsx index 86576aa..0463e89 100644 --- a/src/components/ConversationApp.tsx +++ b/src/components/ConversationApp.tsx @@ -1,6 +1,6 @@ // Main conversation application with sidebar and chat interface -import React, { useState } from 'react'; +import { useState } from 'react'; import { ConversationProvider } from '@/contexts/ConversationContext'; import { ConversationSidebar } from './ConversationSidebar'; import { ChatInterface } from './ChatInterface'; diff --git a/src/components/ConversationSidebar.tsx b/src/components/ConversationSidebar.tsx index ca2490d..094ef33 100644 --- a/src/components/ConversationSidebar.tsx +++ b/src/components/ConversationSidebar.tsx @@ -1,6 +1,5 @@ // Conversation sidebar with conversation list and management -import React from 'react'; import { useConversation } from '@/contexts/ConversationContext'; export function ConversationSidebar() { @@ -91,10 +90,11 @@ export function ConversationSidebar() {

{conversation.messages.length > 0 && (

- {conversation.messages[conversation.messages.length - 1]?.content?.[0]?.type === 'text' - ? conversation.messages[conversation.messages.length - 1].content[0].text - : 'Tool interaction' - } + {(() => { + const lastMessage = conversation.messages[conversation.messages.length - 1]; + const firstBlock = lastMessage?.content?.[0]; + return firstBlock?.type === 'text' ? firstBlock.text : 'Tool interaction'; + })()}

)} diff --git a/src/components/InferenceLogin.tsx b/src/components/InferenceLogin.tsx index dbb2163..431f319 100644 --- a/src/components/InferenceLogin.tsx +++ b/src/components/InferenceLogin.tsx @@ -1,6 +1,6 @@ // Inference provider login component for inline authentication -import React, { useState, useMemo } from 'react'; +import { useState, useMemo } from 'react'; import { useInference } from '@/contexts/InferenceContext'; import { OpenRouterApiProvider } from '@/providers/openrouter/api-provider'; import { OpenRouterOAuthProvider } from '@/providers/openrouter/oauth-provider'; diff --git a/src/components/InferenceTest.tsx b/src/components/InferenceTest.tsx index d246ed0..50d321d 100644 --- a/src/components/InferenceTest.tsx +++ b/src/components/InferenceTest.tsx @@ -1,6 +1,6 @@ // Test UI for inference provider functionality -import React, { useState, useCallback } from 'react'; +import { useState, useCallback } from 'react'; import { useInference } from '@/contexts/InferenceContext'; import { OpenRouterApiProvider, OpenRouterOAuthProvider } from '@/providers/openrouter'; import type { ChatMessage, InferenceRequest } from '@/types/inference'; diff --git a/src/components/MCPStatus.tsx b/src/components/MCPStatus.tsx index 6c2bc54..66a115e 100644 --- a/src/components/MCPStatus.tsx +++ b/src/components/MCPStatus.tsx @@ -1,6 +1,5 @@ // MCP status display for the conversation sidebar -import React from 'react'; import { useMCP } from '@/contexts/MCPContext'; export function MCPStatus() { diff --git a/src/components/MCPTest.tsx b/src/components/MCPTest.tsx index 2636cc4..aa905a8 100644 --- a/src/components/MCPTest.tsx +++ b/src/components/MCPTest.tsx @@ -1,6 +1,6 @@ // Test UI for MCP provider functionality -import React, { useState, useCallback } from 'react'; +import { useState, useCallback } from 'react'; import { useMCP } from '@/contexts/MCPContext'; import type { MCPServerConfig } from '@/types/mcp'; diff --git a/src/components/MessageList.tsx b/src/components/MessageList.tsx index bfc9200..b3b1ee2 100644 --- a/src/components/MessageList.tsx +++ b/src/components/MessageList.tsx @@ -1,7 +1,6 @@ // Message list component to display conversation messages with tool calls -import React from 'react'; -import type { ConversationMessage, ToolUseBlock, ToolResultBlock } from '@/types/conversation'; +import type { ConversationMessage } from '@/types/conversation'; interface MessageListProps { messages: ConversationMessage[]; diff --git a/src/components/OAuthCallback.tsx b/src/components/OAuthCallback.tsx index b96f3b8..e0c6507 100644 --- a/src/components/OAuthCallback.tsx +++ b/src/components/OAuthCallback.tsx @@ -1,6 +1,6 @@ // OAuth callback handler for popup-based OAuth flows -import React, { useEffect } from 'react'; +import { useEffect } from 'react'; interface OAuthCallbackProps { type: 'inference' | 'mcp'; diff --git a/src/contexts/ConversationContext.tsx b/src/contexts/ConversationContext.tsx index d6ba3ec..a56560f 100644 --- a/src/contexts/ConversationContext.tsx +++ b/src/contexts/ConversationContext.tsx @@ -1,6 +1,6 @@ // React context for conversation management and agent loop orchestration -import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode, useRef } from 'react'; +import { createContext, useContext, useState, useCallback, useEffect, ReactNode, useRef } from 'react'; import { v4 as uuidv4 } from 'uuid'; import type { @@ -8,7 +8,6 @@ import type { ConversationMessage, ConversationContextValue, AgentLoopState, - ConversationContentBlock, } from '@/types/conversation'; import { useAgentLoop } from '@/hooks/useAgentLoop'; diff --git a/src/contexts/InferenceContext.tsx b/src/contexts/InferenceContext.tsx index 04cf371..15d2d33 100644 --- a/src/contexts/InferenceContext.tsx +++ b/src/contexts/InferenceContext.tsx @@ -1,6 +1,6 @@ // React context for inference provider management -import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react'; +import { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react'; import type { InferenceProvider, InferenceRequest, @@ -38,12 +38,12 @@ interface InferenceProviderProps { children: ReactNode; } -export function InferenceProvider({ children }: InferenceProviderProps) { +export function InferenceContextProvider({ children }: InferenceProviderProps) { const [provider, setProviderState] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [selectedModelId, setSelectedModelId] = useState(undefined); - const [authStateVersion, setAuthStateVersion] = useState(0); // Force re-renders on auth changes + const [_, setAuthStateVersion] = useState(0); // Force re-renders on auth changes const setProvider = useCallback((newProvider: InferenceProvider) => { setProviderState(newProvider); diff --git a/src/contexts/MCPContext.tsx b/src/contexts/MCPContext.tsx index 3f6b47b..71bc612 100644 --- a/src/contexts/MCPContext.tsx +++ b/src/contexts/MCPContext.tsx @@ -1,6 +1,6 @@ // React context for MCP server connection management -import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode, useRef } from 'react'; +import { createContext, useContext, useState, useCallback, useEffect, ReactNode, useRef } from 'react'; import { v4 as uuidv4 } from 'uuid'; import type { @@ -8,7 +8,6 @@ import type { MCPServerConfig, MCPResource, MCPContextValue, - MCPError, } from '@/types/mcp'; import type { Tool } from '@/types/inference'; import { MCPConnectionManager } from '@/mcp/connection'; @@ -158,18 +157,14 @@ export function MCPProvider({ children }: MCPProviderProps) { authType: config.authType, errorType: typeof error, errorConstructor: error?.constructor?.name, - errorMessage: error?.message, - errorDetails: error?.details, - detailsConstructor: error?.details?.constructor?.name, + errorMessage: error instanceof Error ? error.message : 'Unknown error', fullError: error }); const isUnauthorizedError = config.authType === 'oauth' && ( (error instanceof Error && error.message === 'Unauthorized') || - (error instanceof Error && error.constructor.name === 'UnauthorizedError') || - (error && typeof error === 'object' && error.message === 'Unauthorized' && - error.details && error.details.constructor && error.details.constructor.name === 'UnauthorizedError') + (error instanceof Error && error.constructor.name === 'UnauthorizedError') ); if (isUnauthorizedError) { diff --git a/src/hooks/useAgentLoop.ts b/src/hooks/useAgentLoop.ts index 7c2e6bc..63e9e38 100644 --- a/src/hooks/useAgentLoop.ts +++ b/src/hooks/useAgentLoop.ts @@ -9,7 +9,6 @@ import type { UseAgentLoopReturn, ConversationMessage, Conversation, - ToolUseBlock, ToolResultBlock, TestTool, } from '@/types/conversation'; diff --git a/src/mcp/connection.ts b/src/mcp/connection.ts index fde9646..92ca4a9 100644 --- a/src/mcp/connection.ts +++ b/src/mcp/connection.ts @@ -5,14 +5,8 @@ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; import { - auth, - discoverOAuthMetadata, - extractResourceMetadataUrl, - type OAuthClientProvider, - type OAuthClientMetadata, - type OAuthClientInformation, - type OAuthTokens, - type OAuthClientInformationFull + auth, + type OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'; import type { @@ -20,7 +14,6 @@ import type { MCPServerConfig, MCPResource, MCPPrompt, - MCPConnectionManager, MCPError, } from '@/types/mcp'; import type { Tool } from '@/types/inference'; @@ -31,6 +24,33 @@ interface MCPOAuthState { expiresAt: number; } +// OAuth types matching the MCP SDK's internal interfaces +interface OAuthClientMetadata { + redirect_uris: string[]; + grant_types?: string[]; + response_types?: string[]; + client_name?: string; + token_endpoint_auth_method?: string; + scope?: string; + jwks_uri?: string; +} + +interface OAuthClientInformation { + client_id: string; + client_secret?: string; + registration_access_token?: string; + registration_client_uri?: string; + client_secret_expires_at?: number; +} + +interface OAuthTokens { + access_token: string; + token_type: string; + refresh_token?: string; + expires_in?: number; + scope?: string; +} + class MCPOAuthProvider implements OAuthClientProvider { private connectionId: string; private serverName: string; @@ -85,7 +105,7 @@ class MCPOAuthProvider implements OAuthClientProvider { return stored ? JSON.parse(stored) : undefined; } - async saveClientInformation(clientInformation: OAuthClientInformationFull): Promise { + async saveClientInformation(clientInformation: OAuthClientInformation): Promise { console.log('Registered OAuth client for MCP server'); const serverKey = this.getServerKey(); localStorage.setItem(`mcp_oauth_client_${serverKey}`, JSON.stringify(clientInformation)); @@ -212,11 +232,12 @@ class MCPOAuthProvider implements OAuthClientProvider { } } -export class MCPConnectionManager implements MCPConnectionManager { +export class MCPConnectionManager { private connection: MCPConnection; private client?: Client; private transport?: Transport; private reconnectTimeout?: NodeJS.Timeout; + private healthCheckInterval?: NodeJS.Timeout; private oauthProvider?: MCPOAuthProvider; private onConnectionUpdate?: () => void; @@ -309,6 +330,9 @@ export class MCPConnectionManager implements MCPConnectionManager { this.connection.lastConnected = new Date(); this.connection.connectionAttempts = 0; + // Start health check monitoring + this.startHealthCheck(); + } catch (error) { this.connection.status = 'failed'; this.connection.error = error instanceof Error ? error.message : 'Unknown connection error'; @@ -339,6 +363,11 @@ export class MCPConnectionManager implements MCPConnectionManager { this.reconnectTimeout = undefined; } + if (this.healthCheckInterval) { + clearInterval(this.healthCheckInterval); + this.healthCheckInterval = undefined; + } + if (this.client) { try { await this.client.close(); @@ -441,30 +470,6 @@ export class MCPConnectionManager implements MCPConnectionManager { } } - private async handleOAuthAuthentication(): Promise { - if (!this.oauthProvider) { - throw new Error('OAuth provider not initialized'); - } - - console.log('Starting OAuth authentication flow...'); - - try { - const result = await auth(this.oauthProvider, { - serverUrl: this.connection.url, - scope: this.connection.config.oauthConfig?.scope, - }); - - if (result === 'REDIRECT') { - // OAuth flow was initiated via popup, no further action needed here - console.log('OAuth flow initiated via popup'); - } else if (result === 'AUTHORIZED') { - console.log('OAuth authentication successful'); - } - } catch (error) { - console.error('OAuth authentication failed:', error); - throw new Error(`OAuth authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`); - } - } private async tryStreamableHttp(): Promise { try { @@ -663,6 +668,60 @@ export class MCPConnectionManager implements MCPConnectionManager { return Math.min(1000 * Math.pow(2, this.connection.connectionAttempts - 1), 16000); } + private startHealthCheck(): void { + // Clear any existing health check + if (this.healthCheckInterval) { + clearInterval(this.healthCheckInterval); + } + + // Start health check every 30 seconds + this.healthCheckInterval = setInterval(async () => { + await this.performHealthCheck(); + }, 30000); + + console.log(`Started health check monitoring for ${this.connection.name}`); + } + + private async performHealthCheck(): Promise { + // Only check if we're supposed to be connected + if (this.connection.status !== 'connected' || !this.client) { + return; + } + + try { + // Try to list tools as a health check - this is a lightweight operation + await this.client.listTools(); + console.log(`Health check passed for ${this.connection.name}`); + } catch (error) { + console.warn(`Health check failed for ${this.connection.name}:`, error); + await this.handleHealthCheckFailure(error); + } + } + + private async handleHealthCheckFailure(error: any): Promise { + console.log(`Connection health check failed for ${this.connection.name}, attempting reconnection...`); + + // Stop health check during reconnection attempt + if (this.healthCheckInterval) { + clearInterval(this.healthCheckInterval); + this.healthCheckInterval = undefined; + } + + // Mark as disconnected and attempt reconnection + this.connection.status = 'connecting'; + this.connection.error = error instanceof Error ? error.message : 'Health check failed'; + this.notifyConnectionUpdate(); + + try { + // Attempt reconnection + await this.connect(); + console.log(`Health check reconnection successful for ${this.connection.name}`); + } catch (reconnectError) { + console.error(`Health check reconnection failed for ${this.connection.name}:`, reconnectError); + // The connect method will handle scheduling retry attempts + } + } + private createMCPError(type: MCPError['type'], message: string, details?: any): MCPError { return { type, diff --git a/src/providers/openrouter/client.ts b/src/providers/openrouter/client.ts index 6de1fda..f26fc0f 100644 --- a/src/providers/openrouter/client.ts +++ b/src/providers/openrouter/client.ts @@ -38,10 +38,10 @@ export class OpenRouterClient { ): Promise { const url = `${this.baseUrl}${endpoint}`; - const headers: HeadersInit = { + const headers: Record = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}`, - ...options.headers, + ...(options.headers as Record || {}), }; if (this.httpReferrer) { @@ -128,6 +128,7 @@ export class OpenRouterClient { // Check if model supports tools by looking at supported_parameters const supportsTools = openRouterModel.supported_parameters?.includes('tools') || false; + console.log(`Model ${openRouterModel.id} supports tools: ${supportsTools}`); return { id: openRouterModel.id, diff --git a/tsconfig.json b/tsconfig.json index 714eb81..416e717 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,6 @@ "@/*": ["./src/*"] } }, - "include": ["src", "**/*.ts", "**/*.tsx"], + "include": ["src/**/*"], "references": [{ "path": "./tsconfig.node.json" }] } \ No newline at end of file