Add tabbed sidebar with MCP server management and resizable interface

- Created tabbed interface with MCP, Conversations, and Inference tabs
- Set MCP tab as default tab
- Added quick connect button for example server with OAuth auth
- Created form to add custom MCP servers with auth type selection
- Implemented server management UI with connect/disconnect/remove actions
- Made sidebar resizable with drag handle (240px-600px range)
- Added comprehensive MCP server status display and tools listing
- Created dedicated Inference tab for OpenRouter connection management

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jerome 2025-06-12 15:28:20 +01:00
commit 1ab5496e0a
No known key found for this signature in database

264
src/components/MCPTab.tsx Normal file
View file

@ -0,0 +1,264 @@
// MCP tab with server management and connection functionality
import { useState } from 'react';
import { useMCP } from '@/contexts/MCPContext';
export function MCPTab() {
const { connections, addMcpServer, removeMcpServer, reconnectServer, isLoading } = useMCP();
const [showAddForm, setShowAddForm] = useState(false);
const [newServerName, setNewServerName] = useState('');
const [newServerUrl, setNewServerUrl] = useState('');
const [authType, setAuthType] = useState<'none' | 'oauth'>('none');
const handleAddExampleServer = async () => {
try {
await addMcpServer({
name: 'Example Server',
url: 'https://example-server.modelcontextprotocol.io/sse',
authType: 'oauth',
});
} catch (error) {
console.error('Failed to add example server:', error);
}
};
const handleAddCustomServer = async (e: React.FormEvent) => {
e.preventDefault();
if (!newServerName.trim() || !newServerUrl.trim()) return;
try {
await addMcpServer({
name: newServerName,
url: newServerUrl,
authType,
});
// Reset form
setNewServerName('');
setNewServerUrl('');
setAuthType('none');
setShowAddForm(false);
} catch (error) {
console.error('Failed to add custom server:', error);
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'connected':
return 'text-green-600 dark:text-green-400';
case 'connecting':
return 'text-yellow-600 dark:text-yellow-400';
case 'failed':
return 'text-red-600 dark:text-red-400';
default:
return 'text-gray-600 dark:text-gray-400';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'connected':
return '🟢';
case 'connecting':
return '🟡';
case 'failed':
return '🔴';
default:
return '⚪';
}
};
const connectedServers = connections.filter(conn => conn.status === 'connected');
const allTools = connections.flatMap(conn => conn.tools);
return (
<div className="flex-1 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>
</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 */}
<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"
>
{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 */}
{showAddForm && (
<form onSubmit={handleAddCustomServer} className="mt-3 space-y-3">
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
Server Name
</label>
<input
type="text"
value={newServerName}
onChange={(e) => setNewServerName(e.target.value)}
placeholder="My MCP Server"
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"
required
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
Server URL
</label>
<input
type="url"
value={newServerUrl}
onChange={(e) => setNewServerUrl(e.target.value)}
placeholder="https://your-server.com/sse"
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"
required
/>
</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
type="submit"
disabled={isLoading || !newServerName.trim() || !newServerUrl.trim()}
className="w-full px-4 py-2 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
{isLoading ? 'Adding...' : 'Add Server'}
</button>
</form>
)}
</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>
{connections.length === 0 ? (
<p className="text-sm text-gray-500 dark:text-gray-400">
No MCP servers configured. Add a server above to get started.
</p>
) : (
<div className="space-y-3">
{connections.map((connection) => (
<div
key={connection.id}
className="bg-white dark:bg-gray-700 rounded-lg p-3 border border-gray-200 dark:border-gray-600"
>
<div className="flex items-start justify-between mb-2">
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2">
<span className="text-sm">
{getStatusIcon(connection.status)}
</span>
<h4 className="text-sm font-medium text-gray-900 dark:text-white truncate">
{connection.name}
</h4>
</div>
<p className={`text-xs mt-1 ${getStatusColor(connection.status)}`}>
{connection.status}
</p>
{connection.error && (
<p className="text-xs text-red-600 dark:text-red-400 mt-1">
{connection.error}
</p>
)}
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate">
{connection.url}
</p>
</div>
<div className="flex space-x-1 ml-2">
{(connection.status === 'failed' || connection.status === 'disconnected') && (
<button
onClick={() => reconnectServer(connection.id)}
className="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200 rounded hover:bg-blue-200 dark:hover:bg-blue-800"
title="Reconnect server"
>
Reconnect
</button>
)}
<button
onClick={() => removeMcpServer(connection.id)}
className="px-2 py-1 text-xs 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"
title="Remove server"
>
Remove
</button>
</div>
</div>
{connection.tools.length > 0 && (
<div className="mt-2">
<p className="text-xs text-gray-600 dark:text-gray-400 mb-1">
Tools ({connection.tools.length}):
</p>
<div className="flex flex-wrap gap-1">
{connection.tools.slice(0, 3).map((tool, index) => (
<span
key={index}
className="inline-block px-2 py-1 bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200 text-xs rounded"
>
{tool.function.name.split('__').pop()}
</span>
))}
{connection.tools.length > 3 && (
<span className="text-xs text-gray-500 dark:text-gray-400">
+{connection.tools.length - 3} more
</span>
)}
</div>
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
);
}