MCP Hub - publish/discover MCP Servers within a company (#16857)

* feat: initial commit adding 'public_mcp_servers' endpoint

allow admin to make mcp servers public for AI Hub

* feat: expose public endpoint for getting public mcp's

* feat: initial flow for making MCP servers public via LiteLLM proxy

* fix: fix message on make mcp public

* fix: indicate existing public mcp servers are public, when making mcp servers public

* style: have a public column indicating if mcp server has been made public

* feat: expose new MCP Hub

* feat: include usage examples for mcp hub
This commit is contained in:
Krish Dholakia 2025-11-19 19:06:21 -08:00 committed by GitHub
parent c5c563c302
commit 65ff1eff56
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1576 additions and 20 deletions

View file

@ -388,6 +388,7 @@ disable_add_prefix_to_prompt: bool = (
disable_copilot_system_to_assistant: bool = (
False # If false (default), converts all 'system' role messages to 'assistant' for GitHub Copilot compatibility. Set to true to disable this behavior.
)
public_mcp_servers: Optional[List[str]] = None
public_model_groups: Optional[List[str]] = None
public_agent_groups: Optional[List[str]] = None
public_model_groups_links: Dict[str, str] = {}
@ -1342,7 +1343,9 @@ from .llms.watsonx.completion.transformation import IBMWatsonXAIConfig
from .llms.watsonx.chat.transformation import IBMWatsonXChatConfig
from .llms.watsonx.embed.transformation import IBMWatsonXEmbeddingConfig
from .llms.github_copilot.chat.transformation import GithubCopilotConfig
from .llms.github_copilot.responses.transformation import GithubCopilotResponsesAPIConfig
from .llms.github_copilot.responses.transformation import (
GithubCopilotResponsesAPIConfig,
)
from .llms.nebius.chat.transformation import NebiusConfig
from .llms.wandb.chat.transformation import WandbConfig
from .llms.dashscope.chat.transformation import DashScopeChatConfig

View file

@ -1118,6 +1118,7 @@ SECRET_MANAGER_REFRESH_INTERVAL = int(
)
LITELLM_SETTINGS_SAFE_DB_OVERRIDES = [
"default_internal_user_params",
"public_mcp_servers",
"public_agent_groups",
"public_model_groups",
"public_model_groups_links",

View file

@ -214,7 +214,7 @@ class MCPClient:
raise
except Exception as e:
error_type = type(e).__name__
verbose_logger.error(
verbose_logger.exception(
f"MCP client list_tools failed - "
f"Error Type: {error_type}, "
f"Error: {str(e)}, "

View file

@ -20,6 +20,7 @@ from mcp.types import CallToolRequestParams as MCPCallToolRequestParams
from mcp.types import CallToolResult
from mcp.types import Tool as MCPTool
import litellm
from litellm._logging import verbose_logger
from litellm.exceptions import BlockedPiiEntityError, GuardrailRaisedException
from litellm.experimental_mcp_client.client import MCPClient
@ -385,12 +386,12 @@ class MCPServerManager:
)
# Update tool name to server name mapping (for both prefixed and base names)
self.tool_name_to_mcp_server_name_mapping[
base_tool_name
] = server_prefix
self.tool_name_to_mcp_server_name_mapping[
prefixed_tool_name
] = server_prefix
self.tool_name_to_mcp_server_name_mapping[base_tool_name] = (
server_prefix
)
self.tool_name_to_mcp_server_name_mapping[prefixed_tool_name] = (
server_prefix
)
registered_count += 1
verbose_logger.debug(
@ -1668,11 +1669,16 @@ class MCPServerManager:
return server
return None
def get_mcp_servers_from_ids(self, server_ids: List[str]) -> List[MCPServer]:
servers = []
registry = self.get_registry()
for server in registry.values():
if server.server_id in server_ids:
def get_public_mcp_servers(self) -> List[MCPServer]:
"""
Get the public MCP servers
"""
servers: List[MCPServer] = []
if litellm.public_mcp_servers is None:
return servers
for server_id in litellm.public_mcp_servers:
server = self.get_mcp_server_by_id(server_id)
if server:
servers.append(server)
return servers

View file

@ -35,3 +35,9 @@ agent_list:
litellm_settings:
callbacks: ["prometheus"]
mcp_servers:
# HTTP Streamable Server
deepwiki_mcp_1234:
url: "https://mcp.deepwiki.com/mcp"
server_id: deepwiki_mcp_id

View file

@ -516,6 +516,7 @@ class LiteLLMRoutes(enum.Enum):
"/.well-known/litellm-ui-config",
"/public/model_hub",
"/public/agent_hub",
"/public/mcp_hub",
]
)
@ -1099,6 +1100,10 @@ class LiteLLM_MCPServerTable(LiteLLMPydanticObjectBase):
env: Dict[str, str] = Field(default_factory=dict)
class MakeMCPServersPublicRequest(LiteLLMPydanticObjectBase):
mcp_server_ids: List[str]
class NewUserRequestTeam(LiteLLMPydanticObjectBase):
team_id: str
max_budget_in_team: Optional[float] = None

View file

@ -49,6 +49,7 @@ if MCP_AVAILABLE:
from litellm.proxy._types import (
LiteLLM_MCPServerTable,
LitellmUserRoles,
MakeMCPServersPublicRequest,
NewMCPServerRequest,
SpecialMCPServerName,
UpdateMCPServerRequest,
@ -57,6 +58,7 @@ if MCP_AVAILABLE:
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view
from litellm.proxy.management_helpers.utils import management_endpoint_wrapper
from litellm.types.mcp_server.mcp_server_manager import MCPInfo
def _redact_mcp_credentials(
mcp_server: LiteLLM_MCPServerTable,
@ -292,13 +294,23 @@ if MCP_AVAILABLE:
--header 'Authorization: Bearer your_api_key_here'
```
"""
# Use server manager to get all servers with health and team data
mcp_servers = (
await global_mcp_server_manager.get_all_mcp_servers_with_health_and_teams(
user_api_key_auth=user_api_key_dict
)
)
return _redact_mcp_credentials_list(mcp_servers)
redacted_mcp_servers = _redact_mcp_credentials_list(mcp_servers)
# augment the mcp servers with public status
if litellm.public_mcp_servers is not None:
for server in redacted_mcp_servers:
if server.server_id in litellm.public_mcp_servers:
if server.mcp_info is None:
server.mcp_info = {}
server.mcp_info["is_public"] = True
return redacted_mcp_servers
@router.get(
"/server/{server_id}",
@ -585,3 +597,78 @@ if MCP_AVAILABLE:
pass
return _redact_mcp_credentials(mcp_server_record_updated)
@router.post(
"/make_public",
description="Allows making MCP servers public for AI Hub",
dependencies=[Depends(user_api_key_auth)],
status_code=status.HTTP_202_ACCEPTED,
)
async def make_mcp_servers_public(
request: MakeMCPServersPublicRequest,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Make MCP servers public for AI Hub
"""
try:
# Update the public model groups
import litellm
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
global_mcp_server_manager,
)
from litellm.proxy.proxy_server import proxy_config
# Load existing config
config = await proxy_config.get_config()
# Check if user has admin permissions
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
raise HTTPException(
status_code=403,
detail={
"error": "Only proxy admins can update public mcp servers. Your role={}".format(
user_api_key_dict.user_role
)
},
)
if litellm.public_mcp_servers is None:
litellm.public_mcp_servers = []
for server_id in request.mcp_server_ids:
server = global_mcp_server_manager.get_mcp_server_by_id(
server_id=server_id
)
if server is None:
raise HTTPException(
status_code=404,
detail=f"MCP Server with ID {server_id} not found",
)
litellm.public_mcp_servers = request.mcp_server_ids
# Update config with new settings
if "litellm_settings" not in config or config["litellm_settings"] is None:
config["litellm_settings"] = {}
config["litellm_settings"][
"public_mcp_servers"
] = litellm.public_mcp_servers
# Save the updated config
await proxy_config.save_config(new_config=config)
verbose_proxy_logger.debug(
f"Updated public mcp servers to: {litellm.public_mcp_servers} by user: {user_api_key_dict.user_id}"
)
return {
"message": "Successfully updated public mcp servers",
"public_mcp_servers": litellm.public_mcp_servers,
"updated_by": user_api_key_dict.user_id,
}
except HTTPException:
raise
except Exception as e:
verbose_proxy_logger.exception(f"Error making agent public: {e}")
raise HTTPException(status_code=500, detail=str(e))

View file

@ -8,6 +8,8 @@ from litellm.proxy.public_endpoints.provider_create_metadata import (
get_provider_create_metadata,
)
from litellm.types.agents import AgentCard
from litellm.types.mcp import MCPPublicServer
from litellm.types.mcp_server.mcp_server_manager import MCPServer
from litellm.types.proxy.management_endpoints.model_management_endpoints import (
ModelGroupInfoProxy,
)
@ -68,6 +70,26 @@ async def get_agents():
return agent_card_list
@router.get(
"/public/mcp_hub",
tags=["[beta] MCP", "public"],
dependencies=[Depends(user_api_key_auth)],
response_model=List[MCPPublicServer],
)
async def get_mcp_servers():
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
global_mcp_server_manager,
)
public_mcp_servers = global_mcp_server_manager.get_public_mcp_servers()
return [
MCPPublicServer(
**server.model_dump(),
)
for server in public_mcp_servers
]
@router.get(
"/public/model_hub/info",
tags=["public", "model management"],

View file

@ -54,6 +54,22 @@ MCPAuthType = Optional[
]
class MCPPublicServer(BaseModel):
"""
Safe params for public MCP servers
"""
server_id: str
name: str
alias: Optional[str] = None
server_name: Optional[str] = None
url: Optional[str] = None
transport: MCPTransportType
spec_path: Optional[str] = None
auth_type: Optional[MCPAuthType] = None
mcp_info: Optional[Dict[str, Any]] = None
class MCPCredentials(TypedDict, total=False):
auth_value: Optional[str]
"""

View file

@ -0,0 +1,336 @@
import React, { useState, useEffect } from "react";
import { Modal, Form, Steps, Button, Checkbox } from "antd";
import { Text, Title, Badge } from "@tremor/react";
import { makeMCPPublicCall } from "./networking";
import NotificationsManager from "./molecules/notifications_manager";
import { MCPServerData } from "./mcp_hub_table_columns";
const { Step } = Steps;
interface MakeMCPPublicFormProps {
visible: boolean;
onClose: () => void;
accessToken: string;
mcpHubData: MCPServerData[];
onSuccess: () => void;
}
const MakeMCPPublicForm: React.FC<MakeMCPPublicFormProps> = ({
visible,
onClose,
accessToken,
mcpHubData,
onSuccess,
}) => {
const [currentStep, setCurrentStep] = useState(0);
const [selectedServers, setSelectedServers] = useState<Set<string>>(new Set());
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const handleClose = () => {
setCurrentStep(0);
setSelectedServers(new Set());
form.resetFields();
onClose();
};
const handleNext = () => {
if (currentStep === 0) {
if (selectedServers.size === 0) {
NotificationsManager.fromBackend("Please select at least one MCP server to make public");
return;
}
setCurrentStep(1);
}
};
const handlePrevious = () => {
if (currentStep === 1) {
setCurrentStep(0);
}
};
const handleServerSelection = (serverId: string, checked: boolean) => {
const newSelection = new Set(selectedServers);
if (checked) {
newSelection.add(serverId);
} else {
newSelection.delete(serverId);
}
setSelectedServers(newSelection);
};
const handleSelectAll = (checked: boolean) => {
if (checked) {
const allServerIds = mcpHubData.map((server) => server.server_id);
setSelectedServers(new Set(allServerIds));
} else {
setSelectedServers(new Set());
}
};
// Initialize and preselect already public servers when modal opens
useEffect(() => {
if (visible && mcpHubData.length > 0) {
// Extract server IDs from servers that are already public
const publicServerIds = mcpHubData
.filter((server) => server.mcp_info?.is_public === true)
.map((server) => server.server_id);
// Preselect servers that are already public
setSelectedServers(new Set(publicServerIds));
}
}, [visible]); // Only re-run when modal visibility changes, not when mcpHubData updates
const handleSubmit = async () => {
if (selectedServers.size === 0) {
NotificationsManager.fromBackend("Please select at least one MCP server to make public");
return;
}
setLoading(true);
try {
const serverIdsToMakePublic = Array.from(selectedServers);
// Make batch API call for all servers
await makeMCPPublicCall(accessToken, serverIdsToMakePublic);
NotificationsManager.success(`Successfully made ${serverIdsToMakePublic.length} MCP server(s) public!`);
handleClose();
onSuccess();
} catch (error) {
console.error("Error making MCP servers public:", error);
NotificationsManager.fromBackend("Failed to make MCP servers public. Please try again.");
} finally {
setLoading(false);
}
};
const renderStep1Content = () => {
const allServersSelected =
mcpHubData.length > 0 && mcpHubData.every((server) => selectedServers.has(server.server_id));
const isIndeterminate = selectedServers.size > 0 && !allServersSelected;
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<Title>Select MCP Servers to Make Public</Title>
<div className="flex items-center space-x-2">
<Checkbox
checked={allServersSelected}
indeterminate={isIndeterminate}
onChange={(e) => handleSelectAll(e.target.checked)}
disabled={mcpHubData.length === 0}
>
Select All {mcpHubData.length > 0 && `(${mcpHubData.length})`}
</Checkbox>
</div>
</div>
<Text className="text-sm text-gray-600">
Select the MCP servers you want to be visible on the public model hub. Users will still require a valid API key to
use these servers.
</Text>
<div className="max-h-96 overflow-y-auto border rounded-lg p-4">
<div className="space-y-3">
{mcpHubData.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<Text>No MCP servers available.</Text>
</div>
) : (
mcpHubData.map((server) => {
const isPublic = server.mcp_info?.is_public === true;
return (
<div
key={server.server_id}
className="flex items-center space-x-3 p-3 border rounded-lg hover:bg-gray-50"
>
<Checkbox
checked={selectedServers.has(server.server_id)}
onChange={(e) => handleServerSelection(server.server_id, e.target.checked)}
/>
<div className="flex-1">
<div className="flex items-center space-x-2">
<Text className="font-medium">{server.server_name}</Text>
{isPublic && (
<Badge color="emerald" size="sm">
Public
</Badge>
)}
<Badge color="blue" size="sm">
{server.transport}
</Badge>
<Badge
color={
server.status === "active" || server.status === "healthy"
? "green"
: server.status === "inactive" || server.status === "unhealthy"
? "red"
: "gray"
}
size="sm"
>
{server.status || "unknown"}
</Badge>
</div>
<Text className="text-xs text-gray-600 mt-1">
{server.description || server.url}
</Text>
{server.allowed_tools && server.allowed_tools.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1">
{server.allowed_tools.slice(0, 3).map((tool, idx) => (
<Badge key={idx} color="purple" size="xs">
{tool}
</Badge>
))}
{server.allowed_tools.length > 3 && (
<Text className="text-xs text-gray-500">+{server.allowed_tools.length - 3} more</Text>
)}
</div>
)}
</div>
</div>
);
})
)}
</div>
</div>
{selectedServers.size > 0 && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<Text className="text-sm text-blue-800">
<strong>{selectedServers.size}</strong> MCP server{selectedServers.size !== 1 ? "s" : ""} selected
</Text>
</div>
)}
</div>
);
};
const renderStep2Content = () => {
return (
<div className="space-y-4">
<Title>Confirm Making MCP Servers Public</Title>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<Text className="text-sm text-yellow-800">
<strong>Warning:</strong> Once you make these MCP servers public, anyone who can go to the{" "}
<code>/ui/model_hub_table</code> will be able to know they exist on the proxy.
</Text>
</div>
<div className="space-y-3">
<Text className="font-medium">MCP Servers to be made public:</Text>
<div className="max-h-48 overflow-y-auto border rounded-lg p-3">
<div className="space-y-2">
{Array.from(selectedServers).map((serverId) => {
const server = mcpHubData.find((s) => s.server_id === serverId);
return (
<div key={serverId} className="flex items-center justify-between p-2 bg-gray-50 rounded">
<div className="flex-1">
<div className="flex items-center space-x-2">
<Text className="font-medium">{server?.server_name || serverId}</Text>
{server && (
<>
<Badge color="blue" size="xs">
{server.transport}
</Badge>
<Badge
color={
server.status === "active" || server.status === "healthy"
? "green"
: server.status === "inactive" || server.status === "unhealthy"
? "red"
: "gray"
}
size="xs"
>
{server.status || "unknown"}
</Badge>
</>
)}
</div>
{server?.description && (
<Text className="text-xs text-gray-600 mt-1">{server.description}</Text>
)}
{server?.url && (
<Text className="text-xs text-gray-500 mt-1">{server.url}</Text>
)}
</div>
</div>
);
})}
</div>
</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<Text className="text-sm text-blue-800">
Total: <strong>{selectedServers.size}</strong> MCP server{selectedServers.size !== 1 ? "s" : ""} will be made
public
</Text>
</div>
</div>
);
};
const renderStepContent = () => {
switch (currentStep) {
case 0:
return renderStep1Content();
case 1:
return renderStep2Content();
default:
return null;
}
};
const renderStepButtons = () => {
return (
<div className="flex justify-between mt-6">
<Button onClick={currentStep === 0 ? handleClose : handlePrevious}>
{currentStep === 0 ? "Cancel" : "Previous"}
</Button>
<div className="flex space-x-2">
{currentStep === 0 && (
<Button onClick={handleNext} disabled={selectedServers.size === 0}>
Next
</Button>
)}
{currentStep === 1 && (
<Button onClick={handleSubmit} loading={loading}>
Make Public
</Button>
)}
</div>
</div>
);
};
return (
<Modal
title="Make MCP Servers Public"
open={visible}
onCancel={handleClose}
footer={null}
width={1200}
maskClosable={false}
>
<Form form={form} layout="vertical">
<Steps current={currentStep} className="mb-6">
<Step title="Select Servers" />
<Step title="Confirm" />
</Steps>
{renderStepContent()}
{renderStepButtons()}
</Form>
</Modal>
);
};
export default MakeMCPPublicForm;

View file

@ -0,0 +1,267 @@
import { ColumnDef } from "@tanstack/react-table";
import { Button, Badge, Text } from "@tremor/react";
import { Tooltip, Tag } from "antd";
import { CopyOutlined, InfoCircleOutlined } from "@ant-design/icons";
export interface MCPServerData {
server_id: string;
server_name: string;
alias?: string | null;
description?: string | null;
url: string;
transport: string;
auth_type: string;
credentials?: any;
created_at: string;
created_by: string;
updated_at: string;
updated_by: string;
teams: string[];
mcp_access_groups: string[];
allowed_tools: string[];
extra_headers: any[];
mcp_info: Record<string, any>;
static_headers: Record<string, any>;
status: string;
last_health_check?: string | null;
health_check_error?: string | null;
command?: string | null;
args: string[];
env: Record<string, any>;
[key: string]: any;
}
export const mcpHubColumns = (
showModal: (server: MCPServerData) => void,
copyToClipboard: (text: string) => void,
publicPage: boolean = false,
): ColumnDef<MCPServerData>[] => {
const allColumns: ColumnDef<MCPServerData>[] = [
{
header: "Server Name",
accessorKey: "server_name",
enableSorting: true,
sortingFn: "alphanumeric",
cell: ({ row }) => {
const server = row.original;
return (
<div className="space-y-1">
<div className="flex items-center space-x-2">
<Text className="font-medium text-sm">{server.server_name}</Text>
<Tooltip title="Copy server name">
<CopyOutlined
onClick={() => copyToClipboard(server.server_name)}
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs"
/>
</Tooltip>
</div>
{/* Show description on mobile */}
<div className="md:hidden">
<Text className="text-xs text-gray-600">{server.description || "-"}</Text>
</div>
</div>
);
},
},
{
header: "Description",
accessorKey: "description",
enableSorting: true,
sortingFn: "alphanumeric",
cell: ({ row }) => {
const server = row.original;
return (
<Text className="text-xs line-clamp-2">
{server.description || "-"}
</Text>
);
},
meta: {
className: "hidden md:table-cell",
},
},
{
header: "URL",
accessorKey: "url",
enableSorting: true,
sortingFn: "alphanumeric",
cell: ({ row }) => {
const server = row.original;
return (
<div className="flex items-center space-x-2">
<Text className="text-xs truncate max-w-xs">{server.url}</Text>
<Tooltip title="Copy URL">
<CopyOutlined
onClick={() => copyToClipboard(server.url)}
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs flex-shrink-0"
/>
</Tooltip>
</div>
);
},
meta: {
className: "hidden lg:table-cell",
},
},
{
header: "Transport",
accessorKey: "transport",
enableSorting: true,
sortingFn: "alphanumeric",
cell: ({ row }) => {
const server = row.original;
return (
<Badge color="blue" size="sm">
{server.transport}
</Badge>
);
},
meta: {
className: "hidden md:table-cell",
},
},
{
header: "Auth Type",
accessorKey: "auth_type",
enableSorting: true,
sortingFn: "alphanumeric",
cell: ({ row }) => {
const server = row.original;
const authColor = server.auth_type === "none" ? "gray" : "green";
return (
<Badge color={authColor} size="sm">
{server.auth_type}
</Badge>
);
},
meta: {
className: "hidden md:table-cell",
},
},
{
header: "Status",
accessorKey: "status",
enableSorting: true,
sortingFn: "alphanumeric",
cell: ({ row }) => {
const server = row.original;
const statusColors: Record<string, string> = {
active: "green",
inactive: "red",
unknown: "gray",
healthy: "green",
unhealthy: "red",
};
const color = statusColors[server.status] || "gray";
return (
<Badge color={color} size="sm">
{server.status || "unknown"}
</Badge>
);
},
},
{
header: "Tools",
accessorKey: "allowed_tools",
enableSorting: false,
cell: ({ row }) => {
const server = row.original;
const tools = server.allowed_tools || [];
return (
<div className="space-y-1">
<Text className="text-xs font-medium">
{tools.length > 0 ? `${tools.length} tool${tools.length !== 1 ? "s" : ""}` : "All tools"}
</Text>
{tools.length > 0 && (
<div className="flex flex-wrap gap-1">
{tools.slice(0, 2).map((tool, idx) => (
<Tag key={idx} color="purple" className="text-xs">
{tool}
</Tag>
))}
{tools.length > 2 && (
<Text className="text-xs text-gray-500">+{tools.length - 2}</Text>
)}
</div>
)}
</div>
);
},
meta: {
className: "hidden lg:table-cell",
},
},
{
header: "Created By",
accessorKey: "created_by",
enableSorting: true,
sortingFn: "alphanumeric",
cell: ({ row }) => {
const server = row.original;
return (
<Text className="text-xs">
{server.created_by || "-"}
</Text>
);
},
meta: {
className: "hidden xl:table-cell",
},
},
{
header: "Public",
accessorKey: "mcp_info.is_public",
enableSorting: true,
sortingFn: (rowA, rowB) => {
const publicA = rowA.original.mcp_info?.is_public === true ? 1 : 0;
const publicB = rowB.original.mcp_info?.is_public === true ? 1 : 0;
return publicA - publicB;
},
cell: ({ row }) => {
const server = row.original;
return server.mcp_info?.is_public === true ? (
<Badge color="green" size="xs">
Yes
</Badge>
) : (
<Badge color="gray" size="xs">
No
</Badge>
);
},
meta: {
className: "hidden md:table-cell",
},
},
{
header: "Details",
id: "details",
enableSorting: false,
cell: ({ row }) => {
const server = row.original;
return (
<Button size="xs" variant="secondary" onClick={() => showModal(server)} icon={InfoCircleOutlined}>
<span className="hidden lg:inline">Details</span>
<span className="lg:hidden">Info</span>
</Button>
);
},
},
];
return allColumns;
};

View file

@ -1,13 +1,15 @@
import React, { useEffect, useState, useRef, useCallback } from "react";
import { useRouter } from "next/navigation";
import { modelHubCall, modelHubPublicModelsCall, getAgentsList, getProxyBaseUrl } from "./networking";
import { modelHubCall, modelHubPublicModelsCall, getAgentsList, getProxyBaseUrl, fetchMCPServers } from "./networking";
import { getConfigFieldSetting } from "./networking";
import { ModelDataTable } from "./model_dashboard/table";
import { modelHubColumns } from "./model_hub_table_columns";
import { agentHubColumns, AgentHubData } from "./agent_hub_table_columns";
import { mcpHubColumns, MCPServerData } from "./mcp_hub_table_columns";
import PublicModelHub from "./public_model_hub";
import MakeModelPublicForm from "./make_model_public_form";
import MakeAgentPublicForm from "./make_agent_public_form";
import MakeMCPPublicForm from "./make_mcp_public_form";
import ModelFilters from "./model_filters";
import UsefulLinksManagement from "./useful_links_management";
import { Card, Text, Title, Button, Badge, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
@ -60,9 +62,16 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
const [agentLoading, setAgentLoading] = useState<boolean>(true);
const [selectedAgent, setSelectedAgent] = useState<null | AgentHubData>(null);
const [isAgentModalVisible, setIsAgentModalVisible] = useState(false);
// MCP Hub state
const [mcpHubData, setMcpHubData] = useState<MCPServerData[] | null>(null);
const [mcpLoading, setMcpLoading] = useState<boolean>(true);
const [selectedMcpServer, setSelectedMcpServer] = useState<null | MCPServerData>(null);
const [isMcpModalVisible, setIsMcpModalVisible] = useState(false);
const [isMakeMcpPublicModalVisible, setIsMakeMcpPublicModalVisible] = useState(false);
const router = useRouter();
const tableRef = useRef<TableInstance<any>>(null);
const agentTableRef = useRef<TableInstance<any>>(null);
const mcpTableRef = useRef<TableInstance<any>>(null);
useEffect(() => {
const fetchData = async (accessToken: string) => {
@ -143,6 +152,30 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
}
}, [publicPage, accessToken]);
// Fetch MCP Hub data
useEffect(() => {
const fetchMcpData = async () => {
if (!accessToken) {
return;
}
try {
setMcpLoading(true);
const response = await fetchMCPServers(accessToken);
console.log("MCPHubData:", response);
setMcpHubData(response);
} catch (error) {
console.error("There was an error fetching the MCP server data", error);
} finally {
setMcpLoading(false);
}
};
if (!publicPage) {
fetchMcpData();
}
}, [publicPage, accessToken]);
const showModal = (model: ModelGroupInfo) => {
setSelectedModel(model);
setIsModalVisible(true);
@ -153,6 +186,11 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
setIsAgentModalVisible(true);
};
const showMcpModal = (server: MCPServerData) => {
setSelectedMcpServer(server);
setIsMcpModalVisible(true);
};
const goToPublicModelPage = () => {
router.replace(`/model_hub_table?key=${accessToken}`);
};
@ -175,12 +213,23 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
setIsMakeAgentPublicModalVisible(true);
};
const handleMakeMcpPublicPage = () => {
if (!accessToken) {
return;
}
// Show the modal for selecting MCP servers to make public
setIsMakeMcpPublicModalVisible(true);
};
const handleOk = () => {
setIsModalVisible(false);
setIsPublicPageModalVisible(false);
setSelectedModel(null);
setIsAgentModalVisible(false);
setSelectedAgent(null);
setIsMcpModalVisible(false);
setSelectedMcpServer(null);
};
const handleCancel = () => {
@ -189,6 +238,8 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
setSelectedModel(null);
setIsAgentModalVisible(false);
setSelectedAgent(null);
setIsMcpModalVisible(false);
setSelectedMcpServer(null);
};
const copyToClipboard = (text: string) => {
@ -252,6 +303,21 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
}
};
const handleMakeMcpPublicSuccess = () => {
// Refresh the MCP hub data after successful public operation
if (accessToken) {
const fetchMcpData = async () => {
try {
const response = await fetchMCPServers(accessToken);
setMcpHubData(response);
} catch (error) {
console.error("Error refreshing MCP server data:", error);
}
};
fetchMcpData();
}
};
const handleFilteredDataChange = useCallback((newFilteredData: ModelGroupInfo[]) => {
setFilteredData(newFilteredData);
}, []);
@ -274,7 +340,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
<Title className="text-center">AI Hub</Title>
{isAdminRole(userRole || "") ? (
<p className="text-sm text-gray-600">
Make models and agents public for developers to know what&apos;s available.
Make models, agents, and MCP servers public for developers to know what&apos;s available.
</p>
) : (
<p className="text-sm text-gray-600">A list of all public model names personally available to you.</p>
@ -302,11 +368,12 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
</div>
)}
{/* Tab System for Model Hub and Agent Hub */}
{/* Tab System for Model Hub, Agent Hub, and MCP Hub */}
<TabGroup>
<TabList className="mb-4">
<Tab>Model Hub</Tab>
<Tab>Agent Hub</Tab>
<Tab>MCP Hub</Tab>
</TabList>
<TabPanels>
@ -371,6 +438,35 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
</Text>
</div>
</TabPanel>
{/* MCP Hub Tab */}
<TabPanel>
<Card>
{/* Header with Make Public Button */}
{publicPage == false && isAdminRole(userRole || "") && (
<div className="flex justify-end mb-4">
<Button onClick={() => handleMakeMcpPublicPage()}>
Select MCP Servers to Make Public
</Button>
</div>
)}
{/* MCP Server Table */}
<ModelDataTable
columns={mcpHubColumns(showMcpModal, copyToClipboard, publicPage)}
data={mcpHubData || []}
isLoading={mcpLoading}
table={mcpTableRef}
defaultSorting={[{ id: "server_name", desc: false }]}
/>
</Card>
<div className="mt-4 text-center space-y-2">
<Text className="text-sm text-gray-600">
Showing {mcpHubData?.length || 0} MCP server{mcpHubData?.length !== 1 ? "s" : ""}
</Text>
</div>
</TabPanel>
</TabPanels>
</TabGroup>
</div>
@ -693,6 +789,229 @@ print(response.choices[0].message.content)`}
)}
</Modal>
{/* MCP Server Details Modal */}
<Modal
title={selectedMcpServer?.server_name || "MCP Server Details"}
width={1000}
visible={isMcpModalVisible}
footer={null}
onOk={handleOk}
onCancel={handleCancel}
>
{selectedMcpServer && (
<div className="space-y-6">
{/* Server Overview */}
<div>
<Text className="text-lg font-semibold mb-4">Server Overview</Text>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<Text className="font-medium">Server Name:</Text>
<Text>{selectedMcpServer.server_name}</Text>
</div>
<div>
<Text className="font-medium">Server ID:</Text>
<div className="flex items-center space-x-2">
<Text className="text-xs truncate">{selectedMcpServer.server_id}</Text>
<CopyOutlined
onClick={() => copyToClipboard(selectedMcpServer.server_id)}
className="cursor-pointer text-gray-500 hover:text-blue-500"
/>
</div>
</div>
{selectedMcpServer.alias && (
<div>
<Text className="font-medium">Alias:</Text>
<Text>{selectedMcpServer.alias}</Text>
</div>
)}
<div>
<Text className="font-medium">Transport:</Text>
<Badge color="blue">{selectedMcpServer.transport}</Badge>
</div>
<div>
<Text className="font-medium">Auth Type:</Text>
<Badge color={selectedMcpServer.auth_type === "none" ? "gray" : "green"}>
{selectedMcpServer.auth_type}
</Badge>
</div>
<div>
<Text className="font-medium">Status:</Text>
<Badge
color={
selectedMcpServer.status === "active" || selectedMcpServer.status === "healthy"
? "green"
: selectedMcpServer.status === "inactive" || selectedMcpServer.status === "unhealthy"
? "red"
: "gray"
}
>
{selectedMcpServer.status || "unknown"}
</Badge>
</div>
</div>
{selectedMcpServer.description && (
<div className="mt-2">
<Text className="font-medium">Description:</Text>
<Text className="mt-1">{selectedMcpServer.description}</Text>
</div>
)}
</div>
{/* Connection Details */}
<div>
<Text className="text-lg font-semibold mb-4">Connection Details</Text>
<div className="space-y-2">
<div>
<Text className="font-medium">URL:</Text>
<div className="flex items-center space-x-2 mt-1">
<Text className="text-sm break-all bg-gray-100 p-2 rounded flex-1">
{selectedMcpServer.url}
</Text>
<CopyOutlined
onClick={() => copyToClipboard(selectedMcpServer.url)}
className="cursor-pointer text-gray-500 hover:text-blue-500 flex-shrink-0"
/>
</div>
</div>
{selectedMcpServer.command && (
<div>
<Text className="font-medium">Command:</Text>
<Text className="text-sm bg-gray-100 p-2 rounded mt-1 font-mono">
{selectedMcpServer.command}
</Text>
</div>
)}
</div>
</div>
{/* Tools */}
{selectedMcpServer.allowed_tools && selectedMcpServer.allowed_tools.length > 0 && (
<div>
<Text className="text-lg font-semibold mb-4">Allowed Tools</Text>
<div className="flex flex-wrap gap-2">
{selectedMcpServer.allowed_tools.map((tool, idx) => (
<Badge key={idx} color="purple">
{tool}
</Badge>
))}
</div>
</div>
)}
{/* Teams */}
{selectedMcpServer.teams && selectedMcpServer.teams.length > 0 && (
<div>
<Text className="text-lg font-semibold mb-4">Teams</Text>
<div className="flex flex-wrap gap-2">
{selectedMcpServer.teams.map((team, idx) => (
<Badge key={idx} color="blue">
{team}
</Badge>
))}
</div>
</div>
)}
{/* Access Groups */}
{selectedMcpServer.mcp_access_groups && selectedMcpServer.mcp_access_groups.length > 0 && (
<div>
<Text className="text-lg font-semibold mb-4">Access Groups</Text>
<div className="flex flex-wrap gap-2">
{selectedMcpServer.mcp_access_groups.map((group, idx) => (
<Badge key={idx} color="green">
{group}
</Badge>
))}
</div>
</div>
)}
{/* Metadata */}
<div>
<Text className="text-lg font-semibold mb-4">Metadata</Text>
<div className="grid grid-cols-2 gap-4">
<div>
<Text className="font-medium">Created By:</Text>
<Text>{selectedMcpServer.created_by}</Text>
</div>
<div>
<Text className="font-medium">Updated By:</Text>
<Text>{selectedMcpServer.updated_by}</Text>
</div>
<div>
<Text className="font-medium">Created At:</Text>
<Text className="text-sm">
{new Date(selectedMcpServer.created_at).toLocaleString()}
</Text>
</div>
<div>
<Text className="font-medium">Updated At:</Text>
<Text className="text-sm">
{new Date(selectedMcpServer.updated_at).toLocaleString()}
</Text>
</div>
{selectedMcpServer.last_health_check && (
<div>
<Text className="font-medium">Last Health Check:</Text>
<Text className="text-sm">
{new Date(selectedMcpServer.last_health_check).toLocaleString()}
</Text>
</div>
)}
</div>
{selectedMcpServer.health_check_error && (
<div className="mt-2 p-2 bg-red-50 rounded">
<Text className="font-medium text-red-700">Health Check Error:</Text>
<Text className="text-sm text-red-600 mt-1">
{selectedMcpServer.health_check_error}
</Text>
</div>
)}
</div>
{/* Usage Example */}
<div>
<Text className="text-lg font-semibold mb-4">Usage Example</Text>
<SyntaxHighlighter language="python" className="text-sm">
{`from fastmcp import Client
import asyncio
# Standard MCP configuration
config = {
"mcpServers": {
"${selectedMcpServer.server_name}": {
"url": "http://localhost:4000/${selectedMcpServer.server_name}/mcp",
"headers": {
"x-litellm-api-key": "Bearer sk-1234"
}
}
}
}
# Create a client that connects to the server
client = Client(config)
async def main():
async with client:
# List available tools
tools = await client.list_tools()
print(f"Available tools: {[tool.name for tool in tools]}")
# Call a tool
response = await client.call_tool(
name="tool_name",
arguments={"arg": "value"}
)
print(f"Response: {response}")
if __name__ == "__main__":
asyncio.run(main())`}
</SyntaxHighlighter>
</div>
</div>
)}
</Modal>
{/* Make Model Public Form */}
<MakeModelPublicForm
visible={isMakePublicModalVisible}
@ -710,6 +1029,15 @@ print(response.choices[0].message.content)`}
agentHubData={agentHubData || []}
onSuccess={handleMakeAgentPublicSuccess}
/>
{/* Make MCP Public Form */}
<MakeMCPPublicForm
visible={isMakeMcpPublicModalVisible}
onClose={() => setIsMakeMcpPublicModalVisible(false)}
accessToken={accessToken || ""}
mcpHubData={mcpHubData || []}
onSuccess={handleMakeMcpPublicSuccess}
/>
</div>
);
};

View file

@ -212,11 +212,13 @@ export interface CredentialsResponse {
let lastErrorTime = 0;
const handleError = async (errorData: string) => {
const handleError = async (errorData: string | any) => {
const currentTime = Date.now();
if (currentTime - lastErrorTime > 60000) {
// 60000 milliseconds = 60 seconds
if (errorData.includes("Authentication Error - Expired Key")) {
// Convert errorData to string if it isn't already
const errorString = typeof errorData === 'string' ? errorData : JSON.stringify(errorData);
if (errorString.includes("Authentication Error - Expired Key")) {
NotificationsManager.info("UI Session Expired. Logging out.");
lastErrorTime = currentTime;
clearTokenCookies();
@ -1973,6 +1975,17 @@ export const agentHubPublicModelsCall = async () => {
return response.json();
};
export const mcpHubPublicServersCall = async () => {
const url = proxyBaseUrl ? `${proxyBaseUrl}/public/mcp_hub` : `/public/mcp_hub`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
return response.json();
};
export const modelHubCall = async (accessToken: string) => {
/**
* Get all models on proxy
@ -6592,6 +6605,36 @@ export const makeAgentsPublicCall = async (accessToken: string, agentIds: string
}
};
export const makeMCPPublicCall = async (accessToken: string, mcpServerIds: string[]) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/mcp/make_public` : `/v1/mcp/make_public`;
const response = await fetch(url, {
method: "POST",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
mcp_server_ids: mcpServerIds,
}),
});
if (!response.ok) {
const errorData = await response.text();
handleError(errorData);
throw new Error(errorData);
}
const data = await response.json();
console.log("Make agents public response:", data);
return data;
} catch (error) {
console.error("Failed to make agents public:", error);
throw error;
}
};
export const deleteGuardrailCall = async (accessToken: string, guardrailId: string) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/guardrails/${guardrailId}` : `/guardrails/${guardrailId}`;

View file

@ -1,5 +1,5 @@
import React, { useEffect, useState, useRef, useMemo } from "react";
import { modelHubPublicModelsCall, getPublicModelHubInfo, agentHubPublicModelsCall } from "./networking";
import { modelHubPublicModelsCall, getPublicModelHubInfo, agentHubPublicModelsCall, mcpHubPublicServersCall } from "./networking";
import { ModelDataTable } from "./model_dashboard/table";
import { ColumnDef } from "@tanstack/react-table";
import { Card, Text, Title, Button } from "@tremor/react";
@ -62,6 +62,23 @@ interface AgentCard {
[key: string]: any;
}
interface MCPServerData {
server_id: string;
name: string;
alias?: string | null;
server_name: string;
url: string;
transport: string;
spec_path?: string | null;
auth_type: string;
mcp_info: {
server_name: string;
description?: string;
mcp_server_cost_info?: any;
};
[key: string]: any;
}
interface PublicModelHubProps {
accessToken?: string | null;
isEmbedded?: boolean; // When true, hides navbar and adjusts layout for embedding in dashboard
@ -70,27 +87,34 @@ interface PublicModelHubProps {
const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded = false }) => {
const [modelHubData, setModelHubData] = useState<ModelGroupInfo[] | null>(null);
const [agentHubData, setAgentHubData] = useState<AgentCard[] | null>(null);
const [mcpHubData, setMcpHubData] = useState<MCPServerData[] | null>(null);
const [pageTitle, setPageTitle] = useState<string>("LiteLLM Gateway");
const [customDocsDescription, setCustomDocsDescription] = useState<string | null>(null);
const [litellmVersion, setLitellmVersion] = useState<string>("");
const [usefulLinks, setUsefulLinks] = useState<Record<string, string>>({});
const [loading, setLoading] = useState<boolean>(true);
const [agentLoading, setAgentLoading] = useState<boolean>(true);
const [mcpLoading, setMcpLoading] = useState<boolean>(true);
const [searchTerm, setSearchTerm] = useState<string>("");
const [agentSearchTerm, setAgentSearchTerm] = useState<string>("");
const [mcpSearchTerm, setMcpSearchTerm] = useState<string>("");
const [selectedProviders, setSelectedProviders] = useState<string[]>([]);
const [selectedModes, setSelectedModes] = useState<string[]>([]);
const [selectedFeatures, setSelectedFeatures] = useState<string[]>([]);
const [selectedAgentSkills, setSelectedAgentSkills] = useState<string[]>([]);
const [selectedMcpTransports, setSelectedMcpTransports] = useState<string[]>([]);
const [serviceStatus, setServiceStatus] = useState<string>("I'm alive! ✓");
const [isModalVisible, setIsModalVisible] = useState(false);
const [isAgentModalVisible, setIsAgentModalVisible] = useState(false);
const [isMcpModalVisible, setIsMcpModalVisible] = useState(false);
const [selectedModel, setSelectedModel] = useState<null | ModelGroupInfo>(null);
const [selectedAgent, setSelectedAgent] = useState<null | AgentCard>(null);
const [selectedMcpServer, setSelectedMcpServer] = useState<null | MCPServerData>(null);
const [proxySettings, setProxySettings] = useState<any>({});
const [activeTab, setActiveTab] = useState<string>("models");
const tableRef = useRef<TableInstance<any>>(null);
const agentTableRef = useRef<TableInstance<any>>(null);
const mcpTableRef = useRef<TableInstance<any>>(null);
useEffect(() => {
const fetchPublicData = async () => {
@ -120,6 +144,19 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
}
};
const fetchMcpData = async () => {
try {
setMcpLoading(true);
const _mcpHubData = await mcpHubPublicServersCall();
console.log("MCPHubData:", _mcpHubData);
setMcpHubData(_mcpHubData);
} catch (error) {
console.error("There was an error fetching the public MCP server data", error);
} finally {
setMcpLoading(false);
}
};
const fetchPublicModelHubInfo = async () => {
const publicModelHubInfo = await getPublicModelHubInfo();
console.log("Public Model Hub Info:", publicModelHubInfo);
@ -133,6 +170,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
fetchPublicData();
fetchAgentData();
fetchMcpData();
}, []);
// Clear filters when filter values change to avoid confusion
@ -186,6 +224,14 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
return Array.from(skills).sort();
};
const getUniqueMcpTransports = (data: MCPServerData[]) => {
const transports = new Set<string>();
data.forEach((server) => {
if (server.transport) transports.add(server.transport);
});
return Array.from(transports).sort();
};
const filteredData = useMemo(() => {
if (!modelHubData) return [];
@ -311,6 +357,56 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
});
}, [agentHubData, agentSearchTerm, selectedAgentSkills]);
const filteredMcpData = useMemo(() => {
if (!mcpHubData) return [];
let searchResults = mcpHubData;
// Apply search if there's a search term
if (mcpSearchTerm.trim()) {
const lowercaseSearch = mcpSearchTerm.toLowerCase();
const searchWords = lowercaseSearch.split(/\s+/);
searchResults = mcpHubData.filter((server) => {
const serverName = server.server_name.toLowerCase();
const serverDescription = (server.mcp_info?.description || "").toLowerCase();
// Check if it contains the exact search term
if (serverName.includes(lowercaseSearch) || serverDescription.includes(lowercaseSearch)) {
return true;
}
// Check if it contains all search words
return searchWords.every((word) => serverName.includes(word) || serverDescription.includes(word));
});
// Sort by relevance
searchResults = searchResults.sort((a, b) => {
const aName = a.server_name.toLowerCase();
const bName = b.server_name.toLowerCase();
const aExactMatch = aName === lowercaseSearch ? 1000 : 0;
const bExactMatch = bName === lowercaseSearch ? 1000 : 0;
const aStartsWith = aName.startsWith(lowercaseSearch) ? 100 : 0;
const bStartsWith = bName.startsWith(lowercaseSearch) ? 100 : 0;
const aScore = aExactMatch + aStartsWith + (1000 - aName.length);
const bScore = bExactMatch + bStartsWith + (1000 - bName.length);
return bScore - aScore;
});
}
// Apply transport filters
return searchResults.filter((server) => {
const matchesTransport =
selectedMcpTransports.length === 0 || selectedMcpTransports.includes(server.transport);
return matchesTransport;
});
}, [mcpHubData, mcpSearchTerm, selectedMcpTransports]);
const showModal = (model: ModelGroupInfo) => {
setSelectedModel(model);
setIsModalVisible(true);
@ -341,6 +437,21 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
setSelectedAgent(null);
};
const showMcpModal = (server: MCPServerData) => {
setSelectedMcpServer(server);
setIsMcpModalVisible(true);
};
const handleMcpModalOk = () => {
setIsMcpModalVisible(false);
setSelectedMcpServer(null);
};
const handleMcpModalCancel = () => {
setIsMcpModalVisible(false);
setSelectedMcpServer(null);
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
NotificationsManager.success("Copied to clipboard!");
@ -712,6 +823,94 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
},
];
const publicMCPHubColumns = (): ColumnDef<MCPServerData>[] => [
{
header: "Server Name",
accessorKey: "server_name",
enableSorting: true,
cell: ({ row }) => (
<div className="overflow-hidden">
<Tooltip title={row.original.server_name}>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left"
onClick={() => showMcpModal(row.original)}
>
{row.original.server_name}
</Button>
</Tooltip>
</div>
),
size: 150,
},
{
header: "Description",
accessorKey: "mcp_info.description",
enableSorting: false,
cell: ({ row }) => {
const description = row.original.mcp_info?.description || "-";
const truncated = description.length > 80 ? description.substring(0, 80) + "..." : description;
return (
<Tooltip title={description}>
<Text className="text-sm text-gray-700">{truncated}</Text>
</Tooltip>
);
},
size: 250,
},
{
header: "URL",
accessorKey: "url",
enableSorting: false,
cell: ({ row }) => {
const url = row.original.url;
const truncated = url.length > 40 ? url.substring(0, 40) + "..." : url;
return (
<Tooltip title={url}>
<div className="flex items-center space-x-2">
<Text className="text-xs font-mono">{truncated}</Text>
<Copy
onClick={() => copyToClipboard(url)}
className="cursor-pointer text-gray-500 hover:text-blue-500 w-3 h-3"
/>
</div>
</Tooltip>
);
},
size: 200,
},
{
header: "Transport",
accessorKey: "transport",
enableSorting: true,
cell: ({ row }) => {
const transport = row.original.transport;
return (
<Tag color="blue" className="text-xs uppercase">
{transport}
</Tag>
);
},
size: 100,
},
{
header: "Auth Type",
accessorKey: "auth_type",
enableSorting: true,
cell: ({ row }) => {
const authType = row.original.auth_type;
const color = authType === "none" ? "gray" : "green";
return (
<Tag color={color} className="text-xs capitalize">
{authType}
</Tag>
);
},
size: 100,
},
];
return (
<ThemeProvider accessToken={accessToken}>
<div className={isEmbedded ? "w-full" : "min-h-screen bg-white"}>
@ -979,6 +1178,73 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
</div>
</TabPane>
)}
{/* MCP Servers Tab */}
{mcpHubData && mcpHubData.length > 0 && (
<TabPane tab="MCP Hub" key="mcp">
<div className="flex justify-between items-center mb-8">
<Title className="text-2xl font-semibold text-gray-900">Available MCP Servers</Title>
</div>
{/* Filters */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8 p-6 bg-gray-50 rounded-lg border border-gray-200">
<div>
<div className="flex items-center space-x-2 mb-3">
<Text className="text-sm font-medium text-gray-700">Search MCP Servers:</Text>
<Tooltip
title="Search MCP servers by name or description"
placement="top"
>
<Info className="w-4 h-4 text-gray-400 cursor-help" />
</Tooltip>
</div>
<div className="relative">
<SearchIcon className="w-4 h-4 text-gray-400 absolute left-3 top-1/2 transform -translate-y-1/2" />
<input
type="text"
placeholder="Search MCP server names or descriptions..."
value={mcpSearchTerm}
onChange={(e) => setMcpSearchTerm(e.target.value)}
className="border border-gray-300 rounded-lg pl-10 pr-4 py-2 w-full text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
/>
</div>
</div>
<div>
<Text className="text-sm font-medium mb-3 text-gray-700">Transport:</Text>
<Select
mode="multiple"
value={selectedMcpTransports}
onChange={(values) => setSelectedMcpTransports(values)}
placeholder="Select transport types"
className="w-full"
size="large"
allowClear
>
{mcpHubData &&
getUniqueMcpTransports(mcpHubData).map((transport) => (
<Select.Option key={transport} value={transport}>
{transport}
</Select.Option>
))}
</Select>
</div>
</div>
<ModelDataTable
columns={publicMCPHubColumns()}
data={filteredMcpData}
isLoading={mcpLoading}
table={mcpTableRef}
defaultSorting={[{ id: "server_name", desc: false }]}
/>
<div className="mt-8 text-center">
<Text className="text-sm text-gray-600">
Showing {filteredMcpData.length} of {mcpHubData?.length || 0} MCP servers
</Text>
</div>
</TabPane>
)}
</Tabs>
</Card>
</div>
@ -1512,6 +1778,176 @@ print(response.model_dump(mode='json', exclude_none=True))`;
</div>
)}
</Modal>
{/* MCP Server Details Modal */}
<Modal
title={
<div className="flex items-center space-x-2">
<span>{selectedMcpServer?.server_name || "MCP Server Details"}</span>
{selectedMcpServer && (
<Tooltip title="Copy server name">
<Copy
onClick={() => copyToClipboard(selectedMcpServer.server_name)}
className="cursor-pointer text-gray-500 hover:text-blue-500 w-4 h-4"
/>
</Tooltip>
)}
</div>
}
width={1000}
open={isMcpModalVisible}
footer={null}
onOk={handleMcpModalOk}
onCancel={handleMcpModalCancel}
>
{selectedMcpServer && (
<div className="space-y-6">
{/* Server Overview */}
<div>
<Text className="text-lg font-semibold mb-4">Server Overview</Text>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<Text className="font-medium">Server Name:</Text>
<Text>{selectedMcpServer.server_name}</Text>
</div>
<div>
<Text className="font-medium">Transport:</Text>
<Tag color="blue">{selectedMcpServer.transport}</Tag>
</div>
{selectedMcpServer.alias && (
<div>
<Text className="font-medium">Alias:</Text>
<Text>{selectedMcpServer.alias}</Text>
</div>
)}
<div>
<Text className="font-medium">Auth Type:</Text>
<Tag color={selectedMcpServer.auth_type === "none" ? "gray" : "green"}>
{selectedMcpServer.auth_type}
</Tag>
</div>
<div className="col-span-2">
<Text className="font-medium">Description:</Text>
<Text>{selectedMcpServer.mcp_info?.description || "-"}</Text>
</div>
<div className="col-span-2">
<Text className="font-medium">URL:</Text>
<a
href={selectedMcpServer.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 text-sm break-all flex items-center space-x-2"
>
<span>{selectedMcpServer.url}</span>
<ExternalLinkIcon className="w-4 h-4" />
</a>
</div>
</div>
</div>
{/* Additional Info */}
{selectedMcpServer.mcp_info && Object.keys(selectedMcpServer.mcp_info).length > 0 && (
<div>
<Text className="text-lg font-semibold mb-4">Additional Information</Text>
<div className="bg-gray-50 p-4 rounded-lg">
<pre className="text-xs overflow-x-auto">
{JSON.stringify(selectedMcpServer.mcp_info, null, 2)}
</pre>
</div>
</div>
)}
{/* Usage Example */}
<div>
<Text className="text-lg font-semibold mb-4">Usage Example</Text>
<div className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto">
<pre className="text-sm">
{`# Using MCP Server with Python FastMCP
from fastmcp import Client
import asyncio
# Standard MCP configuration
config = {
"mcpServers": {
"${selectedMcpServer.server_name}": {
"url": "http://localhost:4000/${selectedMcpServer.server_name}/mcp",
"headers": {
"x-litellm-api-key": "Bearer sk-1234"
}
}
}
}
# Create a client that connects to the server
client = Client(config)
async def main():
async with client:
# List available tools
tools = await client.list_tools()
print(f"Available tools: {[tool.name for tool in tools]}")
# Call a tool
response = await client.call_tool(
name="tool_name",
arguments={"arg": "value"}
)
print(f"Response: {response}")
if __name__ == "__main__":
asyncio.run(main())`}
</pre>
</div>
<div className="mt-2 text-right">
<button
onClick={() => {
const codeSnippet = `# Using MCP Server with Python FastMCP
from fastmcp import Client
import asyncio
# Standard MCP configuration
config = {
"mcpServers": {
"${selectedMcpServer.server_name}": {
"url": "http://localhost:4000/${selectedMcpServer.server_name}/mcp",
"headers": {
"x-litellm-api-key": "Bearer sk-1234"
}
}
}
}
# Create a client that connects to the server
client = Client(config)
async def main():
async with client:
# List available tools
tools = await client.list_tools()
print(f"Available tools: {[tool.name for tool in tools]}")
# Call a tool
response = await client.call_tool(
name="tool_name",
arguments={"arg": "value"}
)
print(f"Response: {response}")
if __name__ == "__main__":
asyncio.run(main())`;
copyToClipboard(codeSnippet);
}}
className="text-sm text-blue-600 hover:text-blue-800 cursor-pointer"
>
Copy to clipboard
</button>
</div>
</div>
</div>
)}
</Modal>
</div>
</ThemeProvider>
);