Merge pull request #650 from escapade-mckv/mcp-polish

polish(ui): cyhange mcp copywrite + get all mcp servers from smithery
This commit is contained in:
Bobbie 2025-06-06 01:47:16 +05:30 committed by GitHub
commit 03fad2440a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 141 additions and 29 deletions

View File

@ -69,6 +69,7 @@ class PopularServersV2Response(BaseModel):
categorized: Dict[str, List[Dict[str, Any]]] categorized: Dict[str, List[Dict[str, Any]]]
total: int total: int
categoryCount: int categoryCount: int
pagination: Dict[str, int]
class CustomMCPConnectionRequest(BaseModel): class CustomMCPConnectionRequest(BaseModel):
"""Request model for connecting to a custom MCP server""" """Request model for connecting to a custom MCP server"""
@ -293,6 +294,8 @@ async def get_popular_mcp_servers(
@router.get("/mcp/popular-servers/v2", response_model=PopularServersV2Response) @router.get("/mcp/popular-servers/v2", response_model=PopularServersV2Response)
async def get_popular_mcp_servers_v2( async def get_popular_mcp_servers_v2(
page: int = Query(1, ge=1, description="Page number"),
pageSize: int = Query(200, ge=1, le=500, description="Items per page (max 500 for comprehensive categorization)"),
user_id: str = Depends(get_current_user_id_from_jwt) user_id: str = Depends(get_current_user_id_from_jwt)
): ):
""" """
@ -300,6 +303,10 @@ async def get_popular_mcp_servers_v2(
Returns servers grouped by category with proper metadata and usage statistics. Returns servers grouped by category with proper metadata and usage statistics.
This endpoint fetches real data from the Smithery registry API and categorizes it. This endpoint fetches real data from the Smithery registry API and categorizes it.
Query parameters:
- page: Page number (default: 1)
- pageSize: Number of items per page (default: 200, max: 500)
""" """
logger.info(f"Fetching v2 popular MCP servers for user {user_id}") logger.info(f"Fetching v2 popular MCP servers for user {user_id}")
@ -317,10 +324,10 @@ async def get_popular_mcp_servers_v2(
else: else:
logger.warning("No Smithery API key found in environment variables") logger.warning("No Smithery API key found in environment variables")
# Fetch more servers for better coverage # Use provided pagination parameters
params = { params = {
"page": 1, "page": page,
"pageSize": 100 # Get more servers "pageSize": pageSize
} }
response = await client.get( response = await client.get(
@ -337,11 +344,13 @@ async def get_popular_mcp_servers_v2(
servers=[], servers=[],
categorized={}, categorized={},
total=0, total=0,
categoryCount=0 categoryCount=0,
pagination={"currentPage": page, "pageSize": pageSize, "totalPages": 0, "totalCount": 0}
) )
data = response.json() data = response.json()
servers = data.get("servers", []) servers = data.get("servers", [])
pagination_data = data.get("pagination", {})
# Category mappings based on server types and names # Category mappings based on server types and names
category_mappings = { category_mappings = {
@ -523,8 +532,14 @@ async def get_popular_mcp_servers_v2(
success=True, success=True,
servers=servers, servers=servers,
categorized=sorted_categories, categorized=sorted_categories,
total=len(servers), total=pagination_data.get("totalCount", len(servers)),
categoryCount=len(sorted_categories) categoryCount=len(sorted_categories),
pagination={
"currentPage": pagination_data.get("currentPage", page),
"pageSize": pagination_data.get("pageSize", pageSize),
"totalPages": pagination_data.get("totalPages", 1),
"totalCount": pagination_data.get("totalCount", len(servers))
}
) )
except Exception as e: except Exception as e:
@ -534,6 +549,7 @@ async def get_popular_mcp_servers_v2(
servers=[], servers=[],
categorized={}, categorized={},
total=0, total=0,
categoryCount=0 categoryCount=0,
pagination={"currentPage": page, "pageSize": pageSize, "totalPages": 0, "totalCount": 0}
) )

View File

@ -6,7 +6,7 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { Loader2, Search, Plus, Settings, ExternalLink, Shield, X, Sparkles, ChevronRight } from 'lucide-react'; import { Loader2, Search, Plus, Settings, ExternalLink, Shield, X, Sparkles, ChevronRight, ChevronLeft } from 'lucide-react';
import { usePopularMCPServers, usePopularMCPServersV2, useMCPServers, useMCPServerDetails } from '@/hooks/react-query/mcp/use-mcp-servers'; import { usePopularMCPServers, usePopularMCPServersV2, useMCPServers, useMCPServerDetails } from '@/hooks/react-query/mcp/use-mcp-servers';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@ -194,9 +194,11 @@ export const MCPConfiguration: React.FC<MCPConfigurationProps> = ({
const [configuringServer, setConfiguringServer] = useState<any>(null); const [configuringServer, setConfiguringServer] = useState<any>(null);
const [editingIndex, setEditingIndex] = useState<number | null>(null); const [editingIndex, setEditingIndex] = useState<number | null>(null);
const [selectedCategory, setSelectedCategory] = useState<string | null>(null); const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize] = useState(200);
const { data: popularServers } = usePopularMCPServers(); const { data: popularServers } = usePopularMCPServers();
const { data: popularServersV2, isLoading: isLoadingV2 } = usePopularMCPServersV2(); const { data: popularServersV2, isLoading: isLoadingV2 } = usePopularMCPServersV2(currentPage, pageSize);
const { data: searchResults, isLoading: isSearching } = useMCPServers( const { data: searchResults, isLoading: isSearching } = useMCPServers(
searchQuery.length > 2 ? searchQuery : undefined searchQuery.length > 2 ? searchQuery : undefined
); );
@ -206,6 +208,17 @@ export const MCPConfiguration: React.FC<MCPConfigurationProps> = ({
setEditingIndex(null); setEditingIndex(null);
}; };
React.useEffect(() => {
setCurrentPage(1);
}, [searchQuery]);
React.useEffect(() => {
if (showBrowseDialog) {
setCurrentPage(1);
setSelectedCategory(null);
}
}, [showBrowseDialog]);
const handleEditMCP = (index: number) => { const handleEditMCP = (index: number) => {
const mcp = configuredMCPs[index]; const mcp = configuredMCPs[index];
setConfiguringServer({ setConfiguringServer({
@ -584,6 +597,37 @@ export const MCPConfiguration: React.FC<MCPConfigurationProps> = ({
</ScrollArea> </ScrollArea>
</div> </div>
</div> </div>
{!searchQuery && popularServersV2?.success && popularServersV2.pagination && (
<div className="flex items-center justify-between px-4 py-3 border-t">
<div className="text-sm text-muted-foreground">
Showing {((currentPage - 1) * pageSize) + 1} to {Math.min(currentPage * pageSize, popularServersV2.pagination.totalCount)} of {popularServersV2.pagination.totalCount} servers
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage <= 1}
>
<ChevronLeft className="h-4 w-4" />
Previous
</Button>
<span className="text-sm text-muted-foreground">
Page {currentPage} of {popularServersV2.pagination.totalPages}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => Math.min(popularServersV2.pagination.totalPages, prev + 1))}
disabled={currentPage >= popularServersV2.pagination.totalPages}
>
Next
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
)}
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -1,8 +1,9 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { Search } from 'lucide-react'; import { Button } from '@/components/ui/button';
import { Search, ChevronLeft, ChevronRight } from 'lucide-react';
import { usePopularMCPServers, usePopularMCPServersV2, useMCPServers } from '@/hooks/react-query/mcp/use-mcp-servers'; import { usePopularMCPServers, usePopularMCPServersV2, useMCPServers } from '@/hooks/react-query/mcp/use-mcp-servers';
import { McpServerCard } from './mcp-server-card'; import { McpServerCard } from './mcp-server-card';
import { CategorySidebar } from './category-sidebar'; import { CategorySidebar } from './category-sidebar';
@ -23,15 +24,28 @@ export const BrowseDialog: React.FC<BrowseDialogProps> = ({
}) => { }) => {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string | null>(null); const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize] = useState(200);
const { data: popularServers } = usePopularMCPServers(); const { data: popularServers } = usePopularMCPServers();
const { data: popularServersV2, isLoading: isLoadingV2 } = usePopularMCPServersV2(); const { data: popularServersV2, isLoading: isLoadingV2 } = usePopularMCPServersV2(currentPage, pageSize);
const { data: searchResults, isLoading: isSearching } = useMCPServers( const { data: searchResults, isLoading: isSearching } = useMCPServers(
searchQuery.length > 2 ? searchQuery : undefined searchQuery.length > 2 ? searchQuery : undefined
); );
const categories = popularServersV2?.success ? Object.keys(popularServersV2.categorized) : []; const categories = popularServersV2?.success ? Object.keys(popularServersV2.categorized) : [];
useEffect(() => {
setCurrentPage(1);
}, [searchQuery]);
useEffect(() => {
if (open) {
setCurrentPage(1);
setSelectedCategory(null);
}
}, [open]);
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-6xl max-h-[85vh] overflow-hidden flex flex-col"> <DialogContent className="max-w-6xl max-h-[85vh] overflow-hidden flex flex-col">
@ -41,8 +55,6 @@ export const BrowseDialog: React.FC<BrowseDialogProps> = ({
Discover and add Model Context Protocol servers from Smithery Discover and add Model Context Protocol servers from Smithery
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{/* Search */}
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input <Input
@ -52,7 +64,6 @@ export const BrowseDialog: React.FC<BrowseDialogProps> = ({
className="pl-10" className="pl-10"
/> />
</div> </div>
<div className="flex flex-1 gap-4 overflow-hidden"> <div className="flex flex-1 gap-4 overflow-hidden">
{!searchQuery && categories.length > 0 && ( {!searchQuery && categories.length > 0 && (
<CategorySidebar <CategorySidebar
@ -103,7 +114,39 @@ export const BrowseDialog: React.FC<BrowseDialogProps> = ({
</ScrollArea> </ScrollArea>
</div> </div>
</div> </div>
<DialogFooter className='w-full'>
{!searchQuery && popularServersV2?.success && popularServersV2.pagination && (
<div className="flex items-center justify-between px-4 border-t w-full">
<div className="text-sm text-muted-foreground">
Showing {((currentPage - 1) * pageSize) + 1} to {Math.min(currentPage * pageSize, popularServersV2.pagination.totalCount)} of {popularServersV2.pagination.totalCount} servers
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage <= 1}
>
<ChevronLeft className="h-4 w-4" />
Previous
</Button>
<span className="text-sm text-muted-foreground">
Page {currentPage} of {popularServersV2.pagination.totalPages}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => Math.min(popularServersV2.pagination.totalPages, prev + 1))}
disabled={currentPage >= popularServersV2.pagination.totalPages}
>
Next
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
)}
</DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
}; };

View File

@ -19,7 +19,7 @@ export const CategorySidebar: React.FC<CategorySidebarProps> = ({
categorizedServers, categorizedServers,
}) => { }) => {
return ( return (
<div className="w-76 flex-shrink-0"> <div className="w-68 flex-shrink-0">
<h3 className="text-sm font-semibold mb-3">Categories</h3> <h3 className="text-sm font-semibold mb-3">Categories</h3>
<ScrollArea className="h-full"> <ScrollArea className="h-full">
<div className="space-y-1"> <div className="space-y-1">
@ -48,9 +48,6 @@ export const CategorySidebar: React.FC<CategorySidebarProps> = ({
> >
<span>{categoryIcons[category] || "🧩"}</span> <span>{categoryIcons[category] || "🧩"}</span>
<span className="flex-1 text-left">{category}</span> <span className="flex-1 text-left">{category}</span>
<Badge variant="outline" className="ml-auto text-xs">
{count}
</Badge>
</Button> </Button>
); );
})} })}

View File

@ -2,6 +2,7 @@ import React from 'react';
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Shield, ExternalLink, ChevronRight, Sparkles } from 'lucide-react'; import { Shield, ExternalLink, ChevronRight, Sparkles } from 'lucide-react';
import { truncateString } from '@/lib/utils';
interface McpServerCardProps { interface McpServerCardProps {
server: any; server: any;
@ -14,7 +15,7 @@ export const McpServerCard: React.FC<McpServerCardProps> = ({ server, onClick })
className="p-4 cursor-pointer hover:bg-muted/50 transition-colors" className="p-4 cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() => onClick(server)} onClick={() => onClick(server)}
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-3 flex-shrink-0">
{server.iconUrl ? ( {server.iconUrl ? (
<img src={server.iconUrl} alt={server.displayName || server.name} className="w-6 h-6 rounded" /> <img src={server.iconUrl} alt={server.displayName || server.name} className="w-6 h-6 rounded" />
) : ( ) : (
@ -22,7 +23,7 @@ export const McpServerCard: React.FC<McpServerCardProps> = ({ server, onClick })
<Sparkles className="h-3 w-3 text-primary" /> <Sparkles className="h-3 w-3 text-primary" />
</div> </div>
)} )}
<div className="flex-1"> <div className="flex-1 w-auto">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h4 className="font-medium text-sm">{server.displayName || server.name}</h4> <h4 className="font-medium text-sm">{server.displayName || server.name}</h4>
{server.security?.scanPassed && ( {server.security?.scanPassed && (
@ -34,8 +35,8 @@ export const McpServerCard: React.FC<McpServerCardProps> = ({ server, onClick })
</Badge> </Badge>
)} )}
</div> </div>
<p className="text-xs text-muted-foreground mt-1 line-clamp-2"> <p className="text-xs text-muted-foreground mt-1 line-clamp-2 whitespace-normal">
{server.description} {truncateString(server.description, 100)}
</p> </p>
<div className="flex items-center gap-4 mt-2 text-xs text-muted-foreground"> <div className="flex items-center gap-4 mt-2 text-xs text-muted-foreground">
<span>Used {server.useCount} times</span> <span>Used {server.useCount} times</span>

View File

@ -374,7 +374,7 @@ export default function AgentConfigurationPage() {
<AccordionTrigger className="hover:no-underline text-sm md:text-base"> <AccordionTrigger className="hover:no-underline text-sm md:text-base">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Sparkles className="h-4 w-4" /> <Sparkles className="h-4 w-4" />
MCP Servers Integrations (via MCP)
<Badge variant='new'>New</Badge> <Badge variant='new'>New</Badge>
</div> </div>
</AccordionTrigger> </AccordionTrigger>

View File

@ -71,6 +71,12 @@ interface PopularServersV2Response {
}>>; }>>;
total: number; total: number;
categoryCount: number; categoryCount: number;
pagination: {
currentPage: number;
pageSize: number;
totalPages: number;
totalCount: number;
};
} }
export const useMCPServers = (query?: string, page: number = 1, pageSize: number = 20) => { export const useMCPServers = (query?: string, page: number = 1, pageSize: number = 20) => {
@ -164,17 +170,22 @@ export const usePopularMCPServers = () => {
}); });
}; };
export const usePopularMCPServersV2 = () => { export const usePopularMCPServersV2 = (page: number = 1, pageSize: number = 200) => {
const supabase = createClient(); const supabase = createClient();
return useQuery({ return useQuery({
queryKey: ['mcp-servers-popular-v2'], queryKey: ['mcp-servers-popular-v2', page, pageSize],
queryFn: async (): Promise<PopularServersV2Response> => { queryFn: async (): Promise<PopularServersV2Response> => {
const { data: { session } } = await supabase.auth.getSession(); const { data: { session } } = await supabase.auth.getSession();
if (!session) throw new Error('No session'); if (!session) throw new Error('No session');
const params = new URLSearchParams({
page: page.toString(),
pageSize: pageSize.toString(),
});
const response = await fetch( const response = await fetch(
`${API_URL}/mcp/popular-servers/v2`, `${API_URL}/mcp/popular-servers/v2?${params}`,
{ {
headers: { headers: {
'Authorization': `Bearer ${session.access_token}`, 'Authorization': `Bearer ${session.access_token}`,