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:
parent
1ab5496e0a
commit
91608a0b97
8 changed files with 409 additions and 101 deletions
|
|
@ -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 />
|
||||
|
|
|
|||
161
src/components/MCPMessageMonitor.tsx
Normal file
161
src/components/MCPMessageMonitor.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 />}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
63
src/mcp/debugTransport.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in a new issue