diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index f52288ea72a..cbd64a178b8 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -16289,7 +16289,7 @@ "cache_read_input_token_cost": 3e-08, "input_cost_per_audio_token": 1e-06, "input_cost_per_token": 3e-07, - "litellm_provider": "vertex_ai-language-models", + "litellm_provider": "gemini", "max_audio_length_hours": 8.4, "max_audio_per_prompt": 1, "supports_reasoning": false, diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx index e33e2fff491..eb546378776 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx @@ -33,8 +33,9 @@ vi.mock("./MCPPermissionManagement", () => ({ default: () =>
, })); +const MockMCPToolConfiguration = vi.fn(() =>
); vi.mock("./mcp_tool_configuration", () => ({ - default: () =>
, + default: (props: any) => MockMCPToolConfiguration(props), })); describe("MCPServerEdit (stdio)", () => { @@ -152,3 +153,99 @@ describe("MCPServerEdit (stdio)", () => { expect(payload.env).toEqual({ CIRCLECI_TOKEN: "new-token", CIRCLECI_BASE_URL: "https://circleci.com" }); }); }); + +describe("MCPServerEdit (URL change clears allowed tools)", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should pass live URL to MCPToolConfiguration when URL field changes", async () => { + render( + , + ); + + // Initially, MCPToolConfiguration should receive the original URL + const initialCall = MockMCPToolConfiguration.mock.calls.find( + (call) => call[0]?.formValues?.url === "https://exa.example.com/mcp" + ); + expect(initialCall).toBeDefined(); + + // Change the URL field + const urlInput = screen.getByLabelText("MCP Server URL"); + await act(async () => { + fireEvent.change(urlInput, { + target: { value: "https://seolinkmap.com/mcp" }, + }); + }); + + // After URL change, MCPToolConfiguration should receive the new URL + await waitFor(() => { + const latestCall = MockMCPToolConfiguration.mock.calls[MockMCPToolConfiguration.mock.calls.length - 1]; + expect(latestCall[0].formValues.url).toBe("https://seolinkmap.com/mcp"); + }); + }); + + it("should clear allowedTools when URL changes from original", async () => { + render( + , + ); + + // Initially, allowedTools should be the existing tools + const initialCall = MockMCPToolConfiguration.mock.calls[MockMCPToolConfiguration.mock.calls.length - 1]; + expect(initialCall[0].allowedTools).toEqual(["web_search_exa", "get_code_context_exa"]); + + // Change the URL field to a different server + const urlInput = screen.getByLabelText("MCP Server URL"); + await act(async () => { + fireEvent.change(urlInput, { + target: { value: "https://seolinkmap.com/mcp" }, + }); + }); + + // After URL change, allowedTools should be cleared + await waitFor(() => { + const latestCall = MockMCPToolConfiguration.mock.calls[MockMCPToolConfiguration.mock.calls.length - 1]; + expect(latestCall[0].allowedTools).toEqual([]); + }); + }); +}); 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 4209d8bf111..c4f1bc85832 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 @@ -41,6 +41,8 @@ const MCPServerEdit: React.FC = ({ const [pendingRestoredValues, setPendingRestoredValues] = useState | null>(null); const authType = Form.useWatch("auth_type", form) as string | undefined; const transportType = Form.useWatch("transport", form) as string | undefined; + const urlValue = Form.useWatch("url", form) as string | undefined; + const specPathValue = Form.useWatch("spec_path", form) as string | undefined; const isStdioTransport = transportType === "stdio"; const isOpenAPITransport = transportType === TRANSPORT.OPENAPI; const isMCPTransport = !isStdioTransport && !isOpenAPITransport; @@ -191,6 +193,16 @@ const MCPServerEdit: React.FC = ({ } }, [mcpServer]); + // Clear allowed tools when URL or spec_path changes to a different server + // so that the auto-select logic in MCPToolConfiguration picks up the new tools + useEffect(() => { + const currentEndpoint = urlValue ?? specPathValue; + const originalEndpoint = mcpServer.url ?? mcpServer.spec_path; + if (currentEndpoint !== undefined && currentEndpoint !== originalEndpoint) { + setAllowedTools([]); + } + }, [urlValue, specPathValue, mcpServer.url, mcpServer.spec_path]); + useEffect(() => { if (typeof window === "undefined") { return; @@ -880,9 +892,10 @@ const MCPServerEdit: React.FC = ({ formValues={{ server_id: mcpServer.server_id, server_name: mcpServer.server_name, - url: mcpServer.url, - transport: mcpServer.transport, - auth_type: mcpServer.auth_type, + url: urlValue ?? mcpServer.url, + spec_path: specPathValue ?? mcpServer.spec_path, + transport: transportType ?? mcpServer.transport, + auth_type: authType ?? mcpServer.auth_type, mcp_info: mcpServer.mcp_info, oauth_flow_type: mcpServer.token_url ? OAUTH_FLOW.M2M : OAUTH_FLOW.INTERACTIVE, }} diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx index 404172623b9..46b3f3ce8aa 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx @@ -21,7 +21,7 @@ const MCPToolConfiguration: React.FC = ({ existingAllowedTools, onAllowedToolsChange, }) => { - const previousToolsLengthRef = useRef(0); + const previousToolNamesRef = useRef(""); const [toolSearchTerm, setToolSearchTerm] = useState(""); const { tools, isLoadingTools, toolsError, canFetchTools } = useTestMCPConnection({ @@ -40,27 +40,32 @@ const MCPToolConfiguration: React.FC = ({ ); }); - // Auto-select tools when tools are first loaded + // Auto-select tools when tools are first loaded or when tool list changes (e.g. URL changed) useEffect(() => { + const currentToolNames = tools.map((t) => t.name).sort().join(","); // 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) { + // 2. Tool names changed from last fetch (detects both count and content changes) + // 3. No tools are currently selected (initial state or cleared after URL change) + if (tools.length > 0 && currentToolNames !== previousToolNamesRef.current && allowedTools.length === 0) { 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); + if (validExistingTools.length > 0) { + onAllowedToolsChange(validExistingTools); + } else { + // No existing tools match the new server — select all new tools + onAllowedToolsChange(availableToolNames); + } } 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; + previousToolNamesRef.current = currentToolNames; }, [tools, allowedTools.length, existingAllowedTools, onAllowedToolsChange]); const handleToolToggle = (toolName: string) => {