mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
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:
parent
c5c563c302
commit
65ff1eff56
14 changed files with 1576 additions and 20 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)}, "
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
"""
|
||||
|
|
|
|||
336
ui/litellm-dashboard/src/components/make_mcp_public_form.tsx
Normal file
336
ui/litellm-dashboard/src/components/make_mcp_public_form.tsx
Normal 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;
|
||||
|
||||
267
ui/litellm-dashboard/src/components/mcp_hub_table_columns.tsx
Normal file
267
ui/litellm-dashboard/src/components/mcp_hub_table_columns.tsx
Normal 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;
|
||||
};
|
||||
|
||||
|
|
@ -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's available.
|
||||
Make models, agents, and MCP servers public for developers to know what'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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue