mirror of https://github.com/kortix-ai/suna.git
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:
commit
03fad2440a
|
@ -69,6 +69,7 @@ class PopularServersV2Response(BaseModel):
|
|||
categorized: Dict[str, List[Dict[str, Any]]]
|
||||
total: int
|
||||
categoryCount: int
|
||||
pagination: Dict[str, int]
|
||||
|
||||
class CustomMCPConnectionRequest(BaseModel):
|
||||
"""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)
|
||||
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)
|
||||
):
|
||||
"""
|
||||
|
@ -300,6 +303,10 @@ async def get_popular_mcp_servers_v2(
|
|||
|
||||
Returns servers grouped by category with proper metadata and usage statistics.
|
||||
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}")
|
||||
|
||||
|
@ -317,10 +324,10 @@ async def get_popular_mcp_servers_v2(
|
|||
else:
|
||||
logger.warning("No Smithery API key found in environment variables")
|
||||
|
||||
# Fetch more servers for better coverage
|
||||
# Use provided pagination parameters
|
||||
params = {
|
||||
"page": 1,
|
||||
"pageSize": 100 # Get more servers
|
||||
"page": page,
|
||||
"pageSize": pageSize
|
||||
}
|
||||
|
||||
response = await client.get(
|
||||
|
@ -337,11 +344,13 @@ async def get_popular_mcp_servers_v2(
|
|||
servers=[],
|
||||
categorized={},
|
||||
total=0,
|
||||
categoryCount=0
|
||||
categoryCount=0,
|
||||
pagination={"currentPage": page, "pageSize": pageSize, "totalPages": 0, "totalCount": 0}
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
servers = data.get("servers", [])
|
||||
pagination_data = data.get("pagination", {})
|
||||
|
||||
# Category mappings based on server types and names
|
||||
category_mappings = {
|
||||
|
@ -523,8 +532,14 @@ async def get_popular_mcp_servers_v2(
|
|||
success=True,
|
||||
servers=servers,
|
||||
categorized=sorted_categories,
|
||||
total=len(servers),
|
||||
categoryCount=len(sorted_categories)
|
||||
total=pagination_data.get("totalCount", len(servers)),
|
||||
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:
|
||||
|
@ -534,6 +549,7 @@ async def get_popular_mcp_servers_v2(
|
|||
servers=[],
|
||||
categorized={},
|
||||
total=0,
|
||||
categoryCount=0
|
||||
categoryCount=0,
|
||||
pagination={"currentPage": page, "pageSize": pageSize, "totalPages": 0, "totalCount": 0}
|
||||
)
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
|
|||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
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 { cn } from '@/lib/utils';
|
||||
|
||||
|
@ -194,9 +194,11 @@ export const MCPConfiguration: React.FC<MCPConfigurationProps> = ({
|
|||
const [configuringServer, setConfiguringServer] = useState<any>(null);
|
||||
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize] = useState(200);
|
||||
|
||||
const { data: popularServers } = usePopularMCPServers();
|
||||
const { data: popularServersV2, isLoading: isLoadingV2 } = usePopularMCPServersV2();
|
||||
const { data: popularServersV2, isLoading: isLoadingV2 } = usePopularMCPServersV2(currentPage, pageSize);
|
||||
const { data: searchResults, isLoading: isSearching } = useMCPServers(
|
||||
searchQuery.length > 2 ? searchQuery : undefined
|
||||
);
|
||||
|
@ -206,6 +208,17 @@ export const MCPConfiguration: React.FC<MCPConfigurationProps> = ({
|
|||
setEditingIndex(null);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [searchQuery]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (showBrowseDialog) {
|
||||
setCurrentPage(1);
|
||||
setSelectedCategory(null);
|
||||
}
|
||||
}, [showBrowseDialog]);
|
||||
|
||||
const handleEditMCP = (index: number) => {
|
||||
const mcp = configuredMCPs[index];
|
||||
setConfiguringServer({
|
||||
|
@ -584,6 +597,37 @@ export const MCPConfiguration: React.FC<MCPConfigurationProps> = ({
|
|||
</ScrollArea>
|
||||
</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>
|
||||
</Dialog>
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
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 { McpServerCard } from './mcp-server-card';
|
||||
import { CategorySidebar } from './category-sidebar';
|
||||
|
@ -23,15 +24,28 @@ export const BrowseDialog: React.FC<BrowseDialogProps> = ({
|
|||
}) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize] = useState(200);
|
||||
|
||||
const { data: popularServers } = usePopularMCPServers();
|
||||
const { data: popularServersV2, isLoading: isLoadingV2 } = usePopularMCPServersV2();
|
||||
const { data: popularServersV2, isLoading: isLoadingV2 } = usePopularMCPServersV2(currentPage, pageSize);
|
||||
const { data: searchResults, isLoading: isSearching } = useMCPServers(
|
||||
searchQuery.length > 2 ? searchQuery : undefined
|
||||
);
|
||||
|
||||
const categories = popularServersV2?.success ? Object.keys(popularServersV2.categorized) : [];
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [searchQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setCurrentPage(1);
|
||||
setSelectedCategory(null);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<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
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
|
@ -52,7 +64,6 @@ export const BrowseDialog: React.FC<BrowseDialogProps> = ({
|
|||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 gap-4 overflow-hidden">
|
||||
{!searchQuery && categories.length > 0 && (
|
||||
<CategorySidebar
|
||||
|
@ -103,6 +114,38 @@ export const BrowseDialog: React.FC<BrowseDialogProps> = ({
|
|||
</ScrollArea>
|
||||
</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>
|
||||
</Dialog>
|
||||
);
|
||||
|
|
|
@ -19,7 +19,7 @@ export const CategorySidebar: React.FC<CategorySidebarProps> = ({
|
|||
categorizedServers,
|
||||
}) => {
|
||||
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>
|
||||
<ScrollArea className="h-full">
|
||||
<div className="space-y-1">
|
||||
|
@ -48,9 +48,6 @@ export const CategorySidebar: React.FC<CategorySidebarProps> = ({
|
|||
>
|
||||
<span>{categoryIcons[category] || "🧩"}</span>
|
||||
<span className="flex-1 text-left">{category}</span>
|
||||
<Badge variant="outline" className="ml-auto text-xs">
|
||||
{count}
|
||||
</Badge>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Shield, ExternalLink, ChevronRight, Sparkles } from 'lucide-react';
|
||||
import { truncateString } from '@/lib/utils';
|
||||
|
||||
interface McpServerCardProps {
|
||||
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"
|
||||
onClick={() => onClick(server)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex items-start gap-3 flex-shrink-0">
|
||||
{server.iconUrl ? (
|
||||
<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" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<div className="flex-1 w-auto">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-medium text-sm">{server.displayName || server.name}</h4>
|
||||
{server.security?.scanPassed && (
|
||||
|
@ -34,8 +35,8 @@ export const McpServerCard: React.FC<McpServerCardProps> = ({ server, onClick })
|
|||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">
|
||||
{server.description}
|
||||
<p className="text-xs text-muted-foreground mt-1 line-clamp-2 whitespace-normal">
|
||||
{truncateString(server.description, 100)}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 mt-2 text-xs text-muted-foreground">
|
||||
<span>Used {server.useCount} times</span>
|
||||
|
|
|
@ -374,7 +374,7 @@ export default function AgentConfigurationPage() {
|
|||
<AccordionTrigger className="hover:no-underline text-sm md:text-base">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
MCP Servers
|
||||
Integrations (via MCP)
|
||||
<Badge variant='new'>New</Badge>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
|
|
|
@ -71,6 +71,12 @@ interface PopularServersV2Response {
|
|||
}>>;
|
||||
total: number;
|
||||
categoryCount: number;
|
||||
pagination: {
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
totalCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['mcp-servers-popular-v2'],
|
||||
queryKey: ['mcp-servers-popular-v2', page, pageSize],
|
||||
queryFn: async (): Promise<PopularServersV2Response> => {
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
if (!session) throw new Error('No session');
|
||||
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
pageSize: pageSize.toString(),
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`${API_URL}/mcp/popular-servers/v2`,
|
||||
`${API_URL}/mcp/popular-servers/v2?${params}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${session.access_token}`,
|
||||
|
|
Loading…
Reference in New Issue