housekeeping

This commit is contained in:
Ramnique Singh 2025-07-23 17:01:02 +05:30
parent 775a64c5a8
commit a298036b4b
77 changed files with 2 additions and 14090 deletions

View file

@ -1,175 +0,0 @@
'use client';
import { useState } from 'react';
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Spinner } from "@heroui/react";
import { Button } from "@/components/ui/button";
import { Key, AlertCircle, Eye, EyeOff } from "lucide-react";
import { setServerAuthToken } from '@/app/actions/klavis_actions';
import { MCPServer } from '@/app/lib/types/types';
import { z } from 'zod';
type McpServerType = z.infer<typeof MCPServer>;
interface AuthTokenModalProps {
isOpen: boolean;
onClose: () => void;
server: McpServerType | null;
onSuccess: () => void;
}
export function AuthTokenModal({ isOpen, onClose, server, onSuccess }: AuthTokenModalProps) {
const [authToken, setAuthToken] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showToken, setShowToken] = useState(false);
const handleSubmit = async () => {
if (!server?.instanceId || !authToken.trim()) {
setError('Please enter a valid auth token');
return;
}
setIsSubmitting(true);
setError(null);
try {
const result = await setServerAuthToken(server.instanceId, authToken.trim());
if (result.success) {
// Success - close modal and refresh data
setAuthToken('');
setError(null);
onSuccess();
onClose();
} else {
// Show validation error
setError(result.error || 'Failed to set auth token');
}
} catch (err) {
setError('Network error. Please check your connection and try again.');
} finally {
setIsSubmitting(false);
}
};
const handleClose = () => {
setAuthToken('');
setError(null);
onClose();
};
if (!server) return null;
return (
<Modal
isOpen={isOpen}
onOpenChange={handleClose}
size="lg"
classNames={{
base: "bg-white dark:bg-gray-900",
header: "border-b border-gray-200 dark:border-gray-800",
footer: "border-t border-gray-200 dark:border-gray-800",
}}
>
<ModalContent>
<ModalHeader className="flex gap-2 items-center">
<Key className="w-5 h-5 text-blue-500" />
<span>Authenticate {server.name}</span>
</ModalHeader>
<ModalBody>
<div className="space-y-4">
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-800 rounded-lg p-3">
<p className="text-sm text-blue-700 dark:text-blue-300">
You&apos;ll need to obtain an authentication token from {server.name}. Please refer to their documentation or settings page to find your API key or access token.
</p>
</div>
<div className="space-y-2">
<label htmlFor="auth-token" className="text-sm font-medium text-gray-700 dark:text-gray-300">
Auth Token
</label>
<div className="relative">
<input
id="auth-token"
type={showToken ? 'text' : 'password'}
placeholder="Enter your auth token..."
value={authToken}
onChange={(e) => setAuthToken(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !isSubmitting) {
handleSubmit();
}
}}
className="w-full pr-10 pl-3 py-2 rounded-lg bg-gray-100 dark:bg-gray-800 text-base text-gray-900 dark:text-gray-100 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-900 border-0 shadow-none"
disabled={isSubmitting}
autoComplete="off"
/>
<button
type="button"
tabIndex={-1}
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 p-1"
onClick={() => setShowToken((v) => !v)}
aria-label={showToken ? 'Hide token' : 'Show token'}
>
{showToken ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
{error && (
<div className="flex gap-2 items-start p-3 bg-red-50 dark:bg-red-900/20 border border-red-100 dark:border-red-800 rounded-lg">
<AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
<p className="text-sm text-red-700 dark:text-red-300">{error}</p>
</div>
)}
</div>
<style jsx>{`
#auth-token {
box-shadow: none !important;
outline: none !important;
border: none !important;
background: #f3f4f6 !important;
font-size: 1.05rem;
}
#auth-token:focus {
box-shadow: none !important;
outline: none !important;
border: none !important;
background: #e0e7ef !important;
}
.dark #auth-token {
background: #23272f !important;
color: #f3f4f6 !important;
}
.dark #auth-token:focus {
background: #1a1d23 !important;
}
`}</style>
</ModalBody>
<ModalFooter>
<Button
variant="secondary"
onClick={handleClose}
disabled={isSubmitting}
>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={isSubmitting || !authToken.trim()}
isLoading={isSubmitting}
>
{isSubmitting ? (
<>
<Spinner size="sm" className="mr-2" />
Authenticating...
</>
) : (
'Authenticate'
)}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}

View file

@ -1,786 +0,0 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useParams } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { Info, RefreshCw, Search, AlertTriangle } from 'lucide-react';
import { clsx } from 'clsx';
import {
listAvailableMcpServers,
enableServer,
updateProjectServers,
generateServerAuthUrl,
syncServerTools
} from '@/app/actions/klavis_actions';
import { toggleMcpTool, fetchMcpToolsForServer } from '@/app/actions/mcp_actions';
import { z } from 'zod';
import { MCPServer } from '@/app/lib/types/types';
import { Checkbox } from '@heroui/react';
import {
ServerCard,
ToolManagementPanel,
} from './MCPServersCommon';
import { BillingUpgradeModal } from '@/components/common/billing-upgrade-modal';
import { AuthTokenModal } from './AuthTokenModal';
import { SERVER_URL_PARAMS } from '@/app/lib/constants/klavis';
type McpServerType = z.infer<typeof MCPServer>;
type McpToolType = z.infer<typeof MCPServer>['tools'][number];
function sortServers(servers: McpServerType[]): McpServerType[] {
return [...servers].sort((a, b) => a.name.localeCompare(b.name));
}
const fadeInAnimation = {
'@keyframes fadeIn': {
'0%': { opacity: 0, transform: 'translateY(-5px)' },
'100%': { opacity: 1, transform: 'translateY(0)' }
},
'.animate-fadeIn': {
animation: 'fadeIn 0.2s ease-out'
}
} as const;
const toolCardStyles = {
base: clsx(
"group p-4 rounded-lg transition-all duration-200",
"bg-gray-50/50 dark:bg-gray-800/50",
"hover:bg-gray-100/50 dark:hover:bg-gray-700/50",
"border border-transparent",
"hover:border-gray-200 dark:hover:border-gray-600"
),
};
const ToolCard = ({
tool,
server,
isSelected,
onSelect,
showCheckbox = false
}: {
tool: McpToolType;
server: McpServerType;
isSelected?: boolean;
onSelect?: (selected: boolean) => void;
showCheckbox?: boolean;
}) => {
return (
<div className={toolCardStyles.base}>
<div className="flex items-start gap-3">
{showCheckbox && (
<Checkbox
isSelected={isSelected}
onValueChange={onSelect}
size="sm"
/>
)}
<div className="flex-1">
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1">
{tool.name}
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
{tool.description}
</p>
</div>
</div>
</div>
);
};
const ErrorBanner = ({ onRetry }: { onRetry: () => void }) => (
<div className="mb-6 bg-red-50 dark:bg-red-900/20 border border-red-100 dark:border-red-800 rounded-lg p-4">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<AlertTriangle className="h-5 w-5 text-red-600 dark:text-red-400" />
<p className="text-sm text-red-700 dark:text-red-300">
Unable to load hosted tools. Please check your connection and try again. If the problem persists, contact us on <a href={DISCORD_LINK} target="_blank" rel="noopener noreferrer" className="underline hover:text-red-600 dark:hover:text-red-300">Discord</a>.
</p>
</div>
<Button
size="sm"
variant="secondary"
onClick={onRetry}
className="shrink-0"
>
<RefreshCw className="h-4 w-4 mr-2" />
Retry
</Button>
</div>
</div>
);
const ERROR_MESSAGE = {
NO_HOSTED_TOOLS: 'No hosted tools found. Make sure to set your <a href="https://www.klavis.ai/" target="_blank" rel="noopener noreferrer" class="underline hover:text-red-600 dark:hover:text-red-300">Klavis</a> API key. Contact us on <a href="https://discord.com/invite/rxB8pzHxaS" target="_blank" rel="noopener noreferrer" class="underline hover:text-red-600 dark:hover:text-red-300">discord</a> if you\'re still unable to see hosted tools.'
};
const DISCORD_LINK = 'https://discord.com/invite/rxB8pzHxaS';
const DOCS_LINK = 'https://docs.rowboatlabs.com/add_tools/';
type HostedServersProps = {
onSwitchTab?: (tab: string) => void;
};
export function HostedServers({ onSwitchTab }: HostedServersProps) {
const params = useParams();
const projectId = typeof params.projectId === 'string' ? params.projectId : params.projectId?.[0];
if (!projectId) throw new Error('Project ID is required');
const [servers, setServers] = useState<McpServerType[]>([]);
const [selectedServer, setSelectedServer] = useState<McpServerType | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [showOnlyEnabled, setShowOnlyEnabled] = useState(false);
const [showOnlyReady, setShowOnlyReady] = useState(false);
const [toggleError, setToggleError] = useState<{serverId: string; message: string} | null>(null);
const [enabledServers, setEnabledServers] = useState<Set<string>>(new Set());
const [togglingServers, setTogglingServers] = useState<Set<string>>(new Set());
const [serverOperations, setServerOperations] = useState<Map<string, 'setup' | 'delete' | 'checking-auth'>>(new Map());
const [selectedTools, setSelectedTools] = useState<Set<string>>(new Set());
const [hasToolChanges, setHasToolChanges] = useState(false);
const [savingTools, setSavingTools] = useState(false);
const [serverToolCounts, setServerToolCounts] = useState<Map<string, number>>(new Map());
const [syncingServers, setSyncingServers] = useState<Set<string>>(new Set());
const [billingError, setBillingError] = useState<string | null>(null);
const [showAuthTokenModal, setShowAuthTokenModal] = useState(false);
const [selectedServerForAuth, setSelectedServerForAuth] = useState<McpServerType | null>(null);
const fetchServers = useCallback(async () => {
try {
setLoading(true);
const response = await listAvailableMcpServers(projectId || "");
if (response.error || !response.data) {
setError(ERROR_MESSAGE.NO_HOSTED_TOOLS);
return;
}
// Mark all servers as hosted type
const serversWithType = response.data.map(server => ({
...server,
serverType: 'hosted' as const
}));
setServers(serversWithType);
setError(null);
} catch (err: any) {
setError(ERROR_MESSAGE.NO_HOSTED_TOOLS);
console.error('Error fetching servers:', err);
setServers([]);
} finally {
setLoading(false);
}
}, [projectId]);
useEffect(() => {
fetchServers();
}, [fetchServers]);
// Initialize enabled servers on load and keep it updated
useEffect(() => {
if (servers) {
console.log('Updating enabled servers from server data:', servers);
const enabled = new Set(
servers
.filter(server => server.isActive)
.map(server => server.name)
);
console.log('New enabled servers state:', Array.from(enabled));
setEnabledServers(enabled);
}
}, [servers]);
// Initialize tool counts when servers are loaded
useEffect(() => {
const newCounts = new Map<string, number>();
servers.forEach(server => {
if (isServerEligible(server)) {
newCounts.set(server.name, server.tools.length);
}
});
setServerToolCounts(newCounts);
}, [servers]);
// Initialize selected tools when opening the panel
useEffect(() => {
if (selectedServer) {
setSelectedTools(new Set(selectedServer.tools.map(t => t.id)));
setHasToolChanges(false);
}
}, [selectedServer]);
const isServerEligible = (server: McpServerType) => {
return server.isActive && (!server.authNeeded || server.isAuthenticated);
};
const handleToggleTool = async (server: McpServerType) => {
try {
const serverKey = server.name;
const isCurrentlyEnabled = enabledServers.has(serverKey);
const newState = !isCurrentlyEnabled;
// Immediately update UI state
setServers(prevServers => {
return prevServers.map(s => {
if (s.name === serverKey) {
return {
...s,
isActive: newState,
// If turning off, reset these states
...(newState ? {} : {
serverUrl: undefined,
tools: [],
isAuthenticated: false
})
};
}
return s;
});
});
setTogglingServers(prev => {
const next = new Set(prev);
next.add(serverKey);
return next;
});
setToggleError(null);
setBillingError(null);
setServerOperations(prev => {
const next = new Map(prev);
next.set(serverKey, newState ? 'setup' : 'delete');
return next;
});
try {
const result = await enableServer(server.name, projectId || "", newState);
// Check for billing error
if ('billingError' in result) {
setBillingError(result.billingError);
// Revert UI state
setServers(prevServers => {
return prevServers.map(s => {
if (s.name === serverKey) {
return {
...s,
isActive: isCurrentlyEnabled
};
}
return s;
});
});
return;
}
setEnabledServers(prev => {
const next = new Set(prev);
if (!newState) {
next.delete(serverKey);
} else if ('instanceId' in result) {
next.add(serverKey);
}
return next;
});
if (newState) {
const response = await listAvailableMcpServers(projectId || "");
if (response.data) {
const updatedServer = response.data.find(s => s.name === serverKey);
if (updatedServer) {
setServers(prevServers => {
return prevServers.map(s => {
if (s.name === serverKey) {
return { ...updatedServer, serverType: 'hosted' as const };
}
return s;
});
});
setServerToolCounts(prev => {
const next = new Map(prev);
next.set(serverKey, updatedServer.tools.length);
return next;
});
}
}
} else {
setServerToolCounts(prev => {
const next = new Map(prev);
next.set(serverKey, 0);
return next;
});
}
} catch (err) {
console.error('Toggle failed:', { server: serverKey, error: err });
// Revert the UI state on error
setServers(prevServers => {
return prevServers.map(s => {
if (s.name === serverKey) {
return {
...s,
isActive: isCurrentlyEnabled,
// Restore previous state if the toggle failed
...(isCurrentlyEnabled ? {} : {
serverUrl: undefined,
tools: [],
isAuthenticated: false
})
};
}
return s;
});
});
setEnabledServers(prev => {
const next = new Set(prev);
if (newState) {
next.delete(serverKey);
} else {
next.add(serverKey);
}
return next;
});
setToggleError({
serverId: serverKey,
message: "We're having trouble setting up this server. Please reach out on <a href=\"" + DISCORD_LINK + "\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"underline hover:text-red-600 dark:hover:text-red-300\">discord</a>."
});
}
} finally {
const serverKey = server.name;
setTogglingServers(prev => {
const next = new Set(prev);
next.delete(serverKey);
return next;
});
setServerOperations(prev => {
const next = new Map(prev);
next.delete(serverKey);
return next;
});
}
};
const handleAuthenticate = async (server: McpServerType) => {
try {
if (!server.instanceId) {
throw new Error('Server instance ID not found');
}
// Check if this server uses OAuth (in SERVER_URL_PARAMS) or auth token
const usesOAuth = SERVER_URL_PARAMS[server.name];
if (usesOAuth) {
// Use existing OAuth flow
const authUrl = await generateServerAuthUrl(server.name, projectId, server.instanceId);
const authWindow = window.open(
authUrl,
'_blank',
'width=600,height=700'
);
if (authWindow) {
const checkInterval = setInterval(async () => {
if (authWindow.closed) {
clearInterval(checkInterval);
try {
setServerOperations(prev => {
const next = new Map(prev);
next.set(server.name, 'checking-auth');
return next;
});
await updateProjectServers(projectId, server.name);
const response = await listAvailableMcpServers(projectId);
if (response.data) {
const updatedServer = response.data.find(us => us.name === server.name);
if (updatedServer) {
setServers(prevServers => {
return prevServers.map(s => {
if (s.name === server.name) {
return { ...updatedServer, serverType: 'hosted' as const };
}
return s;
});
});
if (selectedServer?.name === server.name) {
setSelectedServer({ ...updatedServer, serverType: 'hosted' as const });
}
if (!server.authNeeded || updatedServer.isAuthenticated) {
await handleSyncServer(updatedServer);
}
}
}
} finally {
setServerOperations(prev => {
const next = new Map(prev);
next.delete(server.name);
return next;
});
}
}
}, 500);
} else {
window.alert('Failed to open authentication window. Please check your popup blocker settings.');
}
} else {
// Use auth token modal
setSelectedServerForAuth(server);
setShowAuthTokenModal(true);
}
} catch (error) {
console.error('[Auth] Error initiating authentication:', error);
window.alert('Failed to setup authentication');
}
};
const handleSaveToolSelection = async () => {
if (!selectedServer || !projectId) return;
setSavingTools(true);
try {
const availableTools = selectedServer.availableTools || [];
const previousTools = new Set(selectedServer.tools.map(t => t.id));
const updatedTools = new Set<string>();
for (const tool of availableTools) {
const isSelected = selectedTools.has(tool.id);
await toggleMcpTool(projectId, selectedServer.name, tool.id, isSelected);
if (isSelected) {
updatedTools.add(tool.id);
}
}
setServers(prevServers => {
return prevServers.map(s => {
if (s.name === selectedServer.name) {
return {
...s,
tools: availableTools.filter(tool => selectedTools.has(tool.id))
};
}
return s;
});
});
setSelectedServer(prev => {
if (!prev) return null;
return {
...prev,
tools: availableTools.filter(tool => selectedTools.has(tool.id))
};
});
setServerToolCounts(prev => {
const next = new Map(prev);
next.set(selectedServer.name, selectedTools.size);
return next;
});
setHasToolChanges(false);
} catch (error) {
console.error('Error saving tool selection:', error);
} finally {
setSavingTools(false);
}
};
const handleSyncServer = async (server: McpServerType) => {
if (!projectId || !isServerEligible(server)) return;
try {
setSyncingServers(prev => {
const next = new Set(prev);
next.add(server.name);
return next;
});
// Call the server action to sync and update DB
await syncServerTools(projectId, server.name);
// Refresh the server list to get updated data
const response = await listAvailableMcpServers(projectId);
if (response.data) {
const updatedServer = response.data.find(s => s.name === server.name);
if (updatedServer) {
setServers(prevServers => {
return prevServers.map(s => {
if (s.name === server.name) {
return { ...updatedServer, serverType: 'hosted' as const };
}
return s;
});
});
if (selectedServer?.name === server.name) {
setSelectedServer({ ...updatedServer, serverType: 'hosted' as const });
}
}
}
} finally {
setSyncingServers(prev => {
const next = new Set(prev);
next.delete(server.name);
return next;
});
}
};
const handleAuthTokenSuccess = async () => {
if (!selectedServerForAuth) return;
try {
setServerOperations(prev => {
const next = new Map(prev);
next.set(selectedServerForAuth.name, 'checking-auth');
return next;
});
await updateProjectServers(projectId, selectedServerForAuth.name);
const response = await listAvailableMcpServers(projectId);
if (response.data) {
const updatedServer = response.data.find(us => us.name === selectedServerForAuth.name);
if (updatedServer) {
setServers(prevServers => {
return prevServers.map(s => {
if (s.name === selectedServerForAuth.name) {
return { ...updatedServer, serverType: 'hosted' as const };
}
return s;
});
});
if (selectedServer?.name === selectedServerForAuth.name) {
setSelectedServer({ ...updatedServer, serverType: 'hosted' as const });
}
if (!selectedServerForAuth.authNeeded || updatedServer.isAuthenticated) {
await handleSyncServer(updatedServer);
}
}
}
} finally {
setServerOperations(prev => {
const next = new Map(prev);
next.delete(selectedServerForAuth.name);
return next;
});
}
};
const filteredServers = sortServers(servers.filter(server => {
const searchLower = searchQuery.toLowerCase();
const serverTools = server.tools || [];
// Search text filter
const matchesSearch =
server.name.toLowerCase().includes(searchLower) ||
server.description.toLowerCase().includes(searchLower) ||
serverTools.some(tool =>
tool.name.toLowerCase().includes(searchLower) ||
tool.description.toLowerCase().includes(searchLower)
);
// Enabled servers filter
const matchesEnabled = !showOnlyEnabled || server.isActive;
// Ready to use filter (server is active and either doesn't need auth or is already authenticated)
const isReady = server.isActive && (!server.authNeeded || server.isAuthenticated);
const matchesReady = !showOnlyReady || isReady;
return matchesSearch && matchesEnabled && matchesReady;
}));
if (loading) {
return (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-800 dark:border-gray-200 mx-auto"></div>
<p className="mt-4 text-sm text-gray-600 dark:text-gray-400">Loading tools...</p>
</div>
);
}
if (error) {
return (
<div className="flex flex-col items-center justify-center h-[50vh] space-y-6 px-4">
<p
className="text-center text-red-500 dark:text-red-400 max-w-[600px]"
dangerouslySetInnerHTML={{
__html: error
}}
/>
<div className="flex flex-col sm:flex-row gap-4">
<a href={DOCS_LINK} target="_blank" rel="noopener noreferrer">
<Button variant="secondary" className="w-full sm:w-auto">
Read our documentation
</Button>
</a>
<Button
variant="secondary"
onClick={() => onSwitchTab?.('custom')}
className="w-full sm:w-auto"
>
Set up a custom server instead
</Button>
</div>
</div>
);
}
return (
<div className="space-y-6">
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-800 rounded-lg p-4">
<div className="flex gap-3">
<div className="shrink-0">
<Info className="h-5 w-5 text-blue-600 dark:text-blue-400" />
</div>
<p className="text-sm text-blue-700 dark:text-blue-300">
To make hosted MCP tools available to agents in the Build view, first toggle the servers ON here. Some tools may require authentication after enabling.
</p>
</div>
</div>
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between gap-4">
<div className="flex-1 flex items-center gap-4">
<div className="relative flex-1">
<div className="absolute inset-y-0 left-2 flex items-center pointer-events-none">
<Search className="h-4 w-4 text-gray-400 dark:text-gray-500" />
</div>
<input
type="text"
placeholder="Search servers or tools..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-8 pr-4 py-2 text-sm border border-gray-200 dark:border-gray-700 rounded-md
bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100
placeholder-gray-400 dark:placeholder-gray-500
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400
hover:border-gray-300 dark:hover:border-gray-600 transition-colors"
/>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
{filteredServers.length} {filteredServers.length === 1 ? 'server' : 'servers'} {
filteredServers.reduce((total, server) => total + (server.availableTools?.length || 0), 0)
} tools
</div>
<div className="h-4 w-px bg-gray-200 dark:bg-gray-700" />
<div className="flex items-center gap-8">
<div className="group relative flex items-center gap-1">
<label className="flex items-center gap-1 text-sm text-gray-700 dark:text-gray-300">
<Checkbox
isSelected={showOnlyEnabled}
onValueChange={setShowOnlyEnabled}
size="sm"
/>
Enabled Only
</label>
<div className="relative">
<Info className="h-3.5 w-3.5 text-gray-400 dark:text-gray-500 cursor-help ml-1" />
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 dark:bg-gray-800 text-white text-xs rounded-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 whitespace-nowrap shadow-lg">
Shows only servers that are currently toggled ON
</div>
</div>
</div>
<div className="group relative flex items-center gap-1">
<label className="flex items-center gap-1 text-sm text-gray-700 dark:text-gray-300">
<Checkbox
isSelected={showOnlyReady}
onValueChange={setShowOnlyReady}
size="sm"
/>
Ready to Use
</label>
<div className="relative">
<Info className="h-3.5 w-3.5 text-gray-400 dark:text-gray-500 cursor-help ml-1" />
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 dark:bg-gray-800 text-white text-xs rounded-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 whitespace-nowrap shadow-lg">
Shows only servers that are enabled and fully authenticated
</div>
</div>
</div>
</div>
<div className="h-4 w-px bg-gray-200 dark:bg-gray-700" />
</div>
<Button
size="sm"
variant="secondary"
onClick={fetchServers}
disabled={loading}
>
<div className="inline-flex items-center">
<RefreshCw className={clsx("h-4 w-4", loading && "animate-spin")} />
<span className="ml-2">Refresh</span>
</div>
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredServers.map((server) => (
<ServerCard
key={server.instanceId}
server={server}
onToggle={() => handleToggleTool(server)}
onManageTools={() => setSelectedServer(server)}
onSync={() => handleSyncServer(server)}
onAuth={() => handleAuthenticate(server)}
isToggling={togglingServers.has(server.name)}
isSyncing={syncingServers.has(server.name)}
operation={serverOperations.get(server.name)}
error={toggleError?.serverId === server.name ? toggleError : undefined}
showAuth={true}
/>
))}
</div>
<ToolManagementPanel
server={selectedServer}
onClose={() => {
setSelectedServer(null);
setSelectedTools(new Set());
setHasToolChanges(false);
}}
selectedTools={selectedTools}
onToolSelectionChange={(toolId, selected) => {
setSelectedTools(prev => {
const next = new Set(prev);
if (selected) {
next.add(toolId);
} else {
next.delete(toolId);
}
setHasToolChanges(true);
return next;
});
}}
onSaveTools={handleSaveToolSelection}
onSyncTools={selectedServer ? () => handleSyncServer(selectedServer) : undefined}
hasChanges={hasToolChanges}
isSaving={savingTools}
isSyncing={selectedServer ? syncingServers.has(selectedServer.name) : false}
/>
<BillingUpgradeModal
isOpen={!!billingError}
onClose={() => setBillingError(null)}
errorMessage={billingError || ''}
/>
<AuthTokenModal
isOpen={showAuthTokenModal}
onClose={() => {
setShowAuthTokenModal(false);
setSelectedServerForAuth(null);
}}
server={selectedServerForAuth}
onSuccess={handleAuthTokenSuccess}
/>
</div>
);
}