Archon/archon-ui-main/src/pages/MCPPage.tsx

770 lines
30 KiB
TypeScript

import { useState, useEffect, useRef } from 'react';
import { Play, Square, Copy, Clock, Server, AlertCircle, CheckCircle, Loader } from 'lucide-react';
import { motion } from 'framer-motion';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { useStaggeredEntrance } from '../hooks/useStaggeredEntrance';
import { useToast } from '../contexts/ToastContext';
import { mcpServerService, ServerStatus, LogEntry, ServerConfig } from '../services/mcpServerService';
import { IDEGlobalRules } from '../components/settings/IDEGlobalRules';
// import { MCPClients } from '../components/mcp/MCPClients'; // Commented out - feature not implemented
// Supported IDE/Agent types
type SupportedIDE = 'windsurf' | 'cursor' | 'claudecode' | 'cline' | 'kiro' | 'augment' | 'gemini';
/**
* MCP Dashboard Page Component
*
* This is the main dashboard for managing the MCP (Model Context Protocol) server.
* It provides a comprehensive interface for:
*
* 1. Server Control Tab:
* - Start/stop the MCP server
* - Monitor server status and uptime
* - View and copy connection configuration
* - Real-time log streaming via WebSocket
* - Historical log viewing and clearing
*
* 2. MCP Clients Tab:
* - Interactive client management interface
* - Tool discovery and testing
* - Real-time tool execution
* - Parameter input and result visualization
*
* The page uses a tab-based layout with preserved server functionality
* and enhanced client management capabilities.
*
* @component
*/
export const MCPPage = () => {
const [serverStatus, setServerStatus] = useState<ServerStatus>({
status: 'stopped',
uptime: null,
logs: []
});
const [config, setConfig] = useState<ServerConfig | null>(null);
const [logs, setLogs] = useState<LogEntry[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isStarting, setIsStarting] = useState(false);
const [isStopping, setIsStopping] = useState(false);
const [selectedIDE, setSelectedIDE] = useState<SupportedIDE>('windsurf');
const logsEndRef = useRef<HTMLDivElement>(null);
const logsContainerRef = useRef<HTMLDivElement>(null);
const statusPollInterval = useRef<NodeJS.Timeout | null>(null);
const { showToast } = useToast();
// Tab state for switching between Server Control and Clients
const [activeTab, setActiveTab] = useState<'server' | 'clients'>('server');
// Use staggered entrance animation
const { isVisible, containerVariants, itemVariants, titleVariants } = useStaggeredEntrance(
[1, 2, 3],
0.15
);
// Load initial status and start polling
useEffect(() => {
loadStatus();
loadConfiguration();
// Start polling for status updates every 5 seconds
statusPollInterval.current = setInterval(loadStatus, 5000);
return () => {
if (statusPollInterval.current) {
clearInterval(statusPollInterval.current);
}
mcpServerService.disconnectLogs();
};
}, []);
// Start WebSocket connection when server is running
useEffect(() => {
if (serverStatus.status === 'running') {
// Fetch historical logs first (last 100 entries)
mcpServerService.getLogs({ limit: 100 }).then(historicalLogs => {
setLogs(historicalLogs);
}).catch(console.error);
// Then start streaming new logs via WebSocket
mcpServerService.streamLogs((log) => {
setLogs(prev => [...prev, log]);
}, { autoReconnect: true });
// Ensure configuration is loaded when server is running
if (!config) {
loadConfiguration();
}
} else {
mcpServerService.disconnectLogs();
}
}, [serverStatus.status]);
// Auto-scroll logs to bottom when new logs arrive
useEffect(() => {
if (logsContainerRef.current && logsEndRef.current) {
logsContainerRef.current.scrollTop = logsContainerRef.current.scrollHeight;
}
}, [logs]);
/**
* Load the current MCP server status
* Called on mount and every 5 seconds via polling
*/
const loadStatus = async () => {
try {
const status = await mcpServerService.getStatus();
setServerStatus(status);
setIsLoading(false);
} catch (error) {
console.error('Failed to load server status:', error);
setIsLoading(false);
}
};
/**
* Load the MCP server configuration
* Falls back to default values if database load fails
*/
const loadConfiguration = async () => {
try {
const cfg = await mcpServerService.getConfiguration();
console.log('Loaded configuration:', cfg);
setConfig(cfg);
} catch (error) {
console.error('Failed to load configuration:', error);
// Set a default config if loading fails
// Try to detect port from environment or use default
const defaultPort = import.meta.env.ARCHON_MCP_PORT || 8051;
setConfig({
transport: 'http',
host: 'localhost',
port: typeof defaultPort === 'string' ? parseInt(defaultPort) : defaultPort
});
}
};
/**
* Start the MCP server
*/
const handleStartServer = async () => {
try {
setIsStarting(true);
const response = await mcpServerService.startServer();
showToast(response.message, 'success');
// Immediately refresh status
await loadStatus();
} catch (error: any) {
showToast(error.message || 'Failed to start server', 'error');
} finally {
setIsStarting(false);
}
};
const handleStopServer = async () => {
try {
setIsStopping(true);
const response = await mcpServerService.stopServer();
showToast(response.message, 'success');
// Clear logs when server stops
setLogs([]);
// Immediately refresh status
await loadStatus();
} catch (error: any) {
showToast(error.message || 'Failed to stop server', 'error');
} finally {
setIsStopping(false);
}
};
const handleClearLogs = async () => {
try {
await mcpServerService.clearLogs();
setLogs([]);
showToast('Logs cleared', 'success');
} catch (error) {
showToast('Failed to clear logs', 'error');
}
};
const handleCopyConfig = () => {
if (!config) return;
const configText = getConfigForIDE(selectedIDE);
navigator.clipboard.writeText(configText);
showToast('Configuration copied to clipboard', 'success');
};
const generateCursorDeeplink = () => {
if (!config) return '';
const httpConfig = {
url: `http://${config.host}:${config.port}/mcp`
};
const configString = JSON.stringify(httpConfig);
const base64Config = btoa(configString);
return `cursor://anysphere.cursor-deeplink/mcp/install?name=archon&config=${base64Config}`;
};
const handleCursorOneClick = () => {
const deeplink = generateCursorDeeplink();
window.location.href = deeplink;
showToast('Opening Cursor with Archon MCP configuration...', 'info');
};
const getConfigForIDE = (ide: SupportedIDE) => {
if (!config || !config.host || !config.port) {
return '// Configuration not available. Please ensure the server is running.';
}
const mcpUrl = `http://${config.host}:${config.port}/mcp`;
switch(ide) {
case 'claudecode':
return JSON.stringify({
name: "archon",
transport: "http",
url: mcpUrl
}, null, 2);
case 'cline':
case 'kiro':
// Cline and Kiro use stdio transport with mcp-remote
return JSON.stringify({
mcpServers: {
archon: {
command: "npx",
args: ["mcp-remote", mcpUrl]
}
}
}, null, 2);
case 'windsurf':
return JSON.stringify({
mcpServers: {
archon: {
serverUrl: mcpUrl
}
}
}, null, 2);
case 'cursor':
case 'augment':
return JSON.stringify({
mcpServers: {
archon: {
url: mcpUrl
}
}
}, null, 2);
default:
return '';
case 'gemini':
return JSON.stringify({
mcpServers: {
archon: {
httpUrl: mcpUrl
}
}
}, null, 2);
}
};
const getIDEInstructions = (ide: SupportedIDE) => {
switch (ide) {
case 'windsurf':
return {
title: 'Windsurf Configuration',
steps: [
'1. Open Windsurf and click the "MCP servers" button (hammer icon)',
'2. Click "Configure" and then "View raw config"',
'3. Add the configuration shown below to the mcpServers object',
'4. Click "Refresh" to connect to the server'
]
};
case 'cursor':
return {
title: 'Cursor Configuration',
steps: [
'1. Option A: Use the one-click install button below (recommended)',
'2. Option B: Manually edit ~/.cursor/mcp.json',
'3. Add the configuration shown below',
'4. Restart Cursor for changes to take effect'
]
};
case 'claudecode':
return {
title: 'Claude Code Configuration',
steps: [
'1. Open a terminal and run the following command:',
`2. claude mcp add --transport http archon http://${config?.host}:${config?.port}/mcp`,
'3. The connection will be established automatically'
]
};
case 'cline':
return {
title: 'Cline Configuration',
steps: [
'1. Open VS Code settings (Cmd/Ctrl + ,)',
'2. Search for "cline.mcpServers"',
'3. Click "Edit in settings.json"',
'4. Add the configuration shown below',
'5. Restart VS Code for changes to take effect'
]
};
case 'kiro':
return {
title: 'Kiro Configuration',
steps: [
'1. Open Kiro settings',
'2. Navigate to MCP Servers section',
'3. Add the configuration shown below',
'4. Save and restart Kiro'
]
};
case 'augment':
return {
title: 'Augment Configuration',
steps: [
'1. Open Augment settings',
'2. Navigate to Extensions > MCP',
'3. Add the configuration shown below',
'4. Reload configuration'
]
};
case 'gemini':
return {
title: 'Gemini CLI Configuration',
steps: [
'1. Locate or create the settings file at ~/.gemini/settings.json',
'2. Add the configuration shown below to the file',
'3. Launch Gemini CLI in your terminal',
'4. Test the connection by typing /mcp to list available tools'
]
};
default:
return {
title: 'Configuration',
steps: ['Add the configuration to your IDE settings']
};
}
};
const formatUptime = (seconds: number): string => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return `${hours}h ${minutes}m ${secs}s`;
};
const formatLogEntry = (log: LogEntry | string): string => {
if (typeof log === 'string') {
return log;
}
return `[${log.level}] ${log.message}`;
};
const getStatusIcon = () => {
switch (serverStatus.status) {
case 'running':
return <CheckCircle className="w-5 h-5 text-green-500" />;
case 'starting':
case 'stopping':
return <Loader className="w-5 h-5 text-blue-500 animate-spin" />;
default:
return <AlertCircle className="w-5 h-5 text-red-500" />;
}
};
const getStatusColor = () => {
switch (serverStatus.status) {
case 'running':
return 'text-green-500';
case 'starting':
case 'stopping':
return 'text-blue-500';
default:
return 'text-red-500';
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<Loader className="animate-spin text-gray-500" size={32} />
</div>
);
}
return (
<motion.div
initial="hidden"
animate={isVisible ? 'visible' : 'hidden'}
variants={containerVariants}
>
<motion.h1
className="text-3xl font-bold text-gray-800 dark:text-white mb-8 flex items-center gap-3"
variants={titleVariants}
>
<svg fill="currentColor" fillRule="evenodd" height="28" width="28" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" className="text-pink-500 filter drop-shadow-[0_0_8px_rgba(236,72,153,0.8)]">
<path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z"></path>
<path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z"></path>
</svg>
MCP Dashboard
</motion.h1>
{/* Tab Navigation */}
<motion.div className="mb-6 border-b border-gray-200 dark:border-gray-800" variants={itemVariants}>
<div className="flex space-x-8">
<button
onClick={() => setActiveTab('server')}
className={`pb-3 relative ${
activeTab === 'server'
? 'text-blue-600 dark:text-blue-400 font-medium'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
}`}
>
Server Control
{activeTab === 'server' && (
<span className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-500 shadow-[0_0_10px_rgba(59,130,246,0.5)]"></span>
)}
</button>
{/* TODO: MCP Client feature not implemented - commenting out for now
<button
onClick={() => setActiveTab('clients')}
className={`pb-3 relative ${
activeTab === 'clients'
? 'text-cyan-600 dark:text-cyan-400 font-medium'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
}`}
>
MCP Clients
{activeTab === 'clients' && (
<span className="absolute bottom-0 left-0 right-0 h-0.5 bg-cyan-500 shadow-[0_0_10px_rgba(34,211,238,0.5)]"></span>
)}
</button>
*/}
</div>
</motion.div>
{/* Server Control Tab */}
{activeTab === 'server' && (
<>
{/* Server Control + Server Logs */}
<motion.div className="grid grid-cols-1 lg:grid-cols-2 gap-6" variants={itemVariants}>
{/* Left Column: Archon MCP Server */}
<div className="flex flex-col">
<h2 className="text-xl font-semibold text-gray-800 dark:text-white mb-4 flex items-center">
<Server className="mr-2 text-blue-500" size={20} />
Archon MCP Server
</h2>
<Card accentColor="blue" className="space-y-6 flex-1">
{/* Status Display */}
<div className="flex items-center justify-between">
<div
className="flex items-center gap-3 cursor-help"
title={process.env.NODE_ENV === 'development' ?
`Debug Info:\nStatus: ${serverStatus.status}\nConfig: ${config ? 'loaded' : 'null'}\n${config ? `Details: ${JSON.stringify(config, null, 2)}` : ''}` :
undefined
}
>
{getStatusIcon()}
<div>
<p className={`font-semibold ${getStatusColor()}`}>
Status: {serverStatus.status.charAt(0).toUpperCase() + serverStatus.status.slice(1)}
</p>
{serverStatus.uptime !== null && (
<p className="text-sm text-gray-600 dark:text-zinc-400">
Uptime: {formatUptime(serverStatus.uptime)}
</p>
)}
</div>
</div>
{/* Control Buttons */}
<div className="flex gap-2 items-center">
{serverStatus.status === 'stopped' ? (
<Button
onClick={handleStartServer}
disabled={isStarting}
variant="primary"
accentColor="green"
className="shadow-emerald-500/20 shadow-sm"
>
{isStarting ? (
<>
<Loader className="w-4 h-4 mr-2 animate-spin inline" />
Starting...
</>
) : (
<>
<Play className="w-4 h-4 mr-2 inline" />
Start Server
</>
)}
</Button>
) : (
<Button
onClick={handleStopServer}
disabled={isStopping || serverStatus.status !== 'running'}
variant="primary"
accentColor="pink"
className="shadow-pink-500/20 shadow-sm"
>
{isStopping ? (
<>
<Loader className="w-4 h-4 mr-2 animate-spin inline" />
Stopping...
</>
) : (
<>
<Square className="w-4 h-4 mr-2 inline" />
Stop Server
</>
)}
</Button>
)}
</div>
</div>
{/* Connection Details */}
{serverStatus.status === 'running' && config && (
<div className="border-t border-gray-200 dark:border-zinc-800 pt-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium text-gray-700 dark:text-zinc-300">
IDE Configuration
<span className="ml-2 px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-full">
HTTP Mode
</span>
</h3>
<Button
variant="secondary"
accentColor="blue"
size="sm"
onClick={handleCopyConfig}
>
<Copy className="w-3 h-3 mr-1 inline" />
Copy
</Button>
</div>
{/* IDE Selection Tabs */}
<div className="mb-4">
<div className="flex flex-wrap border-b border-gray-200 dark:border-zinc-700 mb-3">
<button
onClick={() => setSelectedIDE('claudecode')}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
selectedIDE === 'claudecode'
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-zinc-400 hover:text-gray-700 dark:hover:text-zinc-300'
} cursor-pointer`}
>
Claude Code
</button>
<button
onClick={() => setSelectedIDE('gemini')}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
selectedIDE === 'gemini'
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-zinc-400 hover:text-gray-700 dark:hover:text-zinc-300'
} cursor-pointer`}
>
Gemini CLI
</button>
<button
onClick={() => setSelectedIDE('cursor')}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
selectedIDE === 'cursor'
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-zinc-400 hover:text-gray-700 dark:hover:text-zinc-300'
} cursor-pointer`}
>
Cursor
</button>
<button
onClick={() => setSelectedIDE('windsurf')}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
selectedIDE === 'windsurf'
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-zinc-400 hover:text-gray-700 dark:hover:text-zinc-300'
} cursor-pointer`}
>
Windsurf
</button>
<button
onClick={() => setSelectedIDE('cline')}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
selectedIDE === 'cline'
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-zinc-400 hover:text-gray-700 dark:hover:text-zinc-300'
} cursor-pointer`}
>
Cline
</button>
<button
onClick={() => setSelectedIDE('kiro')}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
selectedIDE === 'kiro'
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-zinc-400 hover:text-gray-700 dark:hover:text-zinc-300'
} cursor-pointer`}
>
Kiro
</button>
<button
onClick={() => setSelectedIDE('augment')}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
selectedIDE === 'augment'
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-zinc-400 hover:text-gray-700 dark:hover:text-zinc-300'
} cursor-pointer`}
>
Augment
</button>
</div>
</div>
{/* IDE Instructions */}
<div className="mb-4">
<h4 className="text-sm font-medium text-gray-700 dark:text-zinc-300 mb-2">
{getIDEInstructions(selectedIDE).title}
</h4>
<ul className="text-sm text-gray-600 dark:text-zinc-400 space-y-1">
{getIDEInstructions(selectedIDE).steps.map((step, index) => (
<li key={index}>{step}</li>
))}
</ul>
</div>
<div className="bg-gray-50 dark:bg-black/50 rounded-lg p-4 font-mono text-sm relative">
<pre className="text-gray-600 dark:text-zinc-400 whitespace-pre-wrap">
{getConfigForIDE(selectedIDE)}
</pre>
<p className="text-xs text-gray-500 dark:text-zinc-500 mt-3 font-sans">
{selectedIDE === 'cursor'
? 'Copy this configuration and add it to ~/.cursor/mcp.json'
: selectedIDE === 'windsurf'
? 'Copy this configuration and add it to your Windsurf MCP settings'
: selectedIDE === 'claudecode'
? 'This shows the configuration format for Claude Code'
: selectedIDE === 'cline'
? 'Copy this configuration and add it to VS Code settings.json under "cline.mcpServers"'
: selectedIDE === 'kiro'
? 'Copy this configuration and add it to your Kiro MCP settings'
: selectedIDE === 'augment'
? 'Copy this configuration and add it to your Augment MCP settings'
: 'Copy this configuration and add it to your IDE settings'
}
</p>
</div>
{/* One-click install button for Cursor */}
{selectedIDE === 'cursor' && serverStatus.status === 'running' && (
<div className="mt-4">
<Button
variant="primary"
accentColor="blue"
onClick={handleCursorOneClick}
className="w-full"
>
<Server className="w-4 h-4 mr-2 inline" />
One-Click Install for Cursor
</Button>
<p className="text-xs text-gray-500 dark:text-zinc-500 mt-2 text-center">
Requires Cursor to be installed and will open a deeplink
</p>
</div>
)}
</div>
)}
</Card>
</div>
{/* Right Column: Server Logs */}
<div className="flex flex-col">
<h2 className="text-xl font-semibold text-gray-800 dark:text-white mb-4 flex items-center">
<Clock className="mr-2 text-purple-500" size={20} />
Server Logs
</h2>
<Card accentColor="purple" className="h-full flex flex-col">
<div className="flex items-center justify-between mb-4">
<p className="text-sm text-gray-600 dark:text-zinc-400">
{logs.length > 0
? `Showing ${logs.length} log entries`
: 'No logs available'
}
</p>
<Button
variant="ghost"
size="sm"
onClick={handleClearLogs}
disabled={logs.length === 0}
>
Clear Logs
</Button>
</div>
<div
id="mcp-logs-container"
ref={logsContainerRef}
className="bg-gray-50 dark:bg-black border border-gray-200 dark:border-zinc-900 rounded-md p-4 flex-1 overflow-y-auto font-mono text-sm max-h-[600px]"
>
{logs.length === 0 ? (
<p className="text-gray-500 dark:text-zinc-500 text-center py-8">
{serverStatus.status === 'running'
? 'Waiting for log entries...'
: 'Start the server to see logs'
}
</p>
) : (
logs.map((log, index) => (
<div
key={index}
className={`py-1.5 border-b border-gray-100 dark:border-zinc-900 last:border-0 ${
typeof log !== 'string' && log.level === 'ERROR'
? 'text-red-600 dark:text-red-400'
: typeof log !== 'string' && log.level === 'WARNING'
? 'text-yellow-600 dark:text-yellow-400'
: 'text-gray-600 dark:text-zinc-400'
}`}
>
{formatLogEntry(log)}
</div>
))
)}
<div ref={logsEndRef} />
</div>
</Card>
</div>
</motion.div>
{/* Global Rules Section */}
<motion.div className="mt-6" variants={itemVariants}>
<h2 className="text-xl font-semibold text-gray-800 dark:text-white mb-4 flex items-center">
<Server className="mr-2 text-pink-500" size={20} />
Global IDE Rules
</h2>
<IDEGlobalRules />
</motion.div>
</>
)}
{/* Clients Tab - commented out as feature not implemented
{activeTab === 'clients' && (
<motion.div variants={itemVariants}>
<MCPClients />
</motion.div>
)}
*/}
</motion.div>
);
};