Simplify MCP connection handling and remove health checks
- Remove auth type selection from UI - servers will auto-detect OAuth when needed - Remove auto-reconnect logic and health checks to reduce complexity - Always initialize OAuth provider for all connections - Simplify transport selection - always try streamable-http first, then SSE - Remove unused config options (transport preference, auto-reconnect) - Add TODO comment about CORS issues for failed fetch errors - Make authType and transport optional in connection types - Clean up connection state notifications for better UI updates 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
7872b5fdb7
commit
728fea3665
3 changed files with 65 additions and 193 deletions
|
|
@ -9,7 +9,6 @@ export function MCPTab() {
|
||||||
const [showAddForm, setShowAddForm] = useState(false);
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
const [newServerName, setNewServerName] = useState('');
|
const [newServerName, setNewServerName] = useState('');
|
||||||
const [newServerUrl, setNewServerUrl] = useState('');
|
const [newServerUrl, setNewServerUrl] = useState('');
|
||||||
const [authType, setAuthType] = useState<'none' | 'oauth'>('none');
|
|
||||||
|
|
||||||
// Collapsible section states
|
// Collapsible section states
|
||||||
const [showSummary, setShowSummary] = useState(true);
|
const [showSummary, setShowSummary] = useState(true);
|
||||||
|
|
@ -21,7 +20,6 @@ export function MCPTab() {
|
||||||
await addMcpServer({
|
await addMcpServer({
|
||||||
name: 'Example Server',
|
name: 'Example Server',
|
||||||
url: 'https://example-server.modelcontextprotocol.io/sse',
|
url: 'https://example-server.modelcontextprotocol.io/sse',
|
||||||
authType: 'oauth',
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to add example server:', error);
|
console.error('Failed to add example server:', error);
|
||||||
|
|
@ -36,13 +34,11 @@ export function MCPTab() {
|
||||||
await addMcpServer({
|
await addMcpServer({
|
||||||
name: newServerName,
|
name: newServerName,
|
||||||
url: newServerUrl,
|
url: newServerUrl,
|
||||||
authType,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset form
|
// Reset form
|
||||||
setNewServerName('');
|
setNewServerName('');
|
||||||
setNewServerUrl('');
|
setNewServerUrl('');
|
||||||
setAuthType('none');
|
|
||||||
setShowAddForm(false);
|
setShowAddForm(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to add custom server:', error);
|
console.error('Failed to add custom server:', error);
|
||||||
|
|
@ -175,21 +171,6 @@ export function MCPTab() {
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Authentication
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={authType}
|
|
||||||
onChange={(e) => setAuthType(e.target.value as 'none' | 'oauth')}
|
|
||||||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
>
|
|
||||||
<option value="none">None</option>
|
|
||||||
<option value="oauth">OAuth</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isLoading || !newServerName.trim() || !newServerUrl.trim()}
|
disabled={isLoading || !newServerName.trim() || !newServerUrl.trim()}
|
||||||
|
|
|
||||||
|
|
@ -230,7 +230,7 @@ class MCPOAuthProvider implements OAuthClientProvider {
|
||||||
export class MCPConnectionManager {
|
export class MCPConnectionManager {
|
||||||
private connection: MCPConnection;
|
private connection: MCPConnection;
|
||||||
private client?: Client;
|
private client?: Client;
|
||||||
private transport?: Transport;
|
private transport?: Transport | StreamableHTTPClientTransport | SSEClientTransport;
|
||||||
private reconnectTimeout?: NodeJS.Timeout;
|
private reconnectTimeout?: NodeJS.Timeout;
|
||||||
private healthCheckInterval?: NodeJS.Timeout;
|
private healthCheckInterval?: NodeJS.Timeout;
|
||||||
private oauthProvider?: MCPOAuthProvider;
|
private oauthProvider?: MCPOAuthProvider;
|
||||||
|
|
@ -243,22 +243,16 @@ export class MCPConnectionManager {
|
||||||
name: config.name,
|
name: config.name,
|
||||||
url: config.url,
|
url: config.url,
|
||||||
status: 'disconnected',
|
status: 'disconnected',
|
||||||
transport: 'streamable-http', // Will be determined during connection
|
|
||||||
authType: config.authType || 'none',
|
|
||||||
tools: [],
|
tools: [],
|
||||||
resources: [],
|
resources: [],
|
||||||
prompts: [],
|
prompts: [],
|
||||||
connectionAttempts: 0,
|
connectionAttempts: 0,
|
||||||
config,
|
config,
|
||||||
};
|
};
|
||||||
|
this.oauthProvider = new MCPOAuthProvider(id, config.name, config.url, () => {
|
||||||
// Initialize OAuth provider if auth is required
|
// Callback when OAuth completes successfully
|
||||||
if (this.connection.authType === 'oauth') {
|
this.handleOAuthSuccess();
|
||||||
this.oauthProvider = new MCPOAuthProvider(id, config.name, config.url, () => {
|
});
|
||||||
// Callback when OAuth completes successfully
|
|
||||||
this.handleOAuthSuccess();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getConnection(): MCPConnection {
|
getConnection(): MCPConnection {
|
||||||
|
|
@ -272,7 +266,6 @@ export class MCPConnectionManager {
|
||||||
|
|
||||||
// Set callback for message monitoring
|
// Set callback for message monitoring
|
||||||
setMessageCallback(callback: (connectionId: string, client: any, message: any, direction: 'sent' | 'received', extra?: any) => void): void {
|
setMessageCallback(callback: (connectionId: string, client: any, message: any, direction: 'sent' | 'received', extra?: any) => void): void {
|
||||||
console.log(`Setting message callback for connection ${this.connection.id}`); // Debug log
|
|
||||||
this.onMessage = callback;
|
this.onMessage = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -286,28 +279,19 @@ export class MCPConnectionManager {
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
this.connection.status = 'connecting';
|
this.connection.status = 'connecting';
|
||||||
this.connection.error = undefined;
|
this.connection.error = undefined;
|
||||||
|
this.notifyConnectionUpdate();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// Clear any existing connections
|
// Clear any existing connections
|
||||||
await this.disconnect();
|
await this.disconnect();
|
||||||
|
|
||||||
// Determine transport strategy
|
try {
|
||||||
const transportPreference = this.connection.config.transport || 'auto';
|
await this.tryStreamableHttp();
|
||||||
|
this.connection.transport = 'streamable-http';
|
||||||
if (transportPreference === 'auto' || transportPreference === 'streamable-http') {
|
} catch (error) {
|
||||||
try {
|
// TODO: jerome - if this is a TypeError: failed to fetch, then there is likely a CORS (or
|
||||||
await this.tryStreamableHttp();
|
// Access-Control-Expose-Headers) issue with the server.
|
||||||
this.connection.transport = 'streamable-http';
|
|
||||||
} catch (error) {
|
|
||||||
if (transportPreference === 'streamable-http') {
|
|
||||||
throw error; // Don't fallback if explicitly requested
|
|
||||||
}
|
|
||||||
// Try SSE fallback
|
|
||||||
await this.trySSE();
|
|
||||||
this.connection.transport = 'sse';
|
|
||||||
}
|
|
||||||
} else if (transportPreference === 'sse') {
|
|
||||||
await this.trySSE();
|
await this.trySSE();
|
||||||
this.connection.transport = 'sse';
|
this.connection.transport = 'sse';
|
||||||
}
|
}
|
||||||
|
|
@ -318,28 +302,13 @@ export class MCPConnectionManager {
|
||||||
this.connection.status = 'connected';
|
this.connection.status = 'connected';
|
||||||
this.connection.lastConnected = new Date();
|
this.connection.lastConnected = new Date();
|
||||||
this.connection.connectionAttempts = 0;
|
this.connection.connectionAttempts = 0;
|
||||||
|
this.notifyConnectionUpdate();
|
||||||
// Start health check monitoring
|
|
||||||
this.startHealthCheck();
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.connection.status = 'failed';
|
this.connection.status = 'failed';
|
||||||
this.connection.error = error instanceof Error ? error.message : 'Unknown connection error';
|
this.connection.error = error instanceof Error ? error.message : 'Unknown connection error';
|
||||||
this.connection.connectionAttempts++;
|
this.connection.connectionAttempts++;
|
||||||
|
this.notifyConnectionUpdate();
|
||||||
// Schedule auto-reconnect if enabled
|
|
||||||
if (this.connection.config.autoReconnect !== false &&
|
|
||||||
this.connection.connectionAttempts < (this.connection.config.maxReconnectAttempts || 5)) {
|
|
||||||
const delay = this.getBackoffDelay();
|
|
||||||
this.reconnectTimeout = setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
await this.reconnect();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Auto-reconnect failed:', error);
|
|
||||||
// Don't throw here to prevent uncaught promise rejection
|
|
||||||
}
|
|
||||||
}, delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw this.createMCPError('connection', this.connection.error, error);
|
throw this.createMCPError('connection', this.connection.error, error);
|
||||||
}
|
}
|
||||||
|
|
@ -434,55 +403,34 @@ export class MCPConnectionManager {
|
||||||
private async handleOAuthSuccess(): Promise<void> {
|
private async handleOAuthSuccess(): Promise<void> {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Reset connection state and retry
|
|
||||||
this.connection.status = 'connecting';
|
|
||||||
this.connection.error = undefined;
|
|
||||||
this.notifyConnectionUpdate();
|
|
||||||
|
|
||||||
// Attempt to connect now that we have valid tokens
|
// Attempt to connect now that we have valid tokens
|
||||||
await this.connect();
|
await this.connect();
|
||||||
|
|
||||||
this.notifyConnectionUpdate();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Post-OAuth connection failed:', error);
|
console.error('Post-OAuth connection failed:', error);
|
||||||
this.connection.status = 'failed';
|
|
||||||
this.connection.error = error instanceof Error ? error.message : 'Post-OAuth connection failed';
|
|
||||||
this.notifyConnectionUpdate();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async tryStreamableHttp(): Promise<void> {
|
private async tryStreamableHttp(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
const transport = new StreamableHTTPClientTransport(new URL(this.connection.url), {
|
||||||
// Create transport options with OAuth provider if configured
|
authProvider: this.oauthProvider
|
||||||
const transportOptions: any = {};
|
});
|
||||||
|
|
||||||
if (this.connection.authType === 'oauth' && this.oauthProvider) {
|
|
||||||
transportOptions.authProvider = this.oauthProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
const transport = new StreamableHTTPClientTransport(new URL(this.connection.url), transportOptions);
|
|
||||||
await this.initializeClient(transport);
|
await this.initializeClient(transport);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('StreamableHTTP connection failed:', error);
|
console.log('StreamableHTTP connection failed:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async trySSE(): Promise<void> {
|
private async trySSE(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
const transport = new SSEClientTransport(new URL(this.connection.url), {
|
||||||
// Create transport options with OAuth provider if configured
|
authProvider: this.oauthProvider
|
||||||
const transportOptions: any = {};
|
});
|
||||||
|
|
||||||
if (this.connection.authType === 'oauth' && this.oauthProvider) {
|
|
||||||
transportOptions.authProvider = this.oauthProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
const transport = new SSEClientTransport(new URL(this.connection.url), transportOptions);
|
|
||||||
await this.initializeClient(transport);
|
await this.initializeClient(transport);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log('SSE connection failed:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -640,61 +588,6 @@ export class MCPConnectionManager {
|
||||||
return this.connection.status;
|
return this.connection.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getBackoffDelay(): number {
|
|
||||||
// Exponential backoff: 1s, 2s, 4s, 8s, 16s
|
|
||||||
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();
|
|
||||||
}, 10 * 60 * 1000); // 10 minutes
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private async performHealthCheck(): Promise<void> {
|
|
||||||
// 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.ping();
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Health check failed for ${this.connection.name}:`, error);
|
|
||||||
await this.handleHealthCheckFailure(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleHealthCheckFailure(error: any): Promise<void> {
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
} 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 {
|
private createMCPError(type: MCPError['type'], message: string, details?: any): MCPError {
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import type { TransportSendOptions } from '@modelcontextprotocol/sdk/shared/tran
|
||||||
export interface MCPServerConfig {
|
export interface MCPServerConfig {
|
||||||
name: string; // User-provided display name
|
name: string; // User-provided display name
|
||||||
url: string; // Server endpoint URL
|
url: string; // Server endpoint URL
|
||||||
transport?: 'sse' | 'streamable-http' | 'auto'; // Default: auto-detect
|
|
||||||
authType?: 'none' | 'oauth'; // Default: none
|
authType?: 'none' | 'oauth'; // Default: none
|
||||||
oauthConfig?: {
|
oauthConfig?: {
|
||||||
clientId?: string;
|
clientId?: string;
|
||||||
|
|
@ -18,7 +17,6 @@ export interface MCPServerConfig {
|
||||||
scope?: string; // OAuth scope
|
scope?: string; // OAuth scope
|
||||||
redirectUri?: string; // Override default redirect URI
|
redirectUri?: string; // Override default redirect URI
|
||||||
};
|
};
|
||||||
autoReconnect?: boolean; // Default: true
|
|
||||||
maxReconnectAttempts?: number; // Default: 5
|
maxReconnectAttempts?: number; // Default: 5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,8 +73,8 @@ export interface MCPConnection {
|
||||||
url: string; // Server URL
|
url: string; // Server URL
|
||||||
status: 'connecting' | 'connected' | 'failed' | 'disconnected';
|
status: 'connecting' | 'connected' | 'failed' | 'disconnected';
|
||||||
client?: Client; // MCP SDK client instance
|
client?: Client; // MCP SDK client instance
|
||||||
transport: 'sse' | 'streamable-http';
|
transport?: 'sse' | 'streamable-http';
|
||||||
authType: 'none' | 'oauth';
|
authType?: 'none' | 'oauth';
|
||||||
|
|
||||||
// Available capabilities
|
// Available capabilities
|
||||||
tools: Tool[]; // Tools with name-prefixed identifiers
|
tools: Tool[]; // Tools with name-prefixed identifiers
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue