diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index 1458381c02..012ee1569d 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -177,8 +177,8 @@ class MCPServerManager: mcp_server_cost_info=_mcp_info.get("mcp_server_cost_info", None), ), # Stdio-specific fields - command=mcp_server.command, - args=mcp_server.args, + command=getattr(mcp_server, 'command', None), + args=getattr(mcp_server, 'args', None) or [], env=env_dict, ) self.registry[mcp_server.server_id] = new_server diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 4333d547bd..b281c20b25 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -852,8 +852,8 @@ class NewMCPServerRequest(LiteLLMPydanticObjectBase): mcp_access_groups: List[str] = Field(default_factory=list) # Stdio-specific fields command: Optional[str] = None - args: Optional[List[str]] = None - env: Optional[Dict[str, str]] = None + args: List[str] = Field(default_factory=list) + env: Dict[str, str] = Field(default_factory=dict) @model_validator(mode="before") @classmethod @@ -884,8 +884,8 @@ class UpdateMCPServerRequest(LiteLLMPydanticObjectBase): mcp_access_groups: List[str] = Field(default_factory=list) # Stdio-specific fields command: Optional[str] = None - args: Optional[List[str]] = None - env: Optional[Dict[str, str]] = None + args: List[str] = Field(default_factory=list) + env: Dict[str, str] = Field(default_factory=dict) @model_validator(mode="before") @classmethod @@ -923,8 +923,8 @@ class LiteLLM_MCPServerTable(LiteLLMPydanticObjectBase): mcp_info: Optional[MCPInfo] = None # Stdio-specific fields command: Optional[str] = None - args: Optional[List[str]] = None - env: Optional[Dict[str, str]] = None + args: List[str] = Field(default_factory=list) + env: Dict[str, str] = Field(default_factory=dict) class NewUserRequestTeam(LiteLLMPydanticObjectBase): diff --git a/litellm/proxy/management_endpoints/mcp_management_endpoints.py b/litellm/proxy/management_endpoints/mcp_management_endpoints.py index f0f8d48e49..257c48d1ad 100644 --- a/litellm/proxy/management_endpoints/mcp_management_endpoints.py +++ b/litellm/proxy/management_endpoints/mcp_management_endpoints.py @@ -247,9 +247,9 @@ if MCP_AVAILABLE: updated_at=datetime.now(), mcp_info=_server_config.mcp_info, # Stdio-specific fields - command=_server_config.command, - args=_server_config.args, - env=_server_config.env, + command=getattr(_server_config, 'command', None), + args=getattr(_server_config, 'args', None) or [], + env=getattr(_server_config, 'env', None) or {}, ) ) @@ -271,9 +271,9 @@ if MCP_AVAILABLE: mcp_info=server.mcp_info, teams=cast(List[Dict[str, str | None]], server_to_teams_map.get(server.server_id, [])), # Stdio-specific fields - command=server.command, - args=server.args, - env=server.env, + command=getattr(server, 'command', None), + args=getattr(server, 'args', None) or [], + env=getattr(server, 'env', None) or {}, ) for server in LIST_MCP_SERVERS ] diff --git a/ui/litellm-dashboard/src/components/chat_ui.tsx b/ui/litellm-dashboard/src/components/chat_ui.tsx index 58ba741a9a..71ee967af1 100644 --- a/ui/litellm-dashboard/src/components/chat_ui.tsx +++ b/ui/litellm-dashboard/src/components/chat_ui.tsx @@ -617,9 +617,9 @@ const ChatUI: React.FC = ({
{/* Left Sidebar with Controls */} -
+
Configurations -
+
API Key Source @@ -799,7 +799,7 @@ const ChatUI: React.FC = ({ />
-
+
= ({ ...accessGroups.map(group => ({ label: group, value: group, - isAccessGroup: true + isAccessGroup: true, + searchText: `${group} Access Group` })), ...mcpServers.map(server => ({ label: `${server.alias || server.server_id} (${server.server_id})`, value: server.server_id, - isAccessGroup: false + isAccessGroup: false, + searchText: `${server.alias || server.server_id} ${server.server_id} MCP Server` })) ]; @@ -82,29 +84,39 @@ const MCPServerSelector: React.FC = ({ value={selectedValues} loading={loading} className={className} - optionFilterProp="label" showSearch style={{ width: '100%' }} disabled={disabled} + filterOption={(input, option) => { + const searchText = options.find(opt => opt.value === option?.value)?.searchText || ''; + return searchText.toLowerCase().includes(input.toLowerCase()); + }} > {options.map(opt => ( - {opt.isAccessGroup && ( +
- )} - {opt.label} - {opt.isAccessGroup && (Access Group)} + {opt.label} + + {opt.isAccessGroup ? 'Access Group' : 'MCP Server'} + +
))} diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx index 616a62cd1c..3e9ff5bb78 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx @@ -18,6 +18,7 @@ interface CreateMCPServerProps { onCreateSuccess: (newMcpServer: MCPServer) => void isModalVisible: boolean setModalVisible: (visible: boolean) => void + availableAccessGroups: string[] } const CreateMCPServer: React.FC = ({ @@ -26,14 +27,15 @@ const CreateMCPServer: React.FC = ({ onCreateSuccess, isModalVisible, setModalVisible, + availableAccessGroups, }) => { const [form] = Form.useForm() const [isLoading, setIsLoading] = useState(false) const [costConfig, setCostConfig] = useState({}) - const [mcpAccessGroups, setMcpAccessGroups] = useState([]) const [formValues, setFormValues] = useState>({}) const [tools, setTools] = useState([]) const [transportType, setTransportType] = useState("sse") + const [searchValue, setSearchValue] = useState("") const handleCreate = async (formValues: Record) => { setIsLoading(true) @@ -132,6 +134,35 @@ const CreateMCPServer: React.FC = ({ } } + // Generate options with existing groups and potential new group + const getAccessGroupOptions = () => { + const existingOptions = availableAccessGroups.map((group: string) => ({ + value: group, + label: ( +
+
+ {group} +
+ ), + })) + + // If search value doesn't match any existing group and is not empty, add "create new group" option + if (searchValue && !availableAccessGroups.some(group => group.toLowerCase().includes(searchValue.toLowerCase()))) { + existingOptions.push({ + value: searchValue, + label: ( +
+
+ {searchValue} + create new group +
+ ), + }) + } + + return existingOptions + } + // rendering if (!isAdminRole(userRole)) { return null @@ -304,12 +335,13 @@ const CreateMCPServer: React.FC = ({ mode="tags" showSearch placeholder="Select existing groups or type to create new ones" - optionFilterProp="children" + optionFilterProp="value" + filterOption={(input, option) => + (option?.value ?? '').toLowerCase().includes(input.toLowerCase()) + } + onSearch={(value) => setSearchValue(value)} tokenSeparators={[","]} - options={mcpAccessGroups.map((group) => ({ - value: group, - label: group, - }))} + options={getAccessGroupOptions()} maxTagCount="responsive" allowClear /> diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx index 90b5fd6833..7bbed8d8c4 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx @@ -11,13 +11,15 @@ interface MCPServerEditProps { accessToken: string | null; onCancel: () => void; onSuccess: (server: MCPServer) => void; + availableAccessGroups: string[]; } -const MCPServerEdit: React.FC = ({ mcpServer, accessToken, onCancel, onSuccess }) => { +const MCPServerEdit: React.FC = ({ mcpServer, accessToken, onCancel, onSuccess, availableAccessGroups }) => { const [form] = Form.useForm(); const [costConfig, setCostConfig] = useState({}); const [tools, setTools] = useState([]); const [isLoadingTools, setIsLoadingTools] = useState(false); + const [searchValue, setSearchValue] = useState(""); // Initialize cost config from existing server data useEffect(() => { @@ -75,6 +77,35 @@ const MCPServerEdit: React.FC = ({ mcpServer, accessToken, o } }; + // Generate options with existing groups and potential new group + const getAccessGroupOptions = () => { + const existingOptions = availableAccessGroups.map((group: string) => ({ + value: group, + label: ( +
+
+ {group} +
+ ), + })) + + // If search value doesn't match any existing group and is not empty, add "create new group" option + if (searchValue && !availableAccessGroups.some(group => group.toLowerCase().includes(searchValue.toLowerCase()))) { + existingOptions.push({ + value: searchValue, + label: ( +
+
+ {searchValue} + create new group +
+ ), + }) + } + + return existingOptions + } + const handleSave = async (values: Record) => { if (!accessToken) return; try { @@ -160,8 +191,15 @@ const MCPServerEdit: React.FC = ({ mcpServer, accessToken, o