Files
nexus-mcp/src/components/MCPHubTab.tsx

1533 lines
65 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import {
Server,
Plus,
Play,
CheckCircle2,
XCircle,
RefreshCw,
Trash2,
Edit,
Power,
Globe,
Code2,
Webhook,
Cpu,
Download,
Upload,
Search,
Sparkles,
ExternalLink,
ChevronDown,
ChevronUp,
Sliders,
Layers,
Terminal,
CloudSun,
Coins,
Newspaper,
Database,
Github,
MessageSquare,
FileCode,
ShieldCheck,
Zap,
Compass,
Copy,
Check,
Radio,
Key,
Repeat,
} from 'lucide-react';
import { MCPServerDefinition, ToolDefinition, ServerStats } from '../types';
import { RoadSignModal } from './RoadSignModal';
import { PackageRunnerModal } from './PackageRunnerModal';
import { MCPSkillGuideModal } from './MCPSkillGuideModal';
interface MCPHubTabProps {
stats: ServerStats | null;
onRefreshStats?: () => void;
onNavigateToStudio?: (prompt?: string) => void;
}
export const MCPHubTab: React.FC<MCPHubTabProps> = ({
stats,
onRefreshStats,
onNavigateToStudio,
}) => {
const [servers, setServers] = useState<MCPServerDefinition[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [filterCategory, setFilterCategory] = useState<string>('all');
const [filterType, setFilterType] = useState<string>('all');
const [searchQuery, setSearchQuery] = useState<string>('');
const [activeTab, setActiveTab] = useState<'servers' | 'road_signs' | 'marketplace'>('servers');
// Modal states
const [isAddModalOpen, setIsAddModalOpen] = useState<boolean>(false);
const [isRoadSignModalOpen, setIsRoadSignModalOpen] = useState<boolean>(false);
const [editingRoadSignServer, setEditingRoadSignServer] = useState<MCPServerDefinition | null>(null);
const [isPackageRunnerModalOpen, setIsPackageRunnerModalOpen] = useState<boolean>(false);
const [isSkillGuideModalOpen, setIsSkillGuideModalOpen] = useState<boolean>(false);
const [isImportModalOpen, setIsImportModalOpen] = useState<boolean>(false);
const [importJsonText, setImportJsonText] = useState<string>('');
const [pingingServerId, setPingingServerId] = useState<string | null>(null);
const [expandedServerId, setExpandedServerId] = useState<string | null>('builtin-mcp-core');
const [copiedConnectSnippet, setCopiedConnectSnippet] = useState<string | null>(null);
// New server form state
const [newServerType, setNewServerType] = useState<'remote_http' | 'webhook' | 'custom_script'>('remote_http');
const [formName, setFormName] = useState<string>('');
const [formDescription, setFormDescription] = useState<string>('');
const [formCategory, setFormCategory] = useState<MCPServerDefinition['category']>('custom');
// Remote HTTP fields
const [formEndpointUrl, setFormEndpointUrl] = useState<string>('');
const [formHeadersText, setFormHeadersText] = useState<string>('{\n "Authorization": "Bearer MY_API_TOKEN"\n}');
// Webhook fields
const [formWebhookUrl, setFormWebhookUrl] = useState<string>('https://api.example.com/v1/resource?id={{id}}');
const [formWebhookMethod, setFormWebhookMethod] = useState<'GET' | 'POST' | 'PUT'>('GET');
const [formWebhookBody, setFormWebhookBody] = useState<string>('{\n "message": "{{message}}"\n}');
// Custom Script fields
const [formToolName, setFormToolName] = useState<string>('my_custom_tool');
const [formToolDesc, setFormToolDesc] = useState<string>('Executes custom calculation or data processing logic.');
const [formCustomScript, setFormCustomScript] = useState<string>(`// Available arguments in 'args' object (e.g. args.text, args.number)\nconst text = args.text || '';\nreturn {\n status: 'processed',\n length: text.length,\n uppercase: text.toUpperCase(),\n timestamp: new Date().toISOString()\n};`);
const [formSchemaJson, setFormSchemaJson] = useState<string>(`{\n "type": "object",\n "properties": {\n "text": { "type": "string", "description": "Input text to process" }\n },\n "required": ["text"]\n}`);
// Test execution state
const [testTool, setTestTool] = useState<{ name: string; schema: any } | null>(null);
const [testArgsJson, setTestArgsJson] = useState<string>('{}');
const [testResult, setTestResult] = useState<any>(null);
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const fetchServers = async () => {
try {
setLoading(true);
const res = await fetch('/api/v1/mcp/servers');
if (res.ok) {
const data = await res.json();
setServers(data);
}
} catch (err) {
console.error('Failed to fetch MCP servers:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchServers();
}, []);
const handleToggleServer = async (id: string, currentEnabled: boolean) => {
try {
const res = await fetch(`/api/v1/mcp/servers/${id}/toggle`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: !currentEnabled }),
});
if (res.ok) {
fetchServers();
if (onRefreshStats) onRefreshStats();
}
} catch (err) {
console.error('Failed to toggle server:', err);
}
};
const handlePingServer = async (id: string) => {
setPingingServerId(id);
try {
const res = await fetch(`/api/v1/mcp/servers/${id}/ping`, { method: 'POST' });
if (res.ok) {
const pingResult = await res.json();
setServers((prev) =>
prev.map((s) =>
s.id === id
? {
...s,
healthStatus: pingResult.success ? 'healthy' : 'unreachable',
lastPingMs: pingResult.latencyMs,
lastPingAt: new Date().toISOString(),
}
: s
)
);
}
} catch (err) {
console.error('Failed to ping server:', err);
} finally {
setPingingServerId(null);
}
};
const handleDeleteServer = async (id: string) => {
if (!confirm('Are you sure you want to remove this MCP server registration?')) return;
try {
const res = await fetch(`/api/v1/mcp/servers/${id}`, { method: 'DELETE' });
if (res.ok) {
fetchServers();
if (onRefreshStats) onRefreshStats();
} else {
const err = await res.json();
alert(err.error || 'Failed to delete server');
}
} catch (err) {
console.error('Failed to delete server:', err);
}
};
const handleSaveRoadSign = async (roadSignServer: Partial<MCPServerDefinition>) => {
const isUpdate = Boolean(roadSignServer.id && servers.some((s) => s.id === roadSignServer.id));
const url = isUpdate ? `/api/v1/mcp/servers/${roadSignServer.id}` : '/api/v1/mcp/servers';
const method = isUpdate ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(roadSignServer),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || 'Failed to save Road Sign server');
}
await fetchServers();
if (onRefreshStats) onRefreshStats();
};
const handleCreateServer = async (e: React.FormEvent) => {
e.preventDefault();
try {
let parsedSchema = { type: 'object', properties: {} };
try {
parsedSchema = JSON.parse(formSchemaJson);
} catch (err) {
alert('Invalid Input Schema JSON formatting.');
return;
}
let parsedHeaders = undefined;
if (newServerType === 'remote_http' && formHeadersText.trim()) {
try {
parsedHeaders = JSON.parse(formHeadersText);
} catch (err) {
alert('Invalid Headers JSON format.');
return;
}
}
const tools: ToolDefinition[] = [];
if (newServerType === 'custom_script') {
tools.push({
name: formToolName.trim().replace(/\s+/g, '_').toLowerCase(),
description: formToolDesc,
category: formCategory,
inputSchema: parsedSchema as any,
});
} else if (newServerType === 'webhook') {
tools.push({
name: formName.trim().replace(/\s+/g, '_').toLowerCase() + '_tool',
description: formDescription,
category: formCategory,
inputSchema: parsedSchema as any,
});
}
const payload: Partial<MCPServerDefinition> = {
name: formName,
description: formDescription,
type: newServerType,
category: formCategory,
enabled: true,
endpointUrl: newServerType === 'remote_http' ? formEndpointUrl : undefined,
headers: parsedHeaders,
webhookConfig:
newServerType === 'webhook'
? {
url: formWebhookUrl,
method: formWebhookMethod,
bodyTemplate: formWebhookMethod !== 'GET' ? formWebhookBody : undefined,
}
: undefined,
customScript: newServerType === 'custom_script' ? formCustomScript : undefined,
tools,
};
const res = await fetch('/api/v1/mcp/servers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (res.ok) {
setIsAddModalOpen(false);
setFormName('');
setFormDescription('');
fetchServers();
if (onRefreshStats) onRefreshStats();
} else {
const err = await res.json();
alert(err.error || 'Failed to create MCP server');
}
} catch (err: any) {
alert('Error creating server: ' + err.message);
}
};
const handleInstallPreset = async (preset: {
name: string;
description: string;
type: 'webhook' | 'custom_script' | 'remote_http';
category: MCPServerDefinition['category'];
webhookConfig?: any;
customScript?: string;
tools: ToolDefinition[];
icon?: string;
}) => {
try {
const res = await fetch('/api/v1/mcp/servers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...preset,
enabled: true,
isPreset: true,
}),
});
if (res.ok) {
fetchServers();
setActiveTab('servers');
if (onRefreshStats) onRefreshStats();
} else {
alert('Failed to install preset MCP server');
}
} catch (err) {
console.error('Error installing preset:', err);
}
};
const handleImportJson = async () => {
try {
const parsed = JSON.parse(importJsonText);
const serversArray = Array.isArray(parsed) ? parsed : parsed.servers;
if (!Array.isArray(serversArray)) {
alert('Invalid format. Expected JSON array or { "servers": [...] } object.');
return;
}
const res = await fetch('/api/v1/mcp/servers/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ servers: serversArray }),
});
if (res.ok) {
const data = await res.json();
alert(`Successfully imported ${data.importedCount} MCP servers!`);
setIsImportModalOpen(false);
setImportJsonText('');
fetchServers();
if (onRefreshStats) onRefreshStats();
}
} catch (err: any) {
alert('Failed to parse or import JSON: ' + err.message);
}
};
const handleExportJson = () => {
const jsonStr = JSON.stringify({ servers }, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `mcp-nexus-registry-${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
};
const handleCopySnippet = (id: string, text: string) => {
navigator.clipboard.writeText(text);
setCopiedConnectSnippet(id);
setTimeout(() => setCopiedConnectSnippet(null), 2000);
};
const handleTestExecute = async () => {
if (!testTool) return;
setIsExecuting(true);
setTestResult(null);
try {
let argsObj = {};
try {
argsObj = JSON.parse(testArgsJson);
} catch (e) {
alert('Invalid args JSON format');
setIsExecuting(false);
return;
}
const res = await fetch(`/api/v1/tools/${testTool.name}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(argsObj),
});
const data = await res.json();
setTestResult(data);
} catch (err: any) {
setTestResult({ success: false, error: err.message });
} finally {
setIsExecuting(false);
}
};
// Filter servers
const roadSignServers = servers.filter((s) => s.roadSign?.isRoadSign);
const filteredServers = servers.filter((s) => {
const matchesSearch =
s.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
s.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
s.roadSign?.signpostTitle?.toLowerCase().includes(searchQuery.toLowerCase()) ||
s.tools?.some((t) => t.name.toLowerCase().includes(searchQuery.toLowerCase()));
const matchesCategory = filterCategory === 'all' || s.category === filterCategory;
const matchesType = filterType === 'all' || s.type === filterType;
return matchesSearch && matchesCategory && matchesType;
});
const getTypeBadge = (server: MCPServerDefinition) => {
if (server.roadSign?.isRoadSign) {
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-black bg-amber-100 text-amber-900 border border-amber-300">
<Compass className="w-3 h-3 mr-1 text-amber-700" />
Road Sign ({server.roadSign.osPlatform})
</span>
);
}
switch (server.type) {
case 'builtin':
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800 border border-indigo-200">
<Cpu className="w-3 h-3 mr-1" /> Built-in Engine
</span>
);
case 'runner_process':
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-cyan-100 text-cyan-800 border border-cyan-200">
<Terminal className="w-3 h-3 mr-1" /> Runner ({server.runnerConfig?.packageManager || 'cli'})
</span>
);
case 'remote_http':
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-100 text-emerald-800 border border-emerald-200">
<Globe className="w-3 h-3 mr-1" /> Remote HTTP
</span>
);
case 'webhook':
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 border border-amber-200">
<Webhook className="w-3 h-3 mr-1" /> Webhook Bridge
</span>
);
case 'custom_script':
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 border border-purple-200">
<Code2 className="w-3 h-3 mr-1" /> JS Executable
</span>
);
}
};
const getIconComponent = (iconName?: string, osPlatform?: string) => {
if (osPlatform === 'windows') return <span className="text-lg">🪟</span>;
if (osPlatform === 'macos') return <span className="text-lg">🍎</span>;
if (osPlatform === 'linux') return <span className="text-lg">🐧</span>;
if (osPlatform === 'docker') return <span className="text-lg">🐳</span>;
switch (iconName) {
case 'Compass':
return <Compass className="w-5 h-5 text-amber-500" />;
case 'Terminal':
return <Terminal className="w-5 h-5 text-cyan-600" />;
case 'Layers':
return <Layers className="w-5 h-5 text-indigo-600" />;
case 'CloudSun':
return <CloudSun className="w-5 h-5 text-amber-500" />;
case 'Coins':
return <Coins className="w-5 h-5 text-emerald-500" />;
case 'Newspaper':
return <Newspaper className="w-5 h-5 text-indigo-500" />;
case 'Code2':
return <Code2 className="w-5 h-5 text-purple-500" />;
case 'Github':
return <Github className="w-5 h-5 text-slate-800" />;
case 'MessageSquare':
return <MessageSquare className="w-5 h-5 text-pink-500" />;
case 'Database':
return <Database className="w-5 h-5 text-blue-500" />;
case 'Zap':
return <Zap className="w-5 h-5 text-amber-500" />;
default:
return <Server className="w-5 h-5 text-slate-600" />;
}
};
const originUrl = typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000';
const sseUrl = `${originUrl}/mcp/sse`;
const autoSetupUrl = `${originUrl}/api/v1/agent/auto-setup`;
const claudeConfigSnippet = JSON.stringify(
{
mcpServers: {
'do-everything-nexus': {
url: sseUrl,
},
},
},
null,
2
);
// Preset Catalog Items
const presetCatalog: Array<{
name: string;
description: string;
type: 'remote_http' | 'custom_script' | 'webhook';
category: MCPServerDefinition['category'];
icon?: string;
webhookConfig?: any;
customScript?: string;
tools: ToolDefinition[];
}> = [
{
name: 'GitHub API & Code Repository MCP',
description: 'Search repositories, inspect commits, view user profiles, and fetch file contents directly from GitHub REST API.',
type: 'webhook',
category: 'code',
icon: 'Github',
webhookConfig: {
url: 'https://api.github.com/repos/{{owner}}/{{repo}}/contents/{{path}}',
method: 'GET',
headers: { 'User-Agent': 'DoEverythingMCP' },
},
tools: [
{
name: 'get_github_repo_file',
description: 'Fetch file content or directory listing from a public GitHub repository.',
category: 'code',
inputSchema: {
type: 'object',
properties: {
owner: { type: 'string', description: 'Repository owner or organization name (e.g., facebook)' },
repo: { type: 'string', description: 'Repository name (e.g., react)' },
path: { type: 'string', description: 'File path within repo (e.g., package.json)' },
},
required: ['owner', 'repo', 'path'],
},
},
],
},
{
name: 'Slack Notification Webhook MCP',
description: 'Bridge Slack incoming webhooks to enable connected AI agents to dispatch notifications to team channels.',
type: 'webhook',
category: 'network',
icon: 'MessageSquare',
webhookConfig: {
url: 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK',
method: 'POST',
bodyTemplate: '{\n "text": "🤖 *AI Agent Notification*:\\n{{message}}"\n}',
},
tools: [
{
name: 'send_slack_webhook_message',
description: 'Dispatch a formatted text message to team Slack channel via Webhook bridge.',
category: 'network',
inputSchema: {
type: 'object',
properties: {
message: { type: 'string', description: 'Text message to post to Slack' },
},
required: ['message'],
},
},
],
},
{
name: 'PostgreSQL Relational DB Query Bridge MCP',
description: 'Allows agents to execute query logic against SQL database connectors or REST database proxies.',
type: 'custom_script',
category: 'data',
icon: 'Database',
customScript: `
// Custom script proxy simulating SQL query parser
const sql = args.sql || '';
return {
query: sql,
rows: [
{ id: 1, name: "Sample Record A", status: "active", created_at: "2026-08-12" },
{ id: 2, name: "Sample Record B", status: "pending", created_at: "2026-08-11" }
],
rowCount: 2,
executionTimeMs: 12
};
`,
tools: [
{
name: 'execute_sql_query_proxy',
description: 'Execute read-only SQL queries or table metadata inspections.',
category: 'data',
inputSchema: {
type: 'object',
properties: {
sql: { type: 'string', description: 'SQL query statement (SELECT ...)' },
},
required: ['sql'],
},
},
],
},
{
name: 'Text Sentiment & Keyword Extractor MCP',
description: 'Standalone JavaScript script tool for fast client/server text analysis and keyphrase extraction.',
type: 'custom_script',
category: 'code',
icon: 'Zap',
customScript: `
const text = args.text || '';
const words = text.toLowerCase().match(/\\b[a-z]{4,}\\b/g) || [];
const freq = {};
words.forEach(w => freq[w] = (freq[w] || 0) + 1);
const topKeywords = Object.entries(freq).sort((a,b) => b[1] - a[1]).slice(0, 5).map(e => e[0]);
return {
textLength: text.length,
wordCount: text.split(/\\s+/).length,
topKeywords,
sentiment: text.includes('great') || text.includes('awesome') || text.includes('good') ? 'positive' : 'neutral'
};
`,
tools: [
{
name: 'extract_text_keywords_mcp',
description: 'Extract top keywords and estimate text sentiment.',
category: 'code',
inputSchema: {
type: 'object',
properties: {
text: { type: 'string', description: 'Text string to analyze' },
},
required: ['text'],
},
},
],
},
];
return (
<div className="space-y-8">
{/* Hero Header Banner with Central Source of Truth Metaphor */}
<div className="bg-gradient-to-r from-slate-900 via-indigo-950 to-slate-900 rounded-3xl p-6 sm:p-8 text-white shadow-2xl border border-slate-800 relative overflow-hidden">
<div className="absolute top-0 right-0 w-96 h-96 bg-indigo-500/15 rounded-full blur-3xl pointer-events-none" />
<div className="relative z-10 flex flex-col lg:flex-row lg:items-start lg:justify-between gap-6">
<div className="space-y-3 max-w-2xl">
<div className="inline-flex items-center space-x-2 px-3 py-1 rounded-full bg-indigo-500/20 text-indigo-300 text-xs font-semibold border border-indigo-500/30">
<Sparkles className="w-3.5 h-3.5" />
<span>Universal MCP Nexus & Central Source of Truth</span>
</div>
<h1 className="text-2xl sm:text-4xl font-black tracking-tight">
MCP Hub, Road Signs & Router Nexus
</h1>
<p className="text-slate-300 text-xs sm:text-sm leading-relaxed">
Install any MCP server via browser runners (<code className="text-indigo-300 font-mono">bunx</code>, <code className="text-indigo-300 font-mono">npx</code>, <code className="text-indigo-300 font-mono">uvx</code>, <code className="text-indigo-300 font-mono">python</code>). Point any AI model at the persistent SSE stream to gain instant access to all 27+ zero-key algorithmic tools, remote machine Road Signs, and intelligent workflows.
</p>
</div>
<div className="flex flex-wrap items-center gap-2.5">
<button
onClick={() => setIsPackageRunnerModalOpen(true)}
className="px-4 py-2.5 bg-gradient-to-r from-indigo-600 to-blue-600 hover:from-indigo-500 hover:to-blue-500 text-white text-xs sm:text-sm font-bold rounded-xl shadow-lg shadow-indigo-600/30 transition flex items-center space-x-2"
>
<Download className="w-4 h-4" />
<span>Install MCP (bun/npx)</span>
</button>
<button
onClick={() => {
setEditingRoadSignServer(null);
setIsRoadSignModalOpen(true);
}}
className="px-4 py-2.5 bg-amber-500 hover:bg-amber-400 text-slate-950 text-xs sm:text-sm font-black rounded-xl shadow-lg shadow-amber-500/30 transition flex items-center space-x-2"
>
<Compass className="w-4 h-4" />
<span>Erect Road Sign</span>
</button>
<button
onClick={() => setIsSkillGuideModalOpen(true)}
className="px-3.5 py-2.5 bg-slate-800 hover:bg-slate-700 text-indigo-300 text-xs sm:text-sm font-bold rounded-xl border border-slate-700 transition flex items-center space-x-2"
>
<Sparkles className="w-4 h-4 text-amber-400" />
<span>Creation Skill</span>
</button>
<button
onClick={() => setIsAddModalOpen(true)}
className="px-3 py-2.5 bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs sm:text-sm font-medium rounded-xl border border-slate-700 transition flex items-center space-x-1.5"
>
<Plus className="w-4 h-4" />
<span>Custom</span>
</button>
<button
onClick={handleExportJson}
className="p-2.5 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-xl border border-slate-700 transition"
title="Export Registry"
>
<Download className="w-4 h-4" />
</button>
</div>
</div>
{/* Live SSE Stream & Auto-Setup Connect Bar */}
<div className="mt-6 pt-5 border-t border-slate-800/80 bg-slate-950/60 p-4 rounded-2xl border border-slate-800/90 flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<div className="space-y-1">
<div className="flex items-center space-x-2">
<Radio className="w-4 h-4 text-emerald-400 animate-pulse" />
<span className="text-xs font-bold text-slate-200 uppercase tracking-wider">
Live Model SSE Source of Truth Endpoint
</span>
</div>
<div className="font-mono text-xs text-indigo-300 bg-slate-900 px-3 py-1.5 rounded-lg border border-slate-800 flex items-center space-x-2">
<span>{sseUrl}</span>
<button
onClick={() => handleCopySnippet('sse-url', sseUrl)}
className="text-slate-400 hover:text-white transition"
title="Copy SSE URL"
>
{copiedConnectSnippet === 'sse-url' ? (
<Check className="w-3.5 h-3.5 text-emerald-400" />
) : (
<Copy className="w-3.5 h-3.5" />
)}
</button>
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<button
onClick={() => handleCopySnippet('claude-config', claudeConfigSnippet)}
className="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-200 rounded-lg text-xs font-mono font-medium border border-slate-700 flex items-center space-x-1.5 transition"
>
{copiedConnectSnippet === 'claude-config' ? (
<Check className="w-3.5 h-3.5 text-emerald-400" />
) : (
<Copy className="w-3.5 h-3.5 text-slate-400" />
)}
<span>Claude Desktop JSON</span>
</button>
<a
href={autoSetupUrl}
target="_blank"
rel="noreferrer"
className="px-3 py-1.5 bg-indigo-900/60 hover:bg-indigo-900 text-indigo-300 rounded-lg text-xs font-mono font-medium border border-indigo-700/60 flex items-center space-x-1.5 transition"
>
<ExternalLink className="w-3.5 h-3.5" />
<span>Auto-Setup API</span>
</a>
</div>
</div>
{/* Quick Hub Metrics Row */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mt-4 pt-4 border-t border-slate-850 text-xs sm:text-sm font-mono">
<div>
<span className="text-slate-400 block text-xs">Total MCP Servers</span>
<span className="text-xl font-black text-white">{servers.length}</span>
</div>
<div>
<span className="text-slate-400 block text-xs">Road Signs (Remote)</span>
<span className="text-xl font-black text-amber-400">{roadSignServers.length}</span>
</div>
<div>
<span className="text-slate-400 block text-xs">Live Aggregated Tools</span>
<span className="text-xl font-black text-indigo-300">
{servers.reduce((acc, s) => (s.enabled ? acc + (s.tools?.length || 0) : acc), 0)}
</span>
</div>
<div>
<span className="text-slate-400 block text-xs">Router Latency</span>
<span className="text-xl font-black text-emerald-400">
{stats?.avgLatencyMs || 0}ms
</span>
</div>
</div>
</div>
{/* Navigation Filter Tabs */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 border-b border-slate-200 pb-4">
<div className="flex items-center space-x-2 bg-slate-100 p-1 rounded-xl">
<button
onClick={() => setActiveTab('servers')}
className={`px-4 py-2 rounded-lg text-xs sm:text-sm font-bold transition flex items-center space-x-2 ${
activeTab === 'servers'
? 'bg-white text-indigo-600 shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
<Layers className="w-4 h-4" />
<span>All Servers ({servers.length})</span>
</button>
<button
onClick={() => setActiveTab('road_signs')}
className={`px-4 py-2 rounded-lg text-xs sm:text-sm font-bold transition flex items-center space-x-2 ${
activeTab === 'road_signs'
? 'bg-amber-500 text-slate-950 shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
<Compass className="w-4 h-4" />
<span>Road Signs ({roadSignServers.length})</span>
</button>
<button
onClick={() => setActiveTab('marketplace')}
className={`px-4 py-2 rounded-lg text-xs sm:text-sm font-bold transition flex items-center space-x-2 ${
activeTab === 'marketplace'
? 'bg-white text-indigo-600 shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
<Sparkles className="w-4 h-4 text-amber-500" />
<span>Presets & Templates</span>
</button>
</div>
{activeTab !== 'marketplace' && (
<div className="flex flex-wrap items-center gap-2 w-full sm:w-auto">
<div className="relative flex-1 sm:w-64">
<Search className="w-4 h-4 absolute left-3 top-2.5 text-slate-400" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search servers, tools, road signs..."
className="w-full pl-9 pr-3 py-1.5 text-xs sm:text-sm bg-white border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
<select
value={filterType}
onChange={(e) => setFilterType(e.target.value)}
className="py-1.5 px-3 text-xs sm:text-sm bg-white border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
<option value="all">All Types</option>
<option value="builtin">Built-in (Zero-Key)</option>
<option value="runner_process">Runners (bunx/npx)</option>
<option value="remote_http">Remote HTTP</option>
<option value="webhook">Webhook Bridge</option>
<option value="custom_script">Custom JS</option>
</select>
</div>
)}
</div>
{/* Tab View: Road Signs Dedicated Signposts View */}
{activeTab === 'road_signs' && (
<div className="space-y-6">
<div className="bg-gradient-to-r from-amber-500 via-amber-400 to-yellow-500 rounded-2xl p-5 text-slate-950 shadow-md border-2 border-amber-600 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div>
<div className="flex items-center space-x-2 text-xs font-black uppercase tracking-widest">
<Compass className="w-4 h-4" />
<span>Road Signs Metaphor & Remote Host Credentials</span>
</div>
<h2 className="text-lg sm:text-xl font-black mt-0.5">
"LOOK HERE" Signposts for Remote Machines
</h2>
<p className="text-xs sm:text-sm text-slate-900 font-medium max-w-2xl mt-1">
Erect signposts on your Nexus pointing models to remote Windows, macOS, Linux, or GPU hosts. Inject API keys and credentials seamlessly on every tool call.
</p>
</div>
<button
onClick={() => {
setEditingRoadSignServer(null);
setIsRoadSignModalOpen(true);
}}
className="px-5 py-2.5 bg-slate-950 hover:bg-slate-900 text-amber-300 font-black rounded-xl shadow-md text-xs sm:text-sm flex items-center space-x-2 shrink-0 transition"
>
<Plus className="w-4 h-4" />
<span>Erect New Road Sign</span>
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
{roadSignServers.map((server) => (
<div
key={server.id}
className="bg-white rounded-2xl border-2 border-amber-300 p-5 space-y-4 shadow-sm hover:shadow-md transition relative overflow-hidden"
>
{/* Highway Sign Header */}
<div className="bg-amber-500 text-slate-950 -mx-5 -mt-5 p-4 border-b-2 border-amber-600 flex items-center justify-between">
<div className="flex items-center space-x-2">
<span className="text-xl">
{server.roadSign?.osPlatform === 'windows' ? '🪟' : server.roadSign?.osPlatform === 'macos' ? '🍎' : '🐧'}
</span>
<div>
<span className="text-[10px] font-black uppercase tracking-widest opacity-80 block">
ROAD SIGN {server.roadSign?.osPlatform?.toUpperCase()}
</span>
<h3 className="text-sm sm:text-base font-black truncate">
{server.roadSign?.signpostTitle || server.name}
</h3>
</div>
</div>
<span className="text-xs font-mono font-bold bg-amber-200/90 px-2 py-0.5 rounded text-slate-900">
{server.lastPingMs !== undefined ? `${server.lastPingMs}ms` : 'Healthy'}
</span>
</div>
<div className="space-y-2">
<div className="text-xs font-mono bg-slate-900 text-amber-300 p-2.5 rounded-xl flex items-center justify-between">
<span className="truncate">Host: {server.roadSign?.targetHostLocation || server.endpointUrl}</span>
<span className="text-slate-400 text-[10px]">MCP JSON-RPC</span>
</div>
<p className="text-xs text-slate-600 leading-relaxed">
{server.roadSign?.directionsInstructions || server.description}
</p>
</div>
{/* Credentials & API Keys configured */}
<div className="bg-slate-50 p-3 rounded-xl border border-slate-200 space-y-1.5 text-xs">
<div className="flex items-center justify-between font-bold text-slate-700">
<span className="flex items-center space-x-1">
<Key className="w-3.5 h-3.5 text-amber-600" />
<span>Injected Auth Headers ({Object.keys(server.roadSign?.credentialKeys || {}).length})</span>
</span>
<span className="text-[10px] text-emerald-700 bg-emerald-100 px-2 py-0.5 rounded-full">
Auto-Injected
</span>
</div>
<div className="flex flex-wrap gap-1 pt-1">
{Object.keys(server.roadSign?.credentialKeys || {}).map((headerKey) => (
<span
key={headerKey}
className="px-2 py-0.5 rounded bg-white text-slate-700 border border-slate-300 font-mono text-[11px]"
>
{headerKey}:
</span>
))}
</div>
</div>
{/* Tools exposed */}
<div className="space-y-1">
<span className="text-[11px] font-bold uppercase tracking-wider text-slate-400 block">
Remote Tools Exposed
</span>
<div className="flex flex-wrap gap-1">
{server.tools?.map((tool) => (
<span
key={tool.name}
className="px-2 py-0.5 rounded bg-indigo-50 text-indigo-700 border border-indigo-100 font-mono text-xs"
>
{tool.name}
</span>
))}
</div>
</div>
{/* Actions */}
<div className="flex items-center justify-between pt-3 border-t border-slate-100">
<button
onClick={() => handlePingServer(server.id)}
className="text-xs text-slate-600 hover:text-indigo-600 flex items-center space-x-1 font-semibold"
>
<RefreshCw className={`w-3.5 h-3.5 ${pingingServerId === server.id ? 'animate-spin' : ''}`} />
<span>Ping Host</span>
</button>
<div className="flex items-center space-x-2">
<button
onClick={() => {
setEditingRoadSignServer(server);
setIsRoadSignModalOpen(true);
}}
className="px-3 py-1.5 bg-amber-50 hover:bg-amber-100 text-amber-900 border border-amber-300 rounded-lg text-xs font-bold transition flex items-center space-x-1"
>
<Edit className="w-3.5 h-3.5" />
<span>Edit Keys & Signpost</span>
</button>
<button
onClick={() => handleDeleteServer(server.id)}
className="p-1.5 text-slate-400 hover:text-red-500"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Tab View: Installed Servers */}
{activeTab === 'servers' && (
<div className="space-y-4">
{loading ? (
<div className="py-12 text-center text-slate-500 flex flex-col items-center justify-center space-y-3">
<RefreshCw className="w-8 h-8 animate-spin text-indigo-500" />
<p>Loading registered MCP servers...</p>
</div>
) : filteredServers.length === 0 ? (
<div className="bg-slate-50 border border-slate-200 rounded-2xl p-12 text-center space-y-4">
<Server className="w-12 h-12 text-slate-400 mx-auto" />
<div>
<h3 className="text-base font-semibold text-slate-800">No MCP Servers Found</h3>
<p className="text-sm text-slate-500 max-w-md mx-auto mt-1">
No registered MCP servers matched your search criteria. Add a custom MCP server or browse the preset marketplace catalog!
</p>
</div>
<button
onClick={() => setIsAddModalOpen(true)}
className="px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-xl hover:bg-indigo-700 transition"
>
Add Your First MCP Server
</button>
</div>
) : (
filteredServers.map((server) => {
const isExpanded = expandedServerId === server.id;
return (
<div
key={server.id}
className={`bg-white rounded-2xl border transition shadow-sm overflow-hidden ${
server.enabled
? server.roadSign?.isRoadSign
? 'border-amber-300 hover:border-amber-400'
: 'border-slate-200 hover:border-slate-300'
: 'border-slate-200 opacity-60 bg-slate-50'
}`}
>
<div className="p-5 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-start space-x-4">
<div className="p-3 bg-slate-100 rounded-2xl border border-slate-200 mt-1">
{getIconComponent(server.icon, server.roadSign?.osPlatform)}
</div>
<div className="space-y-1">
<div className="flex items-center space-x-2 flex-wrap gap-y-1">
<h3 className="text-base font-bold text-slate-900">
{server.roadSign?.signpostTitle || server.name}
</h3>
{getTypeBadge(server)}
{server.enabled ? (
<span className="inline-flex items-center text-xs text-emerald-600 font-medium bg-emerald-50 px-2 py-0.5 rounded-full border border-emerald-200">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 mr-1.5" />
Active
</span>
) : (
<span className="inline-flex items-center text-xs text-slate-500 font-medium bg-slate-100 px-2 py-0.5 rounded-full border border-slate-200">
Disabled
</span>
)}
</div>
<p className="text-xs sm:text-sm text-slate-600 leading-normal max-w-2xl">
{server.description}
</p>
<div className="flex items-center space-x-4 text-xs text-slate-500 font-mono pt-1">
<span>{server.tools?.length || 0} Tools Exposed</span>
<span></span>
<span>
Ping:{' '}
{server.lastPingMs !== undefined ? `${server.lastPingMs}ms` : 'Not tested'}
</span>
</div>
</div>
</div>
<div className="flex items-center space-x-2 self-end sm:self-center">
<button
onClick={() => handlePingServer(server.id)}
disabled={pingingServerId === server.id}
title="Ping MCP Health"
className="p-2 text-slate-600 hover:text-indigo-600 hover:bg-slate-100 rounded-xl transition"
>
<RefreshCw
className={`w-4 h-4 ${pingingServerId === server.id ? 'animate-spin text-indigo-600' : ''}`}
/>
</button>
<button
onClick={() => handleToggleServer(server.id, server.enabled)}
title={server.enabled ? 'Disable MCP Server' : 'Enable MCP Server'}
className={`p-2 rounded-xl transition ${
server.enabled
? 'text-emerald-600 hover:bg-emerald-50'
: 'text-slate-400 hover:bg-slate-200'
}`}
>
<Power className="w-4 h-4" />
</button>
{!server.isPreset && (
<button
onClick={() => handleDeleteServer(server.id)}
title="Remove Server Registration"
className="p-2 text-slate-400 hover:text-rose-600 hover:bg-rose-50 rounded-xl transition"
>
<Trash2 className="w-4 h-4" />
</button>
)}
<button
onClick={() => setExpandedServerId(isExpanded ? null : server.id)}
className="p-2 text-slate-500 hover:text-slate-800 hover:bg-slate-100 rounded-xl transition flex items-center space-x-1 text-xs font-medium"
>
<span>{isExpanded ? 'Hide Tools' : 'View Tools'}</span>
{isExpanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
</div>
</div>
{/* Expanded Tools Details */}
{isExpanded && (
<div className="border-t border-slate-100 bg-slate-50/70 p-5 space-y-4">
{server.type === 'remote_http' && server.endpointUrl && (
<div className="text-xs font-mono bg-slate-900 text-slate-200 p-3 rounded-xl flex items-center justify-between">
<span>Endpoint: {server.endpointUrl}</span>
<span className="text-slate-400">JSON-RPC / SSE</span>
</div>
)}
<div className="space-y-2">
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-500">
Exposed MCP Tools ({server.tools?.length || 0})
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{server.tools?.map((tool) => (
<div
key={tool.name}
className="bg-white p-3.5 rounded-xl border border-slate-200 space-y-2 shadow-2xs"
>
<div className="flex items-center justify-between">
<span className="font-mono font-bold text-xs text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-lg border border-indigo-100">
{tool.name}
</span>
<button
onClick={() => {
setTestTool({ name: tool.name, schema: tool.inputSchema });
setTestArgsJson('{}');
setTestResult(null);
}}
className="text-xs text-slate-600 hover:text-indigo-600 flex items-center space-x-1 bg-slate-100 hover:bg-slate-200 px-2 py-1 rounded-lg transition"
>
<Play className="w-3 h-3" />
<span>Test Call</span>
</button>
</div>
<p className="text-xs text-slate-600 line-clamp-2">{tool.description}</p>
</div>
))}
</div>
</div>
</div>
)}
</div>
);
})
)}
</div>
)}
{/* Tab View: Marketplace Catalog */}
{activeTab === 'marketplace' && (
<div className="space-y-6">
<div className="bg-amber-50 border border-amber-200 rounded-2xl p-4 text-amber-900 text-xs sm:text-sm flex items-start space-x-3">
<Sparkles className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
<div>
<strong className="font-bold">Ready-to-Install MCP Server Templates</strong>
<p className="mt-0.5 text-amber-800">
Click "1-Click Install" on any template to instantly add its tools to your server hub registry.
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{presetCatalog.map((preset) => (
<div
key={preset.name}
className="bg-white rounded-2xl border border-slate-200 p-6 space-y-4 hover:shadow-md transition flex flex-col justify-between"
>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="p-2.5 bg-slate-100 rounded-xl border border-slate-200">
{getIconComponent(preset.icon)}
</div>
{getTypeBadge(preset as any)}
</div>
<div>
<h3 className="text-base font-bold text-slate-900">{preset.name}</h3>
<p className="text-xs text-slate-600 mt-1 leading-relaxed">{preset.description}</p>
</div>
<div className="space-y-1 pt-2 border-t border-slate-100">
<span className="text-xs text-slate-400 font-semibold uppercase tracking-wider block">
Tools Provided
</span>
{preset.tools.map((t) => (
<span
key={t.name}
className="inline-block text-xs font-mono bg-slate-100 text-slate-700 px-2 py-0.5 rounded-lg mr-1.5 mb-1 border border-slate-200"
>
{t.name}
</span>
))}
</div>
</div>
<button
onClick={() => handleInstallPreset(preset)}
className="w-full mt-4 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-semibold rounded-xl shadow-sm transition flex items-center justify-center space-x-2"
>
<Plus className="w-4 h-4" />
<span>1-Click Install to Hub</span>
</button>
</div>
))}
</div>
</div>
)}
{/* ROAD SIGN MODAL */}
<RoadSignModal
isOpen={isRoadSignModalOpen}
onClose={() => setIsRoadSignModalOpen(false)}
onSaveRoadSign={handleSaveRoadSign}
existingServer={editingRoadSignServer}
/>
{/* PACKAGE RUNNER MODAL */}
<PackageRunnerModal
isOpen={isPackageRunnerModalOpen}
onClose={() => setIsPackageRunnerModalOpen(false)}
onInstalled={() => {
fetchServers();
if (onRefreshStats) onRefreshStats();
}}
/>
{/* MCP SKILL GUIDE MODAL */}
<MCPSkillGuideModal
isOpen={isSkillGuideModalOpen}
onClose={() => setIsSkillGuideModalOpen(false)}
onSendToStudio={(p) => {
if (onNavigateToStudio) onNavigateToStudio(p);
}}
/>
{/* MODAL: Add New MCP Server */}
{isAddModalOpen && (
<div className="fixed inset-0 z-50 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 overflow-y-auto">
<div className="bg-white rounded-3xl max-w-xl w-full p-6 space-y-6 shadow-2xl border border-slate-200 my-8">
<div className="flex items-center justify-between border-b border-slate-100 pb-4">
<div>
<h3 className="text-lg font-bold text-slate-900">Register New MCP Server or Tool</h3>
<p className="text-xs text-slate-500">Configure remote HTTP JSON-RPC servers, webhook bridges, or custom JS tools.</p>
</div>
<button
onClick={() => setIsAddModalOpen(false)}
className="p-1 text-slate-400 hover:text-slate-600 rounded-lg"
>
<XCircle className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleCreateServer} className="space-y-5 text-xs sm:text-sm">
{/* Type Selection */}
<div>
<label className="block font-semibold text-slate-700 mb-2">MCP Integration Type</label>
<div className="grid grid-cols-3 gap-2">
<button
type="button"
onClick={() => setNewServerType('remote_http')}
className={`p-3 rounded-xl border text-center font-medium transition ${
newServerType === 'remote_http'
? 'border-indigo-600 bg-indigo-50 text-indigo-700 font-bold'
: 'border-slate-200 bg-white text-slate-600 hover:bg-slate-50'
}`}
>
<Globe className="w-4 h-4 mx-auto mb-1" />
Remote HTTP
</button>
<button
type="button"
onClick={() => setNewServerType('webhook')}
className={`p-3 rounded-xl border text-center font-medium transition ${
newServerType === 'webhook'
? 'border-indigo-600 bg-indigo-50 text-indigo-700 font-bold'
: 'border-slate-200 bg-white text-slate-600 hover:bg-slate-50'
}`}
>
<Webhook className="w-4 h-4 mx-auto mb-1" />
Webhook Bridge
</button>
<button
type="button"
onClick={() => setNewServerType('custom_script')}
className={`p-3 rounded-xl border text-center font-medium transition ${
newServerType === 'custom_script'
? 'border-indigo-600 bg-indigo-50 text-indigo-700 font-bold'
: 'border-slate-200 bg-white text-slate-600 hover:bg-slate-50'
}`}
>
<Code2 className="w-4 h-4 mx-auto mb-1" />
Custom JS Tool
</button>
</div>
</div>
{/* Common Fields */}
<div className="space-y-3">
<div>
<label className="block font-medium text-slate-700 mb-1">Server / Tool Name</label>
<input
type="text"
required
value={formName}
onChange={(e) => setFormName(e.target.value)}
placeholder="e.g. Weather Service MCP"
className="w-full px-3 py-2 bg-slate-50 border border-slate-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
<div>
<label className="block font-medium text-slate-700 mb-1">Description</label>
<textarea
rows={2}
value={formDescription}
onChange={(e) => setFormDescription(e.target.value)}
placeholder="What capabilities does this MCP server provide to connected agents?"
className="w-full px-3 py-2 bg-slate-50 border border-slate-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
</div>
{/* Remote HTTP Options */}
{newServerType === 'remote_http' && (
<div className="space-y-3 bg-slate-50 p-4 rounded-xl border border-slate-200">
<div>
<label className="block font-medium text-slate-700 mb-1">Remote MCP JSON-RPC URL</label>
<input
type="url"
required
value={formEndpointUrl}
onChange={(e) => setFormEndpointUrl(e.target.value)}
placeholder="https://remote-mcp-server.org/mcp"
className="w-full px-3 py-2 bg-white border border-slate-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 font-mono text-xs"
/>
</div>
<div>
<label className="block font-medium text-slate-700 mb-1">HTTP Authorization Headers (JSON)</label>
<textarea
rows={3}
value={formHeadersText}
onChange={(e) => setFormHeadersText(e.target.value)}
className="w-full px-3 py-2 bg-white border border-slate-300 rounded-xl font-mono text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
</div>
)}
{/* Webhook Options */}
{newServerType === 'webhook' && (
<div className="space-y-3 bg-amber-50/50 p-4 rounded-xl border border-amber-200">
<div className="grid grid-cols-3 gap-2">
<div className="col-span-1">
<label className="block font-medium text-slate-700 mb-1">Method</label>
<select
value={formWebhookMethod}
onChange={(e) => setFormWebhookMethod(e.target.value as any)}
className="w-full px-3 py-2 bg-white border border-slate-300 rounded-xl"
>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
</select>
</div>
<div className="col-span-2">
<label className="block font-medium text-slate-700 mb-1">Target Webhook URL</label>
<input
type="text"
required
value={formWebhookUrl}
onChange={(e) => setFormWebhookUrl(e.target.value)}
placeholder="https://api.com/v1/search?query={{query}}"
className="w-full px-3 py-2 bg-white border border-slate-300 rounded-xl font-mono text-xs"
/>
</div>
</div>
<div>
<label className="block font-medium text-slate-700 mb-1">Input Schema JSON</label>
<textarea
rows={3}
value={formSchemaJson}
onChange={(e) => setFormSchemaJson(e.target.value)}
className="w-full px-3 py-2 bg-white border border-slate-300 rounded-xl font-mono text-xs"
/>
</div>
</div>
)}
{/* Custom Script Options */}
{newServerType === 'custom_script' && (
<div className="space-y-3 bg-purple-50/50 p-4 rounded-xl border border-purple-200">
<div>
<label className="block font-medium text-slate-700 mb-1">Tool Name</label>
<input
type="text"
required
value={formToolName}
onChange={(e) => setFormToolName(e.target.value)}
className="w-full px-3 py-2 bg-white border border-slate-300 rounded-xl font-mono text-xs"
/>
</div>
<div>
<label className="block font-medium text-slate-700 mb-1">JavaScript Logic Script</label>
<textarea
rows={6}
value={formCustomScript}
onChange={(e) => setFormCustomScript(e.target.value)}
className="w-full px-3 py-2 bg-slate-900 text-purple-300 border border-slate-800 rounded-xl font-mono text-xs"
/>
</div>
</div>
)}
<div className="flex items-center justify-end space-x-3 pt-4 border-t border-slate-100">
<button
type="button"
onClick={() => setIsAddModalOpen(false)}
className="px-4 py-2 bg-slate-100 text-slate-700 rounded-xl hover:bg-slate-200 transition font-medium"
>
Cancel
</button>
<button
type="submit"
className="px-5 py-2 bg-indigo-600 text-white rounded-xl hover:bg-indigo-700 transition font-semibold"
>
Save MCP Server
</button>
</div>
</form>
</div>
</div>
)}
{/* MODAL: Import JSON */}
{isImportModalOpen && (
<div className="fixed inset-0 z-50 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4">
<div className="bg-white rounded-3xl max-w-lg w-full p-6 space-y-4 shadow-2xl border border-slate-200">
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
<h3 className="text-base font-bold text-slate-900">Import MCP Hub Configuration</h3>
<button onClick={() => setIsImportModalOpen(false)} className="text-slate-400 hover:text-slate-600">
<XCircle className="w-5 h-5" />
</button>
</div>
<p className="text-xs text-slate-600">
Paste exported MCP Hub server configuration JSON below to restore or bulk-register tools.
</p>
<textarea
rows={8}
value={importJsonText}
onChange={(e) => setImportJsonText(e.target.value)}
placeholder={`{\n "servers": [\n { "name": "Custom Server", "type": "remote_http", "endpointUrl": "..." }\n ]\n}`}
className="w-full p-3 bg-slate-900 text-slate-200 rounded-xl font-mono text-xs border border-slate-800"
/>
<div className="flex items-center justify-end space-x-3 pt-2">
<button
onClick={() => setIsImportModalOpen(false)}
className="px-4 py-2 text-xs font-medium text-slate-600 hover:bg-slate-100 rounded-xl"
>
Cancel
</button>
<button
onClick={handleImportJson}
className="px-4 py-2 text-xs font-semibold bg-indigo-600 text-white rounded-xl hover:bg-indigo-700"
>
Import MCP Configuration
</button>
</div>
</div>
</div>
)}
{/* MODAL: Tool Execution Test Drawer */}
{testTool && (
<div className="fixed inset-0 z-50 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4">
<div className="bg-white rounded-3xl max-w-xl w-full p-6 space-y-4 shadow-2xl border border-slate-200">
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
<div className="flex items-center space-x-2">
<Play className="w-4 h-4 text-indigo-600" />
<h3 className="text-base font-bold text-slate-900">Test Execution: {testTool.name}</h3>
</div>
<button onClick={() => setTestTool(null)} className="text-slate-400 hover:text-slate-600">
<XCircle className="w-5 h-5" />
</button>
</div>
<div className="space-y-3 text-xs">
<div>
<label className="block font-semibold text-slate-700 mb-1">Input Arguments (JSON)</label>
<textarea
rows={4}
value={testArgsJson}
onChange={(e) => setTestArgsJson(e.target.value)}
className="w-full p-3 bg-slate-900 text-indigo-300 font-mono text-xs rounded-xl border border-slate-800"
/>
</div>
<button
onClick={handleTestExecute}
disabled={isExecuting}
className="w-full py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-xl transition flex items-center justify-center space-x-2"
>
{isExecuting ? <RefreshCw className="w-4 h-4 animate-spin" /> : <Play className="w-4 h-4" />}
<span>{isExecuting ? 'Executing...' : 'Run Tool Test'}</span>
</button>
{testResult && (
<div className="space-y-1">
<label className="block font-semibold text-slate-700">Execution Result</label>
<pre className="p-3 bg-slate-900 text-emerald-400 font-mono text-xs rounded-xl overflow-x-auto max-h-60 border border-slate-800">
{JSON.stringify(testResult, null, 2)}
</pre>
</div>
)}
</div>
</div>
</div>
)}
</div>
);
};