Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
309 changes: 308 additions & 1 deletion frontend/src/components/ServerCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
ClockIcon,
CheckCircleIcon,
XCircleIcon,
QuestionMarkCircleIcon
QuestionMarkCircleIcon,
CogIcon,
ClipboardDocumentIcon
} from '@heroicons/react/24/outline';

interface Server {
Expand Down Expand Up @@ -90,6 +92,8 @@ const ServerCard: React.FC<ServerCardProps> = ({ server, onToggle, onEdit, canMo
const [tools, setTools] = useState<Tool[]>([]);
const [loadingTools, setLoadingTools] = useState(false);
const [showTools, setShowTools] = useState(false);
const [showConfig, setShowConfig] = useState(false);
const [selectedIDE, setSelectedIDE] = useState<'vscode' | 'cursor' | 'cline' | 'windsurf' | 'agents'>('vscode');
const [loadingRefresh, setLoadingRefresh] = useState(false);

const getStatusIcon = () => {
Expand Down Expand Up @@ -175,6 +179,119 @@ const ServerCard: React.FC<ServerCardProps> = ({ server, onToggle, onEdit, canMo
}
}, [server.path, loadingRefresh, onRefreshSuccess, onShowToast, onServerUpdate]);

// Generate MCP configuration for the server
const generateMCPConfig = useCallback(() => {
const serverName = server.name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');

// Get base URL and strip port for nginx proxy compatibility
const currentUrl = new URL(window.location.origin);
const baseUrl = `${currentUrl.protocol}//${currentUrl.hostname}`;

// Clean up server path - remove trailing slashes and ensure single leading slash
const cleanPath = server.path.replace(/\/+$/, '').replace(/^\/+/, '/');
const url = `${baseUrl}${cleanPath}/mcp`;

// Generate different config formats for different IDEs
switch(selectedIDE) {
// https://code.visualstudio.com/docs/copilot/customization/mcp-servers
case 'vscode':
return {
"servers": {
[serverName]: {
"type": "http",
"url": url,
"headers": {
"Authorization": "Bearer [YOUR_AUTH_TOKEN]"
}
}
},
"inputs": [
{
"type": "promptString",
"id": "auth-token",
"description": "Gateway Authentication Token"
}
]
};

// https://cursor.com/docs/context/mcp
case 'cursor':
return {
"mcpServers": {
[serverName]: {
"url": url,
"headers": {
"Authorization": "Bearer [YOUR_AUTH_TOKEN]"
}
}
}
};

// https://docs.cline.bot/mcp/configuring-mcp-servers
case 'cline':
return {
"mcpServers": {
[serverName]: {
"command": "curl",
"args": ["-X", "POST", url, "-H", "Authorization: Bearer [YOUR_AUTH_TOKEN]"],
"env": {
"AUTH_TOKEN": "[YOUR_AUTH_TOKEN]"
},
"alwaysAllow": [],
"disabled": false
}
}
};

// https://docs.windsurf.com/windsurf/cascade/mcp
case 'windsurf':
return {
"mcpServers": {
[serverName]: {
"serverUrl": url
}
}
};

case 'agents':
default:
return {
"mcpServers": {
[serverName]: {
"type": "streamable-http",
"url": url,
"headers": {
"X-Authorization": "Bearer [INGRESS_AUTH_TOKEN]",
"X-User-Pool-Id": "",
"X-Client-Id": "[YOUR_CLIENT_ID]",
"X-Region": "us-east-1"
},
"disabled": false,
"alwaysAllow": []
}
}
};
}
}, [server.name, server.path, selectedIDE]);

// Copy configuration to clipboard
const copyConfigToClipboard = useCallback(async () => {
try {
const config = generateMCPConfig();
const configText = JSON.stringify(config, null, 2);
await navigator.clipboard.writeText(configText);

if (onShowToast) {
onShowToast('Configuration copied to clipboard!', 'success');
}
} catch (error) {
console.error('Failed to copy to clipboard:', error);
if (onShowToast) {
onShowToast('Failed to copy configuration', 'error');
}
}
}, [generateMCPConfig, onShowToast]);

return (
<>
<div className="group bg-white dark:bg-gray-800 rounded-2xl border border-gray-100 dark:border-gray-700 hover:border-gray-200 dark:hover:border-gray-600 shadow-sm hover:shadow-xl transition-all duration-300 h-full flex flex-col">
Expand Down Expand Up @@ -207,6 +324,15 @@ const ServerCard: React.FC<ServerCardProps> = ({ server, onToggle, onEdit, canMo
<PencilIcon className="h-4 w-4" />
</button>
)}

{/* Configuration Generator Button */}
<button
onClick={() => setShowConfig(true)}
className="p-2 text-gray-400 hover:text-green-600 dark:hover:text-green-300 hover:bg-green-50 dark:hover:bg-green-700/50 rounded-lg transition-all duration-200 flex-shrink-0"
title="Copy mcp.json configuration"
>
<CogIcon className="h-4 w-4" />
</button>
</div>

{/* Description */}
Expand Down Expand Up @@ -408,6 +534,187 @@ const ServerCard: React.FC<ServerCardProps> = ({ server, onToggle, onEdit, canMo
</div>
</div>
)}

{/* Configuration Modal */}
{showConfig && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 max-w-3xl w-full mx-4 max-h-[80vh] overflow-auto">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
MCP Configuration for {server.name}
</h3>
<button
onClick={() => setShowConfig(false)}
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
</button>
</div>

<div className="space-y-4">
{/* Instructions */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">
How to use this configuration:
</h4>
<ol className="text-sm text-blue-800 dark:text-blue-200 space-y-1 list-decimal list-inside">
<li>Copy the configuration below</li>
<li>Paste it into your <code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">mcp.json</code> file</li>
<li>Replace <code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">[YOUR_AUTH_TOKEN]</code> with your gateway authentication token</li>
<li>Replace <code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">[YOUR_CLIENT_ID]</code> with your client ID</li>
<li>Restart your AI coding assistant to load the new configuration</li>
</ol>
</div>

{/* Authentication Note */}
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4">
<h4 className="font-medium text-amber-900 dark:text-amber-100 mb-2">
🔐 Authentication Required
</h4>
<p className="text-sm text-amber-800 dark:text-amber-200">
This configuration requires gateway authentication tokens. The tokens authenticate your AI assistant
with the MCP Gateway, not the individual server. Visit the authentication documentation for setup instructions.
</p>
</div>

{/* IDE Selection */}
<div className="bg-gray-50 dark:bg-gray-900 border dark:border-gray-700 rounded-lg p-4">
<h4 className="font-medium text-gray-900 dark:text-white mb-3">
Select your IDE/Tool:
</h4>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedIDE('vscode')}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
selectedIDE === 'vscode'
? 'bg-blue-600 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
}`}
>
VS Code
</button>
<button
onClick={() => setSelectedIDE('cursor')}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
selectedIDE === 'cursor'
? 'bg-blue-600 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
}`}
>
Cursor
</button>
<button
onClick={() => setSelectedIDE('cline')}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
selectedIDE === 'cline'
? 'bg-blue-600 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
}`}
>
Cline
</button>
<button
onClick={() => setSelectedIDE('windsurf')}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
selectedIDE === 'windsurf'
? 'bg-blue-600 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
}`}
>
Windsurf
</button>
<button
onClick={() => setSelectedIDE('agents')}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
selectedIDE === 'agents'
? 'bg-blue-600 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
}`}
>
AI Agents
</button>
</div>
<p className="text-xs text-gray-600 dark:text-gray-400 mt-2">
{selectedIDE === 'agents'
? 'Uses "streamable-http" transport type for AI agent compatibility'
: 'Uses "sse" (Server-Sent Events) transport type for IDE compatibility'
}
</p>
</div>

{/* Configuration JSON */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="font-medium text-gray-900 dark:text-white">
Configuration JSON:
</h4>
<button
onClick={copyConfigToClipboard}
className="flex items-center gap-2 px-3 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors duration-200"
>
<ClipboardDocumentIcon className="h-4 w-4" />
Copy to Clipboard
</button>
</div>

<pre className="p-4 bg-gray-50 dark:bg-gray-900 border dark:border-gray-700 rounded-lg overflow-x-auto text-sm text-gray-900 dark:text-gray-100">
{JSON.stringify(generateMCPConfig(), null, 2)}
</pre>
</div>

{/* Usage Examples */}
<div className="bg-gray-50 dark:bg-gray-900 border dark:border-gray-700 rounded-lg p-4">
<h4 className="font-medium text-gray-900 dark:text-white mb-2">
Configuration for: {
selectedIDE === 'vscode' ? 'VS Code' :
selectedIDE === 'cursor' ? 'Cursor' :
selectedIDE === 'cline' ? 'Cline' :
selectedIDE === 'windsurf' ? 'Windsurf' :
'AI Agents'
}
</h4>
<div className="flex flex-wrap gap-2">
<span className={`px-2 py-1 rounded text-sm ${
selectedIDE === 'vscode'
? 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200'
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400'
}`}>
VS Code {selectedIDE === 'vscode' ? '(Selected)' : ''}
</span>
<span className={`px-2 py-1 rounded text-sm ${
selectedIDE === 'cursor'
? 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200'
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400'
}`}>
Cursor {selectedIDE === 'cursor' ? '(Selected)' : ''}
</span>
<span className={`px-2 py-1 rounded text-sm ${
selectedIDE === 'cline'
? 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200'
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400'
}`}>
Cline {selectedIDE === 'cline' ? '(Selected)' : ''}
</span>
<span className={`px-2 py-1 rounded text-sm ${
selectedIDE === 'windsurf'
? 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200'
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400'
}`}>
Windsurf {selectedIDE === 'windsurf' ? '(Selected)' : ''}
</span>
<span className={`px-2 py-1 rounded text-sm ${
selectedIDE === 'agents'
? 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200'
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400'
}`}>
AI Agents {selectedIDE === 'agents' ? '(Selected)' : ''}
</span>
</div>
</div>
</div>
</div>
</div>
)}
</>
);
};
Expand Down
Loading