Merge pull request #15185 from BerriAI/litellm_dev_10_03_2025_p1

(MCP - feat) UI - show health status of MCP servers, allow setting extra headers on the UI, allow editing allowed tools on the UI
This commit is contained in:
Krish Dholakia
2025-10-04 15:46:02 -07:00
committed by GitHub
16 changed files with 422 additions and 165 deletions
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "LiteLLM_MCPServerTable" ADD COLUMN "allowed_tools" TEXT[] DEFAULT ARRAY[]::TEXT[];
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "LiteLLM_MCPServerTable" ADD COLUMN "extra_headers" TEXT[] DEFAULT ARRAY[]::TEXT[];
@@ -179,6 +179,7 @@ model LiteLLM_MCPServerTable {
mcp_info Json? @default("{}")
mcp_access_groups String[]
allowed_tools String[] @default([])
extra_headers String[] @default([])
// Health check status
status String? @default("unknown")
last_health_check DateTime?
+2 -2
View File
@@ -1,7 +1,7 @@
from litellm._uuid import uuid
from typing import Any, Dict, Iterable, List, Optional, Set, Union
from litellm._logging import verbose_proxy_logger
from litellm._uuid import uuid
from litellm.proxy._types import (
LiteLLM_MCPServerTable,
LiteLLM_ObjectPermissionTable,
@@ -30,7 +30,7 @@ def _prepare_mcp_server_data(
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
# Convert model to dict
data_dict = data.model_dump()
data_dict = data.model_dump(exclude_none=True)
# Ensure alias is always present in the dict (even if None)
if "alias" not in data_dict:
data_dict["alias"] = getattr(data, "alias", None)
@@ -10,7 +10,7 @@ import asyncio
import datetime
import hashlib
import json
from typing import Any, Dict, List, Optional, Union, cast
from typing import Any, Dict, List, Optional, Set, Union, cast
from fastapi import HTTPException
from mcp.types import CallToolRequestParams as MCPCallToolRequestParams
@@ -240,50 +240,64 @@ class MCPServerManager:
)
def add_update_server(self, mcp_server: LiteLLM_MCPServerTable):
if mcp_server.server_id not in self.get_registry():
_mcp_info: MCPInfo = mcp_server.mcp_info or {}
# Use helper to deserialize environment dictionary
# Safely access env field which may not exist on Prisma model objects
env_data = getattr(mcp_server, "env", None)
env_dict = _deserialize_env_dict(env_data)
# Use alias for name if present, else server_name
name_for_prefix = (
mcp_server.alias or mcp_server.server_name or mcp_server.server_id
)
# Preserve all custom fields from database while setting defaults for core fields
mcp_info: MCPInfo = _mcp_info.copy()
# Set default values for core fields if not present
if "server_name" not in mcp_info:
mcp_info["server_name"] = mcp_server.server_name or mcp_server.server_id
if "description" not in mcp_info and mcp_server.description:
mcp_info["description"] = mcp_server.description
try:
if mcp_server.server_id not in self.get_registry():
_mcp_info: MCPInfo = mcp_server.mcp_info or {}
# Use helper to deserialize environment dictionary
# Safely access env field which may not exist on Prisma model objects
env_data = getattr(mcp_server, "env", None)
env_dict = _deserialize_env_dict(env_data)
# Use alias for name if present, else server_name
name_for_prefix = (
mcp_server.alias or mcp_server.server_name or mcp_server.server_id
)
# Preserve all custom fields from database while setting defaults for core fields
mcp_info: MCPInfo = _mcp_info.copy()
# Set default values for core fields if not present
if "server_name" not in mcp_info:
mcp_info["server_name"] = (
mcp_server.server_name or mcp_server.server_id
)
if "description" not in mcp_info and mcp_server.description:
mcp_info["description"] = mcp_server.description
new_server = MCPServer(
server_id=mcp_server.server_id,
name=name_for_prefix,
alias=getattr(mcp_server, "alias", None),
server_name=getattr(mcp_server, "server_name", None),
url=mcp_server.url,
transport=cast(MCPTransportType, mcp_server.transport),
auth_type=cast(MCPAuthType, mcp_server.auth_type),
mcp_info=mcp_info,
extra_headers=getattr(mcp_server, "extra_headers", None),
# oauth specific fields
client_id=getattr(mcp_server, "client_id", None),
client_secret=getattr(mcp_server, "client_secret", None),
scopes=getattr(mcp_server, "scopes", None),
authorization_url=getattr(mcp_server, "authorization_url", None),
token_url=getattr(mcp_server, "token_url", None),
# Stdio-specific fields
command=getattr(mcp_server, "command", None),
args=getattr(mcp_server, "args", None) or [],
env=env_dict,
access_groups=getattr(mcp_server, "mcp_access_groups", None),
allowed_tools=getattr(mcp_server, "allowed_tools", None),
disallowed_tools=getattr(mcp_server, "disallowed_tools", None),
)
self.registry[mcp_server.server_id] = new_server
verbose_logger.debug(f"Added MCP Server: {name_for_prefix}")
new_server = MCPServer(
server_id=mcp_server.server_id,
name=name_for_prefix,
alias=getattr(mcp_server, "alias", None),
server_name=getattr(mcp_server, "server_name", None),
url=mcp_server.url,
transport=cast(MCPTransportType, mcp_server.transport),
auth_type=cast(MCPAuthType, mcp_server.auth_type),
mcp_info=mcp_info,
extra_headers=getattr(mcp_server, "extra_headers", None),
# oauth specific fields
client_id=getattr(mcp_server, "client_id", None),
client_secret=getattr(mcp_server, "client_secret", None),
scopes=getattr(mcp_server, "scopes", None),
authorization_url=getattr(mcp_server, "authorization_url", None),
token_url=getattr(mcp_server, "token_url", None),
# Stdio-specific fields
command=getattr(mcp_server, "command", None),
args=getattr(mcp_server, "args", None) or [],
env=env_dict,
access_groups=getattr(mcp_server, "mcp_access_groups", None),
allowed_tools=getattr(mcp_server, "allowed_tools", None),
disallowed_tools=getattr(mcp_server, "disallowed_tools", None),
)
self.registry[mcp_server.server_id] = new_server
verbose_logger.debug(f"Added MCP Server: {name_for_prefix}")
except Exception as e:
verbose_logger.debug(f"Failed to add MCP server: {str(e)}")
raise e
def get_all_mcp_server_ids(self) -> Set[str]:
"""
Get all MCP server IDs
"""
all_servers = list(self.get_registry().values())
return {server.server_id for server in all_servers}
async def get_allowed_mcp_servers(
self, user_api_key_auth: Optional[UserAPIKeyAuth] = None
@@ -1118,25 +1132,23 @@ class MCPServerManager:
if _server_id in allowed_server_ids:
list_mcp_servers.append(
LiteLLM_MCPServerTable(
server_id=_server_id,
server_name=_server_config.name,
alias=_server_config.alias,
url=_server_config.url,
transport=_server_config.transport,
auth_type=_server_config.auth_type,
created_at=datetime.datetime.now(),
updated_at=datetime.datetime.now(),
description=(
_server_config.mcp_info.get("description")
if _server_config.mcp_info
else None
),
mcp_info=_server_config.mcp_info,
mcp_access_groups=_server_config.access_groups or [],
# Stdio-specific fields
command=getattr(_server_config, "command", None),
args=getattr(_server_config, "args", None) or [],
env=getattr(_server_config, "env", None) or {},
**{
**_server_config.model_dump(),
"created_at": datetime.datetime.now(),
"updated_at": datetime.datetime.now(),
"description": (
_server_config.mcp_info.get("description")
if _server_config.mcp_info
else None
),
"allowed_tools": _server_config.allowed_tools or [],
"mcp_info": _server_config.mcp_info,
"mcp_access_groups": _server_config.access_groups or [],
"extra_headers": _server_config.extra_headers or [],
"command": getattr(_server_config, "command", None),
"args": getattr(_server_config, "args", None) or [],
"env": getattr(_server_config, "env", None) or {},
}
)
)
@@ -1176,44 +1188,19 @@ class MCPServerManager:
}
)
# Map servers to their teams and return with health data
from typing import cast
## mark invalid servers w/ reason for being invalid
valid_server_ids = self.get_all_mcp_server_ids()
for server in list_mcp_servers:
if server.server_id not in valid_server_ids:
server.status = "unhealthy"
## try adding server to registry to get error
try:
self.add_update_server(server)
except Exception as e:
server.health_check_error = str(e)
server.health_check_error = "Server is not in in memory registry yet. This could be a temporary sync issue."
return [
LiteLLM_MCPServerTable(
server_id=server.server_id,
server_name=server.server_name,
alias=server.alias,
description=server.description,
url=server.url,
transport=server.transport,
auth_type=server.auth_type,
created_at=server.created_at,
created_by=server.created_by,
updated_at=server.updated_at,
updated_by=server.updated_by,
mcp_access_groups=(
server.mcp_access_groups
if server.mcp_access_groups is not None
else []
),
allowed_tools=(
server.allowed_tools
if server.allowed_tools is not None
else []
),
mcp_info=server.mcp_info,
teams=cast(
List[Dict[str, str | None]],
server_to_teams_map.get(server.server_id, []),
),
# Stdio-specific fields
command=getattr(server, "command", None),
args=getattr(server, "args", None) or [],
env=getattr(server, "env", None) or {},
)
for server in list_mcp_servers
]
return list_mcp_servers
async def reload_servers_from_database(self):
"""
+3 -1
View File
@@ -925,6 +925,7 @@ class NewMCPServerRequest(LiteLLMPydanticObjectBase):
mcp_info: Optional[MCPInfo] = None
mcp_access_groups: List[str] = Field(default_factory=list)
allowed_tools: Optional[List[str]] = None
extra_headers: Optional[List[str]] = None
# Stdio-specific fields
command: Optional[str] = None
args: List[str] = Field(default_factory=list)
@@ -994,9 +995,10 @@ class LiteLLM_MCPServerTable(LiteLLMPydanticObjectBase):
teams: List[Dict[str, Optional[str]]] = Field(default_factory=list)
mcp_access_groups: List[str] = Field(default_factory=list)
allowed_tools: List[str] = Field(default_factory=list)
extra_headers: List[str] = Field(default_factory=list)
mcp_info: Optional[MCPInfo] = None
# Health check status
status: Optional[str] = Field(
status: Optional[Literal["healthy", "unhealthy", "unknown"]] = Field(
default="unknown",
description="Health status: 'healthy', 'unhealthy', 'unknown'",
)
+1
View File
@@ -179,6 +179,7 @@ model LiteLLM_MCPServerTable {
mcp_info Json? @default("{}")
mcp_access_groups String[]
allowed_tools String[] @default([])
extra_headers String[] @default([])
// Health check status
status String? @default("unknown")
last_health_check DateTime?
+1
View File
@@ -179,6 +179,7 @@ model LiteLLM_MCPServerTable {
mcp_info Json? @default("{}")
mcp_access_groups String[]
allowed_tools String[] @default([])
extra_headers String[] @default([])
// Health check status
status String? @default("unknown")
last_health_check DateTime?
@@ -654,6 +654,7 @@ class TestMCPServerManager:
"Tool tool3 is not allowed for server test-server"
in exc_info.value.detail["error"]
)
async def test_get_tools_from_server_add_prefix(self):
"""Verify _get_tools_from_server respects add_prefix True/False."""
manager = MCPServerManager()
@@ -909,6 +910,39 @@ class TestMCPServerManager:
assert "tool_1" in tool_names
assert "tool_2" in tool_names
def test_add_db_mcp_server_to_registry(self):
"""Test that add_db_mcp_server_to_registry adds a MCP server to the registry"""
manager = MCPServerManager()
server = LiteLLM_MCPServerTable(
**{
"server_id": "4c679a81-acd9-4954-9f84-30b739362498",
"server_name": "edc_mcp_server",
"alias": "edc_mcp_server",
"description": None,
"url": "fake_mcp_url",
"transport": "http",
"auth_type": "none",
"created_at": "2025-09-30T08:28:31.353000Z",
"created_by": "a1248959",
"updated_at": "2025-09-30T08:28:31.353000Z",
"updated_by": "a1248959",
"teams": [],
"mcp_access_groups": [],
"mcp_info": {
"server_name": "edc_mcp_server",
"mcp_server_cost_info": None,
},
"status": "unknown",
"last_health_check": None,
"health_check_error": None,
"command": None,
"args": [],
"env": {},
},
)
manager.add_update_server(server)
assert server.server_id in manager.get_registry()
if __name__ == "__main__":
pytest.main([__file__])
@@ -0,0 +1,121 @@
import React, { useState, useEffect } from "react"
import { Form, Select, Tooltip, Collapse } from "antd"
import { InfoCircleOutlined } from "@ant-design/icons"
import { MCPServer } from "./types"
const { Panel } = Collapse
interface MCPPermissionManagementProps {
availableAccessGroups: string[]
mcpServer: MCPServer | null
searchValue: string
setSearchValue: (value: string) => void
getAccessGroupOptions: () => Array<{
value: string
label: React.ReactNode
}>
}
const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
availableAccessGroups,
mcpServer,
searchValue,
setSearchValue,
getAccessGroupOptions,
}) => {
const form = Form.useFormInstance()
// Set initial values when mcpServer changes
useEffect(() => {
if (mcpServer) {
// Set extra_headers if they exist
if (mcpServer.extra_headers) {
form.setFieldValue('extra_headers', mcpServer.extra_headers)
}
}
}, [mcpServer, form])
return (
<Collapse
className="bg-gray-50 border border-gray-200 rounded-lg"
expandIconPosition="end"
ghost={false}
>
<Panel
header={
<div className="flex items-center">
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
<h3 className="text-lg font-semibold text-gray-900">Permission Management / Access Control</h3>
</div>
<p className="text-sm text-gray-600 ml-4">
Configure access permissions and security settings (Optional)
</p>
</div>
}
key="permissions"
className="border-0"
>
<div className="space-y-6 pt-4">
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
MCP Access Groups
<Tooltip title="Specify access groups for this MCP server. Users must be in at least one of these groups to access the server.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name="mcp_access_groups"
className="mb-4"
>
<Select
mode="tags"
showSearch
placeholder="Select existing groups or type to create new ones"
optionFilterProp="value"
filterOption={(input, option) => (option?.value ?? "").toLowerCase().includes(input.toLowerCase())}
onSearch={(value) => setSearchValue(value)}
tokenSeparators={[","]}
options={getAccessGroupOptions()}
maxTagCount="responsive"
allowClear
/>
</Form.Item>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Extra Headers
<Tooltip title="Forward custom headers from incoming requests to this MCP server (e.g., Authorization, X-Custom-Header, User-Agent)">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
{mcpServer?.extra_headers && mcpServer.extra_headers.length > 0 && (
<span className="ml-2 text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full">
{mcpServer.extra_headers.length} configured
</span>
)}
</span>
}
name="extra_headers"
>
<Select
mode="tags"
placeholder={
mcpServer?.extra_headers && mcpServer.extra_headers.length > 0
? `Currently: ${mcpServer.extra_headers.join(', ')}`
: "Enter header names (e.g., Authorization, X-Custom-Header)"
}
className="rounded-lg"
size="large"
tokenSeparators={[","]}
allowClear
/>
</Form.Item>
</div>
</Panel>
</Collapse>
)
}
export default MCPPermissionManagement
@@ -8,6 +8,7 @@ import MCPServerCostConfig from "./mcp_server_cost_config"
import MCPConnectionStatus from "./mcp_connection_status"
import MCPToolConfiguration from "./mcp_tool_configuration"
import StdioConfiguration from "./StdioConfiguration"
import MCPPermissionManagement from "./MCPPermissionManagement"
import { isAdminRole } from "@/utils/roles"
import { validateMCPServerUrl, validateMCPServerName } from "./utils"
import NotificationsManager from "../molecules/notifications_manager"
@@ -381,32 +382,17 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
{/* Stdio Configuration - only show for stdio transport */}
<StdioConfiguration isVisible={transportType === "stdio"} />
</div>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
MCP Access Groups
<Tooltip title="Specify access groups for this MCP server. Users must be in at least one of these groups to access the server.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name="mcp_access_groups"
className="mb-4"
>
<Select
mode="tags"
showSearch
placeholder="Select existing groups or type to create new ones"
optionFilterProp="value"
filterOption={(input, option) => (option?.value ?? "").toLowerCase().includes(input.toLowerCase())}
onSearch={(value) => setSearchValue(value)}
tokenSeparators={[","]}
options={getAccessGroupOptions()}
maxTagCount="responsive"
allowClear
/>
</Form.Item>
{/* Permission Management / Access Control Section */}
<div className="mt-8">
<MCPPermissionManagement
availableAccessGroups={availableAccessGroups}
mcpServer={null}
searchValue={searchValue}
setSearchValue={setSearchValue}
getAccessGroupOptions={getAccessGroupOptions}
/>
</div>
{/* Connection Status Section */}
@@ -420,6 +406,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
accessToken={accessToken}
formValues={formValues}
allowedTools={allowedTools}
existingAllowedTools={null}
onAllowedToolsChange={setAllowedTools}
/>
</div>
@@ -61,6 +61,67 @@ export const mcpServerColumns = (
</span>
),
},
{
id: "health_status",
header: "Health Status",
cell: ({ row }) => {
const server = row.original;
const status = server.status || "unknown";
const lastCheck = server.last_health_check;
const error = server.health_check_error;
const getStatusColor = (status: string) => {
switch (status) {
case "healthy":
return "text-green-500 bg-green-50 hover:bg-green-100";
case "unhealthy":
return "text-red-500 bg-red-50 hover:bg-red-100";
default:
return "text-gray-500 bg-gray-50 hover:bg-gray-100";
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case "healthy":
return "●";
case "unhealthy":
return "●";
default:
return "●";
}
};
const tooltipContent = (
<div className="max-w-xs">
<div className="font-semibold mb-1">Health Status: {status}</div>
{lastCheck && (
<div className="text-xs mb-1">
Last Check: {new Date(lastCheck).toLocaleString()}
</div>
)}
{error && (
<div className="text-xs">
<div className="font-medium text-red-400 mb-1">Error:</div>
<div className="break-words">{error}</div>
</div>
)}
{!lastCheck && !error && (
<div className="text-xs text-gray-400">No health check data available</div>
)}
</div>
);
return (
<Tooltip title={tooltipContent} placement="top">
<button className={`font-mono text-xs font-normal px-2 py-0.5 text-left w-full truncate whitespace-nowrap cursor-pointer max-w-[10ch] ${getStatusColor(status)}`}>
<span className="mr-1">{getStatusIcon(status)}</span>
{status.charAt(0).toUpperCase() + status.slice(1)}
</button>
</Tooltip>
);
},
},
{
id: "mcp_access_groups",
header: "Access Groups",
@@ -4,6 +4,8 @@ import { Button, TextInput, TabGroup, TabList, Tab, TabPanels, TabPanel } from "
import { MCPServer, MCPServerCostInfo } from "./types";
import { updateMCPServer, testMCPToolsListRequest } from "../networking";
import MCPServerCostConfig from "./mcp_server_cost_config";
import MCPPermissionManagement from "./MCPPermissionManagement";
import MCPToolConfiguration from "./mcp_tool_configuration";
import { MinusCircleOutlined, PlusOutlined, InfoCircleOutlined } from "@ant-design/icons";
import { validateMCPServerUrl, validateMCPServerName } from "./utils";
import NotificationsManager from "../molecules/notifications_manager";
@@ -22,7 +24,8 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({ mcpServer, accessToken, o
const [tools, setTools] = useState<any[]>([]);
const [isLoadingTools, setIsLoadingTools] = useState(false);
const [searchValue, setSearchValue] = useState<string>("");
const [aliasManuallyEdited, setAliasManuallyEdited] = useState(false)
const [aliasManuallyEdited, setAliasManuallyEdited] = useState(false);
const [allowedTools, setAllowedTools] = useState<string[]>([]);
// Initialize cost config from existing server data
useEffect(() => {
@@ -31,6 +34,13 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({ mcpServer, accessToken, o
}
}, [mcpServer]);
// Initialize allowed tools from existing server data
useEffect(() => {
if (mcpServer.allowed_tools) {
setAllowedTools(mcpServer.allowed_tools);
}
}, [mcpServer]);
// Transform string array to object array for initial form values
useEffect(() => {
if (mcpServer.mcp_access_groups) {
@@ -114,7 +124,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({ mcpServer, accessToken, o
// Ensure access groups is always a string array
const accessGroups = (values.mcp_access_groups || []).map((g: any) => typeof g === 'string' ? g : g.name || String(g));
// Prepare the payload with cost configuration
// Prepare the payload with cost configuration and permission fields
const payload = {
...values,
server_id: mcpServer.server_id,
@@ -125,6 +135,10 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({ mcpServer, accessToken, o
},
mcp_access_groups: accessGroups,
alias: values.alias,
// Include permission management fields
extra_headers: values.extra_headers || [],
allowed_tools: allowedTools.length > 0 ? allowedTools : null,
disallowed_tools: values.disallowed_tools || [],
};
const updated = await updateMCPServer(accessToken, payload);
@@ -179,34 +193,34 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({ mcpServer, accessToken, o
</Select>
</Form.Item>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
MCP Access Groups
<Tooltip title="Define access groups for this MCP server. Each group represents a set of permissions.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name="mcp_access_groups"
getValueFromEvent={value => value}
>
<Select
mode="tags"
style={{ width: '100%' }}
showSearch
placeholder="Add or select access groups"
tokenSeparators={[',']}
optionFilterProp="value"
filterOption={(input, option) =>
(option?.value ?? '').toLowerCase().includes(input.toLowerCase())
}
onSearch={(value) => setSearchValue(value)}
options={getAccessGroupOptions()}
// Ensure value is always an array of strings
getPopupContainer={trigger => trigger.parentNode}
{/* Permission Management / Access Control Section */}
<div className="mt-6">
<MCPPermissionManagement
availableAccessGroups={availableAccessGroups}
mcpServer={mcpServer}
searchValue={searchValue}
setSearchValue={setSearchValue}
getAccessGroupOptions={getAccessGroupOptions}
/>
</Form.Item>
</div>
{/* Tool Configuration Section */}
<div className="mt-6">
<MCPToolConfiguration
accessToken={accessToken}
formValues={{
server_id: mcpServer.server_id,
server_name: mcpServer.server_name,
url: mcpServer.url,
transport: mcpServer.transport,
auth_type: mcpServer.auth_type,
mcp_info: mcpServer.mcp_info,
}}
allowedTools={allowedTools}
existingAllowedTools={mcpServer.allowed_tools || null}
onAllowedToolsChange={setAllowedTools}
/>
</div>
<div className="flex justify-end gap-2">
<AntdButton onClick={onCancel}>Cancel</AntdButton>
@@ -221,6 +221,10 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
<Text className="font-medium">Transport</Text>
<div>{handleTransport(mcpServer.transport)}</div>
</div>
<div>
<Text className="font-medium">Extra Headers</Text>
<div>{mcpServer.extra_headers?.join(", ")}</div>
</div>
<div>
<Text className="font-medium">Auth Type</Text>
<div>{handleAuth(mcpServer.auth_type)}</div>
@@ -8,6 +8,7 @@ interface MCPToolConfigurationProps {
accessToken: string | null
formValues: Record<string, any>
allowedTools: string[]
existingAllowedTools: string[] | null
onAllowedToolsChange: (tools: string[]) => void
}
@@ -15,6 +16,7 @@ const MCPToolConfiguration: React.FC<MCPToolConfigurationProps> = ({
accessToken,
formValues,
allowedTools,
existingAllowedTools,
onAllowedToolsChange,
}) => {
const previousToolsLengthRef = useRef(0)
@@ -25,19 +27,28 @@ const MCPToolConfiguration: React.FC<MCPToolConfigurationProps> = ({
enabled: true,
})
// Auto-select all tools when tools are first loaded
// Auto-select tools when tools are first loaded
useEffect(() => {
// Only auto-select if:
// 1. We have tools
// 2. Tools length changed (new tools loaded)
// 3. No tools are currently selected (initial state)
if (tools.length > 0 && tools.length !== previousToolsLengthRef.current && allowedTools.length === 0) {
const allToolNames = tools.map((tool) => tool.name)
onAllowedToolsChange(allToolNames)
if (existingAllowedTools && existingAllowedTools.length > 0) {
// If we have existing allowed tools, use those as the initial selection
// Filter to only include tools that are actually available from the server
const availableToolNames = tools.map((tool) => tool.name)
const validExistingTools = existingAllowedTools.filter(toolName => availableToolNames.includes(toolName))
onAllowedToolsChange(validExistingTools)
} else {
// If no existing allowed tools, auto-select all tools (create mode)
const allToolNames = tools.map((tool) => tool.name)
onAllowedToolsChange(allToolNames)
}
}
// Update ref to track tools length (will be 0 when tools clear)
previousToolsLengthRef.current = tools.length
}, [tools, allowedTools.length, onAllowedToolsChange])
}, [tools, allowedTools.length, existingAllowedTools, onAllowedToolsChange])
const handleToolToggle = (toolName: string) => {
if (allowedTools.includes(toolName)) {
@@ -78,6 +89,14 @@ const MCPToolConfiguration: React.FC<MCPToolConfigurationProps> = ({
)}
</div>
</div>
{/* Description */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<Text className="text-blue-800 text-sm">
<strong>Select which tools users can call:</strong> Only checked tools will be available for users to invoke.
Unchecked tools will be blocked from execution.
</Text>
</div>
{/* Loading state */}
{isLoadingTools && (
@@ -124,7 +143,7 @@ const MCPToolConfiguration: React.FC<MCPToolConfigurationProps> = ({
<div className="flex items-center gap-2 p-3 bg-green-50 rounded-lg border border-green-200 flex-1">
<CheckCircleOutlined className="text-green-600" />
<Text className="text-green-700 font-medium">
{allowedTools.length} of {tools.length} {tools.length === 1 ? "tool" : "tools"} selected
{allowedTools.length} of {tools.length} {tools.length === 1 ? "tool" : "tools"} enabled for user access
</Text>
</div>
<div className="flex gap-2 ml-3">
@@ -133,14 +152,14 @@ const MCPToolConfiguration: React.FC<MCPToolConfigurationProps> = ({
onClick={handleSelectAll}
className="px-3 py-1.5 text-sm text-blue-600 hover:text-blue-700 hover:bg-blue-50 rounded-md transition-colors"
>
Select All
Enable All
</button>
<button
type="button"
onClick={handleDeselectAll}
className="px-3 py-1.5 text-sm text-gray-600 hover:text-gray-700 hover:bg-gray-100 rounded-md transition-colors"
>
Deselect All
Disable All
</button>
</div>
</div>
@@ -160,8 +179,23 @@ const MCPToolConfiguration: React.FC<MCPToolConfigurationProps> = ({
<div className="flex items-start gap-3">
<Checkbox checked={allowedTools.includes(tool.name)} onChange={() => handleToolToggle(tool.name)} />
<div className="flex-1">
<Text className="font-medium text-gray-900">{tool.name}</Text>
<div className="flex items-center gap-2">
<Text className="font-medium text-gray-900">{tool.name}</Text>
<span className={`px-2 py-0.5 text-xs rounded-full font-medium ${
allowedTools.includes(tool.name)
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}>
{allowedTools.includes(tool.name) ? "Enabled" : "Disabled"}
</span>
</div>
{tool.description && <Text className="text-gray-500 text-sm block mt-1">{tool.description}</Text>}
<Text className="text-gray-400 text-xs block mt-1">
{allowedTools.includes(tool.name)
? "✓ Users can call this tool"
: "✗ Users cannot call this tool"
}
</Text>
</div>
</div>
</div>
@@ -138,6 +138,10 @@ export interface MCPServer {
created_by: string
updated_at: string
updated_by: string
extra_headers?: string[] | null
status?: "healthy" | "unhealthy" | "unknown"
last_health_check?: string | null
health_check_error?: string | null
teams?: Team[]
mcp_access_groups?: string[]
allowed_tools?: string[]