Fix MCP message monitor layout and add collapsible sections

- Made MCP tab sections (Summary, Add Servers, Server List) collapsible to save space
- Expanded sidebar width limit from 600px to 80% of window width for better debugging
- Fixed message monitor overflow by establishing proper height constraint chain
- Removed auto-scroll and show/hide toggle for message monitor - always visible now
- Server list now has fixed height (max-h-60) with internal scroll
- Message monitor takes remaining space with proper internal scrolling
- Used h-full chain instead of flex-1 to establish explicit height constraints
- Message monitor now properly scrolls within its container instead of at app level

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jerome 2025-06-13 19:07:01 +01:00
commit 91608a0b97
No known key found for this signature in database
8 changed files with 409 additions and 101 deletions

View file

@ -21,7 +21,7 @@ export function ConversationApp() {
if (!isResizing.current) return;
const newWidth = e.clientX;
if (newWidth >= 240 && newWidth <= 600) {
if (newWidth >= 240 && newWidth <= window.innerWidth * 0.8) {
setSidebarWidth(newWidth);
}
}, []);
@ -39,7 +39,7 @@ export function ConversationApp() {
<div className="h-screen flex bg-gray-100 dark:bg-gray-900">
{/* Left Sidebar - Tabbed Interface */}
<div
className="bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 flex flex-col relative"
className="bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 flex flex-col relative h-full"
style={{ width: `${sidebarWidth}px` }}
>
<TabbedSidebar />

View file

@ -0,0 +1,161 @@
// Component to monitor and display MCP messages
import { useState, useEffect, useRef } from 'react';
import { useMCP } from '@/contexts/MCPContext';
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
interface MCPMessageEntry {
id: string;
timestamp: Date;
connectionId: string;
connectionName: string;
direction: 'sent' | 'received';
message: JSONRPCMessage;
extra?: any;
}
export function MCPMessageMonitor() {
const { addMessageCallback, removeMessageCallback, connections } = useMCP();
const [messages, setMessages] = useState<MCPMessageEntry[]>([]);
const [maxMessages, setMaxMessages] = useState(50);
const messagesEndRef = useRef<HTMLDivElement>(null);
// Remove auto-scroll - let user control scroll position
// Register message callback
useEffect(() => {
const callbackId = addMessageCallback((connectionId, _client, message, direction, extra) => {
const connection = connections.find(c => c.id === connectionId);
setMessages(prev => {
const newMessage: MCPMessageEntry = {
id: `${Date.now()}-${Math.random()}`,
timestamp: new Date(),
connectionId,
connectionName: connection?.name || 'Unknown',
direction,
message,
extra,
};
// Keep only the latest maxMessages
const newMessages = [...prev, newMessage];
return newMessages.slice(-maxMessages);
});
});
return () => {
removeMessageCallback(callbackId);
};
}, [addMessageCallback, removeMessageCallback, connections, maxMessages]);
const formatMessage = (msg: JSONRPCMessage) => {
if ('method' in msg) {
return `${msg.method}${'id' in msg ? ` (${msg.id})` : ''}`;
}
if ('id' in msg && 'result' in msg) {
return `Response (${msg.id})`;
}
if ('id' in msg && 'error' in msg) {
return `Error (${msg.id})`;
}
return 'Unknown';
};
const getMessageTypeColor = (msg: JSONRPCMessage, direction: string) => {
const baseClass = direction === 'sent'
? 'bg-blue-50 dark:bg-blue-900/30 border-blue-200 dark:border-blue-800'
: 'bg-green-50 dark:bg-green-900/30 border-green-200 dark:border-green-800';
if ('error' in msg) {
return 'bg-red-50 dark:bg-red-900/30 border-red-200 dark:border-red-800';
}
return baseClass;
};
const clearMessages = () => {
setMessages([]);
};
return (
<div className="flex-1 flex flex-col border-t border-gray-200 dark:border-gray-700 min-h-0">
<div className="flex items-center justify-between p-4 pb-2 flex-shrink-0">
<h3 className="font-medium text-gray-900 dark:text-white">
Message Monitor ({messages.length})
</h3>
<button
onClick={clearMessages}
className="text-xs px-2 py-1 bg-red-100 dark:bg-red-900/50 text-red-800 dark:text-red-200 rounded hover:bg-red-200 dark:hover:bg-red-800"
>
Clear
</button>
</div>
<div className="flex-1 mx-4 mb-2 overflow-y-auto border border-gray-200 dark:border-gray-600 rounded-lg">
{messages.length === 0 ? (
<div className="p-4 text-center text-gray-500 dark:text-gray-400 text-sm">
No messages yet. Messages will appear here as they're sent/received.
</div>
) : (
<div className="p-2 space-y-2">
{messages.map((entry) => (
<div
key={entry.id}
className={`p-2 rounded border text-xs ${getMessageTypeColor(entry.message, entry.direction)}`}
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center space-x-2">
<span className={entry.direction === 'sent' ? 'text-blue-600 dark:text-blue-400' : 'text-green-600 dark:text-green-400'}>
{entry.direction === 'sent' ? '→' : '←'}
</span>
<span className="font-medium text-gray-900 dark:text-white">
{formatMessage(entry.message)}
</span>
<span className="text-gray-500 dark:text-gray-400">
{entry.connectionName}
</span>
</div>
<span className="text-gray-500 dark:text-gray-400">
{entry.timestamp.toLocaleTimeString()}
</span>
</div>
<details className="text-gray-600 dark:text-gray-300">
<summary className="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100">
Details
</summary>
<pre className="mt-1 text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded overflow-x-auto">
{JSON.stringify(entry.message, null, 2)}
</pre>
</details>
</div>
))}
<div ref={messagesEndRef} />
</div>
)}
</div>
{/* Controls */}
<div className="p-4 pt-2 flex items-center justify-between text-xs border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
<div className="flex items-center space-x-2">
<label className="text-gray-600 dark:text-gray-400">
Max messages:
</label>
<select
value={maxMessages}
onChange={(e) => setMaxMessages(Number(e.target.value))}
className="px-2 py-1 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
<option value={25}>25</option>
<option value={50}>50</option>
<option value={100}>100</option>
<option value={200}>200</option>
</select>
</div>
<div className="text-gray-500 dark:text-gray-400">
Live monitoring active
</div>
</div>
</div>
);
}

View file

@ -2,6 +2,7 @@
import { useState } from 'react';
import { useMCP } from '@/contexts/MCPContext';
import { MCPMessageMonitor } from './MCPMessageMonitor';
export function MCPTab() {
const { connections, addMcpServer, removeMcpServer, reconnectServer, isLoading } = useMCP();
@ -9,6 +10,11 @@ export function MCPTab() {
const [newServerName, setNewServerName] = useState('');
const [newServerUrl, setNewServerUrl] = useState('');
const [authType, setAuthType] = useState<'none' | 'oauth'>('none');
// Collapsible section states
const [showSummary, setShowSummary] = useState(true);
const [showAddServers, setShowAddServers] = useState(true);
const [showServerList, setShowServerList] = useState(true);
const handleAddExampleServer = async () => {
try {
@ -73,50 +79,75 @@ export function MCPTab() {
const allTools = connections.flatMap(conn => conn.tools);
return (
<div className="flex-1 flex flex-col overflow-hidden">
<div className="h-full flex flex-col overflow-hidden">
{/* Summary */}
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
<h3 className="font-medium text-gray-900 dark:text-white mb-2">
MCP Summary
</h3>
<div className="text-sm space-y-1">
<p className="text-gray-700 dark:text-gray-300">
<span className="font-medium">{connectedServers.length}</span> servers connected
</p>
<p className="text-gray-700 dark:text-gray-300">
<span className="font-medium">{allTools.length}</span> tools available
</p>
<div className="border-b border-gray-200 dark:border-gray-700">
<button
onClick={() => setShowSummary(!showSummary)}
className="w-full p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
>
<div className="flex items-center justify-between">
<h3 className="font-medium text-gray-900 dark:text-white">
MCP Summary
</h3>
<span className="text-gray-500 dark:text-gray-400">
{showSummary ? '−' : '+'}
</span>
</div>
</div>
</button>
{showSummary && (
<div className="px-4 pb-4">
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
<div className="text-sm space-y-1">
<p className="text-gray-700 dark:text-gray-300">
<span className="font-medium">{connectedServers.length}</span> servers connected
</p>
<p className="text-gray-700 dark:text-gray-300">
<span className="font-medium">{allTools.length}</span> tools available
</p>
</div>
</div>
</div>
)}
</div>
{/* Add Servers Section */}
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="font-medium text-gray-900 dark:text-white mb-3">
Add Servers
</h3>
{/* Example Server Button */}
<div className="border-b border-gray-200 dark:border-gray-700">
<button
onClick={handleAddExampleServer}
disabled={isLoading || connections.some(conn => conn.name === 'Example Server')}
className="w-full mb-3 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
onClick={() => setShowAddServers(!showAddServers)}
className="w-full p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
>
{connections.some(conn => conn.name === 'Example Server') ? 'Example Server Added' : 'Add Example Server'}
<div className="flex items-center justify-between">
<h3 className="font-medium text-gray-900 dark:text-white">
Add Servers
</h3>
<span className="text-gray-500 dark:text-gray-400">
{showAddServers ? '−' : '+'}
</span>
</div>
</button>
{showAddServers && (
<div className="px-4 pb-4 space-y-3">
{/* Example Server Button */}
<button
onClick={handleAddExampleServer}
disabled={isLoading || connections.some(conn => conn.name === 'Example Server')}
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
{connections.some(conn => conn.name === 'Example Server') ? 'Example Server Added' : 'Add Example Server'}
</button>
{/* Custom Server Form Toggle */}
<button
onClick={() => setShowAddForm(!showAddForm)}
className="w-full px-4 py-2 text-sm bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
{showAddForm ? 'Cancel' : 'Add Custom Server'}
</button>
{/* Custom Server Form Toggle */}
<button
onClick={() => setShowAddForm(!showAddForm)}
className="w-full px-4 py-2 text-sm bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
{showAddForm ? 'Cancel' : 'Add Custom Server'}
</button>
{/* Custom Server Form */}
{showAddForm && (
<form onSubmit={handleAddCustomServer} className="mt-3 space-y-3">
{/* Custom Server Form */}
{showAddForm && (
<form onSubmit={handleAddCustomServer} className="space-y-3">
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
Server Name
@ -168,15 +199,27 @@ export function MCPTab() {
</button>
</form>
)}
</div>
)}
</div>
{/* Server List */}
<div className="flex-1 overflow-y-auto p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="font-medium text-gray-900 dark:text-white">
Servers ({connections.length})
</h3>
</div>
<div className="flex-shrink-0">
<button
onClick={() => setShowServerList(!showServerList)}
className="w-full p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors border-b border-gray-200 dark:border-gray-700"
>
<div className="flex items-center justify-between">
<h3 className="font-medium text-gray-900 dark:text-white">
Servers ({connections.length})
</h3>
<span className="text-gray-500 dark:text-gray-400">
{showServerList ? '−' : '+'}
</span>
</div>
</button>
{showServerList && (
<div className="max-h-60 overflow-y-auto p-4">
{connections.length === 0 ? (
<p className="text-sm text-gray-500 dark:text-gray-400">
@ -258,7 +301,12 @@ export function MCPTab() {
))}
</div>
)}
</div>
)}
</div>
{/* Message Monitor */}
<MCPMessageMonitor />
</div>
);
}

View file

@ -17,9 +17,9 @@ export function TabbedSidebar() {
];
return (
<div className="flex-1 flex flex-col">
<div className="h-full flex flex-col">
{/* Tab Headers */}
<div className="border-b border-gray-200 dark:border-gray-700">
<div className="flex-shrink-0 border-b border-gray-200 dark:border-gray-700">
<div className="flex">
{tabs.map((tab) => (
<button
@ -39,7 +39,7 @@ export function TabbedSidebar() {
</div>
{/* Tab Content */}
<div className="flex-1 overflow-hidden">
<div className="flex-1 overflow-hidden min-h-0">
{activeTab === 'mcp' && <MCPTab />}
{activeTab === 'conversations' && <ConversationSidebar />}
{activeTab === 'inference' && <InferenceTab />}

View file

@ -8,6 +8,7 @@ import type {
MCPServerConfig,
MCPResource,
MCPContextValue,
MCPMessageCallback,
} from '@/types/mcp';
import type { Tool } from '@/types/inference';
import { MCPConnectionManager } from '@/mcp/connection';
@ -24,6 +25,33 @@ export function MCPProvider({ children }: MCPProviderProps) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const hasLoadedPersisted = useRef(false);
// Message callback management
const [messageCallbacks, setMessageCallbacks] = useState<Map<string, MCPMessageCallback>>(new Map());
const messageCallbacksRef = useRef<Map<string, MCPMessageCallback>>(new Map());
// Keep ref in sync with state
useEffect(() => {
messageCallbacksRef.current = messageCallbacks;
}, [messageCallbacks]);
// Function to broadcast messages to all callbacks (stable reference)
const broadcastMessage = useCallback((
connectionId: string,
client: any,
message: any,
direction: 'sent' | 'received',
extra?: any
) => {
console.log(`MCP Message [${direction}] from ${connectionId}:`, message); // Debug log
messageCallbacksRef.current.forEach(callback => {
try {
callback(connectionId, client, message, direction, extra);
} catch (error) {
console.error('Error in MCP message callback:', error);
}
});
}, []); // No dependencies - uses ref
// Load persisted connections from localStorage on mount
useEffect(() => {
@ -61,6 +89,9 @@ export function MCPProvider({ children }: MCPProviderProps) {
);
});
// Set up message callback
manager.setMessageCallback(broadcastMessage);
// Add to managers map
setManagers(prev => new Map(prev).set(connectionId, manager));
@ -127,6 +158,9 @@ export function MCPProvider({ children }: MCPProviderProps) {
);
});
// Set up message callback
manager.setMessageCallback(broadcastMessage);
// Add to managers map
setManagers(prev => new Map(prev).set(connectionId, manager));
@ -336,6 +370,21 @@ export function MCPProvider({ children }: MCPProviderProps) {
}
}, [managers]);
// Message callback management
const addMessageCallback = useCallback((callback: MCPMessageCallback): string => {
const callbackId = uuidv4();
setMessageCallbacks(prev => new Map(prev).set(callbackId, callback));
return callbackId;
}, []);
const removeMessageCallback = useCallback((callbackId: string) => {
setMessageCallbacks(prev => {
const newMap = new Map(prev);
newMap.delete(callbackId);
return newMap;
});
}, []);
const contextValue: MCPContextValue = {
connections,
isLoading,
@ -353,6 +402,8 @@ export function MCPProvider({ children }: MCPProviderProps) {
getConnectionById,
updateServerConfig,
handleOAuthCallback,
addMessageCallback,
removeMessageCallback,
};
return (

View file

@ -18,6 +18,7 @@ import type {
} from '@/types/mcp';
import type { Tool } from '@/types/inference';
import { normalizeServerName } from '@/utils/mcpUtils';
import { DebugTransport } from './debugTransport';
interface MCPOAuthState {
codeVerifier: string;
@ -107,7 +108,6 @@ class MCPOAuthProvider implements OAuthClientProvider {
}
async saveClientInformation(clientInformation: OAuthClientInformation): Promise<void> {
console.log('Registered OAuth client for MCP server');
const serverKey = this.getServerKey();
localStorage.setItem(`mcp_oauth_client_${serverKey}`, JSON.stringify(clientInformation));
}
@ -118,12 +118,10 @@ class MCPOAuthProvider implements OAuthClientProvider {
}
async saveTokens(tokens: OAuthTokens): Promise<void> {
console.log('OAuth tokens saved successfully');
localStorage.setItem(`mcp_oauth_tokens_${this.connectionId}`, JSON.stringify(tokens));
}
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
console.log('🔐 Starting OAuth flow in popup window...');
// Open popup for OAuth flow
const popup = window.open(
@ -149,13 +147,10 @@ class MCPOAuthProvider implements OAuthClientProvider {
popup.close();
if (event.data.error) {
console.error('❌ OAuth authorization failed:', event.data.error);
this.authError = event.data.error;
} else if (event.data.code) {
console.log('✅ OAuth authorization successful, exchanging code for tokens...');
this.processAuthorizationCode(event.data.code);
} else {
console.error('❌ OAuth callback missing authorization code');
this.authError = 'No authorization code received';
}
};
@ -206,7 +201,6 @@ class MCPOAuthProvider implements OAuthClientProvider {
});
if (result === 'AUTHORIZED') {
console.log('🎉 OAuth authentication completed! Connecting to MCP server...');
this.notifyOAuthComplete();
} else {
console.error('❌ OAuth token exchange failed');
@ -241,6 +235,7 @@ export class MCPConnectionManager {
private healthCheckInterval?: NodeJS.Timeout;
private oauthProvider?: MCPOAuthProvider;
private onConnectionUpdate?: () => void;
private onMessage?: (connectionId: string, client: any, message: any, direction: 'sent' | 'received', extra?: any) => void;
constructor(id: string, config: MCPServerConfig) {
this.connection = {
@ -275,6 +270,12 @@ export class MCPConnectionManager {
this.onConnectionUpdate = callback;
}
// Set callback for message monitoring
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;
}
// Notify about connection state changes
private notifyConnectionUpdate(): void {
if (this.onConnectionUpdate) {
@ -287,19 +288,6 @@ export class MCPConnectionManager {
this.connection.error = undefined;
try {
// Validate URL format
console.log('Validating URL:', this.connection.url);
try {
const url = new URL(this.connection.url);
console.log('URL parsed successfully:', {
protocol: url.protocol,
hostname: url.hostname,
pathname: url.pathname,
port: url.port
});
} catch (urlError) {
throw new Error(`Invalid URL format: ${this.connection.url}`);
}
// Clear any existing connections
await this.disconnect();
@ -343,7 +331,6 @@ export class MCPConnectionManager {
if (this.connection.config.autoReconnect !== false &&
this.connection.connectionAttempts < (this.connection.config.maxReconnectAttempts || 5)) {
const delay = this.getBackoffDelay();
console.log(`Scheduling reconnect attempt ${this.connection.connectionAttempts} in ${delay}ms`);
this.reconnectTimeout = setTimeout(async () => {
try {
await this.reconnect();
@ -397,14 +384,11 @@ export class MCPConnectionManager {
throw new Error('OAuth provider not initialized');
}
console.log('Processing OAuth callback with authorization code...');
try {
// If we have an active transport, use its finishAuth method
if (this.transport && typeof (this.transport as any).finishAuth === 'function') {
console.log('Calling transport.finishAuth...');
await (this.transport as any).finishAuth(authorizationCode);
console.log('OAuth authorization completed via transport');
// Now attempt to connect - the transport should be authenticated
await this.connect();
@ -412,11 +396,9 @@ export class MCPConnectionManager {
// If no transport yet, store the authorization code and try connecting
// The transport creation will handle the auth flow
this.oauthProvider.pendingAuthorizationCode = authorizationCode;
console.log('Stored authorization code, attempting connection...');
await this.connect();
}
console.log('OAuth callback processed successfully');
} catch (error) {
console.error('OAuth callback processing failed:', error);
throw new Error(`OAuth callback failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
@ -450,7 +432,6 @@ export class MCPConnectionManager {
// Handle successful OAuth completion
private async handleOAuthSuccess(): Promise<void> {
console.log('OAuth completed successfully, retrying connection...');
try {
// Reset connection state and retry
@ -461,7 +442,6 @@ export class MCPConnectionManager {
// Attempt to connect now that we have valid tokens
await this.connect();
console.log('Post-OAuth connection successful!');
this.notifyConnectionUpdate();
} catch (error) {
console.error('Post-OAuth connection failed:', error);
@ -474,7 +454,6 @@ export class MCPConnectionManager {
private async tryStreamableHttp(): Promise<void> {
try {
console.log('Attempting StreamableHTTP connection to:', this.connection.url);
// Create transport options with OAuth provider if configured
const transportOptions: any = {};
@ -485,7 +464,6 @@ export class MCPConnectionManager {
const transport = new StreamableHTTPClientTransport(new URL(this.connection.url), transportOptions);
await this.initializeClient(transport);
console.log('StreamableHTTP connection successful');
} catch (error) {
console.error('StreamableHTTP connection failed:', error);
throw error;
@ -494,7 +472,6 @@ export class MCPConnectionManager {
private async trySSE(): Promise<void> {
try {
console.log('Attempting SSE connection to:', this.connection.url);
// Create transport options with OAuth provider if configured
const transportOptions: any = {};
@ -504,25 +481,17 @@ export class MCPConnectionManager {
}
const transport = new SSEClientTransport(new URL(this.connection.url), transportOptions);
console.log('SSE transport created, attempting client connection...');
await this.initializeClient(transport);
console.log('SSE connection successful');
} catch (error) {
console.error('SSE connection failed:', error);
console.error('Error details:', {
message: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined,
url: this.connection.url
});
throw error;
}
}
private async initializeClient(transport: Transport): Promise<void> {
try {
console.log('Initializing MCP client...');
this.transport = transport;
this.transport.onmessage = console.log.bind(console, 'MCP Client message received:');
const debugTransport = new DebugTransport(transport);
this.transport = debugTransport;
this.client = new Client(
{
name: 'example-remote-client',
@ -532,19 +501,23 @@ export class MCPConnectionManager {
capabilities: {},
}
);
console.log('Connecting client to transport...');
console.log('Transport type:', transport.constructor.name);
console.log('Transport details:', transport);
await this.client.connect(transport);
// Set up message callbacks to broadcast to UI after client is created
debugTransport.onsendmessage_ = async (message, options) => {
if (this.onMessage && this.client) {
this.onMessage(this.connection.id, this.client, message, 'sent', { options });
}
};
debugTransport.onreceivemessage_ = (message, extra) => {
if (this.onMessage && this.client) {
this.onMessage(this.connection.id, this.client, message, 'received', extra);
}
};
await this.client.connect(debugTransport);
this.connection.client = this.client;
console.log('Client connected successfully');
} catch (error) {
console.error('Client initialization failed:', error);
console.error('Error type:', error?.constructor?.name);
console.error('Error message:', error instanceof Error ? error.message : error);
console.error('Full error object:', error);
throw error;
}
}
@ -683,7 +656,6 @@ export class MCPConnectionManager {
await this.performHealthCheck();
}, 30000);
console.log(`Started health check monitoring for ${this.connection.name}`);
}
private async performHealthCheck(): Promise<void> {
@ -695,7 +667,6 @@ export class MCPConnectionManager {
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);
@ -703,7 +674,6 @@ export class MCPConnectionManager {
}
private async handleHealthCheckFailure(error: any): Promise<void> {
console.log(`Connection health check failed for ${this.connection.name}, attempting reconnection...`);
// Stop health check during reconnection attempt
if (this.healthCheckInterval) {
@ -719,7 +689,6 @@ export class MCPConnectionManager {
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

63
src/mcp/debugTransport.ts Normal file
View file

@ -0,0 +1,63 @@
import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
import type { Transport, TransportSendOptions } from "@modelcontextprotocol/sdk/shared/transport.d.ts";
import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
/**
* Debug transport for MCP that logs messages to the console.
* This is useful for debugging purposes and does not implement any actual transport logic.
*/
export class DebugTransport implements Transport {
// Allow observability of the transport lifecycle
onclose_?: () => void;
onerror_?: (error: Error) => void;
onreceivemessage_?: (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo }) => void;
onsendmessage_?: (message: JSONRPCMessage, options?: TransportSendOptions) => Promise<void>;
private innerTransport: Transport;
constructor(innerTransport: Transport) {
this.innerTransport = innerTransport;
this.innerTransport.onclose = () => {
if (this.onclose_) {
this.onclose_();
}
if (this.onclose) {
this.onclose();
}
};
this.innerTransport.onerror = (error: Error) => {
if (this.onerror_) {
this.onerror_(error);
}
if (this.onerror) {
this.onerror(error);
}
}
this.innerTransport.onmessage = (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo }) => {
if (this.onreceivemessage_) {
this.onreceivemessage_(message, extra);
}
if (this.onmessage) {
this.onmessage(message, extra);
}
}
}
async start(): Promise<void> {
await this.innerTransport.start();
}
async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise<void> {
if (this.onsendmessage_) {
await this.onsendmessage_(message, options);
}
await this.innerTransport.send(message, options);
}
async close(): Promise<void> {
await this.innerTransport.close();
}
// These are taken over by the MCP client
onclose?: () => void;
onerror?: (error: Error) => void;
onmessage?: (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo }) => void;
}

View file

@ -2,6 +2,9 @@
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
import type { Tool } from './inference';
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
import type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
import type { TransportSendOptions } from '@modelcontextprotocol/sdk/shared/transport.d.ts';
export interface MCPServerConfig {
name: string; // User-provided display name
@ -29,6 +32,15 @@ export interface MCPMessage {
error?: any;
}
// Message callback types
export type MCPMessageCallback = (
connectionId: string,
client: Client,
message: JSONRPCMessage,
direction: 'sent' | 'received',
extra?: { authInfo?: AuthInfo; options?: TransportSendOptions }
) => void;
export interface MCPResource {
uri: string;
name: string;
@ -129,4 +141,8 @@ export interface MCPContextValue {
// OAuth handling
handleOAuthCallback: (connectionId: string, authorizationCode: string) => Promise<void>;
// Message monitoring
addMessageCallback: (callback: MCPMessageCallback) => string; // Returns callback ID
removeMessageCallback: (callbackId: string) => void;
}