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