diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index d51704551e4..a8bae24e30e 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -623,7 +623,7 @@ "count": 1 }, "no-restricted-imports": { - "count": 2 + "count": 1 }, "react-hooks/set-state-in-effect": { "count": 4 @@ -669,7 +669,7 @@ "count": 1 }, "no-restricted-imports": { - "count": 2 + "count": 1 }, "react-hooks/set-state-in-effect": { "count": 1 @@ -685,7 +685,7 @@ "count": 1 }, "no-restricted-imports": { - "count": 2 + "count": 1 } }, "src/app/(dashboard)/mcp-servers/_components/OpenAPIFormSection.tsx": { @@ -736,7 +736,7 @@ "count": 3 }, "no-restricted-imports": { - "count": 2 + "count": 1 }, "react-hooks/set-state-in-effect": { "count": 1 @@ -760,7 +760,7 @@ "count": 1 }, "no-restricted-imports": { - "count": 2 + "count": 1 }, "react-hooks/static-components": { "count": 4 @@ -803,7 +803,7 @@ "count": 1 }, "no-restricted-imports": { - "count": 2 + "count": 1 }, "react-hooks/immutability": { "count": 1 diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx index 33887c250ca..ed8d3bddc98 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx @@ -1,7 +1,9 @@ import React, { useState } from "react"; -import { Modal, Tooltip, Form, Select, Input, InputNumber, Collapse } from "antd"; +import { Modal, Tooltip, Form, Select, Input as AntdInput, InputNumber, Collapse } from "antd"; import { InfoCircleOutlined } from "@ant-design/icons"; -import { Button, TextInput } from "@tremor/react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { createMCPServer, registerMCPServer, storeMCPOAuthUserCredential } from "@/components/networking"; import { setToken } from "@/utils/mcpTokenStore"; import { @@ -663,7 +665,7 @@ const CreateMCPServer: React.FC = ({ { validator: (_, value) => validateMCPServerName(value) }, ]} > - @@ -681,7 +683,7 @@ const CreateMCPServer: React.FC = ({ name="alias" rules={[{ required: false }, { validator: (_, value) => validateMCPServerName(value) }]} > - setAliasManuallyEdited(true)} @@ -698,7 +700,7 @@ const CreateMCPServer: React.FC = ({ }, ]} > - @@ -710,7 +712,7 @@ const CreateMCPServer: React.FC = ({ label={GitHub / Source URL} name="source_url" > - @@ -745,7 +747,7 @@ const CreateMCPServer: React.FC = ({ { validator: (_, value) => validateMCPServerUrl(value) }, ]} > - @@ -852,8 +854,7 @@ const CreateMCPServer: React.FC = ({ }, ]} > - @@ -957,7 +958,8 @@ const CreateMCPServer: React.FC = ({ - diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPToolsetsTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPToolsetsTab.tsx index 0bced76e24e..a7a56fe040f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPToolsetsTab.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPToolsetsTab.tsx @@ -1,9 +1,9 @@ import React, { useState, useCallback } from "react"; -import { Button, Text, Title } from "@tremor/react"; +import { Button } from "@/components/ui/button"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { Modal, Form, Input, message, Spin } from "antd"; -import { PlusIcon } from "@heroicons/react/outline"; import { SortingState } from "@tanstack/react-table"; -import { Inbox } from "lucide-react"; +import { Inbox, Plus } from "lucide-react"; import { useMCPToolsets } from "@/app/(dashboard)/hooks/mcpServers/useMCPToolsets"; import { useMCPServers } from "@/app/(dashboard)/hooks/mcpServers/useMCPServers"; import { useQueryClient } from "@tanstack/react-query"; @@ -212,7 +212,7 @@ function CreateToolsetModal({ open, onClose, onSave, accessToken, initialToolset {/* Left panel: Available Tools */}
- Available Tools +

Available Tools

{filteredServers.length === 0 ? ( - +

{mcpServers.length === 0 ? "No MCP servers configured" : "No servers match your search"} - +

) : ( filteredServers.map((server) => ( - +

Your Toolset ({selectedTools.length} tools) - +

{selectedTools.length === 0 ? ( - No tools added yet +

No tools added yet

) : ( selectedTools.map((tool, idx) => ( -
@@ -406,14 +407,15 @@ export function MCPToolsetsTab({ accessToken, userRole }: MCPToolsetsTabProps) {
- MCP Toolsets - +

MCP Toolsets

+

Curated collections of tools from one or more MCP servers. Assign toolsets to keys and teams via the MCP permissions dropdown. - +

{isAdmin && ( - )} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/OAuthFormFields.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/OAuthFormFields.tsx index 5650bd1d7e4..cbe6ac18d22 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/OAuthFormFields.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/OAuthFormFields.tsx @@ -1,7 +1,8 @@ import React from "react"; -import { Form, Input, InputNumber, Select, Tooltip } from "antd"; +import { Form, Input as AntdInput, InputNumber, Select, Tooltip } from "antd"; import { InfoCircleOutlined } from "@ant-design/icons"; -import { Button, TextInput } from "@tremor/react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { OAUTH_FLOW } from "@/components/mcp_tools/types"; import TokenEndpointAuthMethodField from "./TokenEndpointAuthMethodField"; @@ -44,7 +45,7 @@ const UpstreamResourceField: React.FC = () => ( label={} name={["credentials", "upstream_resource"]} > - + ); @@ -93,11 +94,7 @@ const OAuthFormFields: React.FC = ({ name={["credentials", "client_id"]} rules={requiredWhenCreating("Client ID is required for M2M OAuth")} > - + = ({ name={["credentials", "client_secret"]} rules={requiredWhenCreating("Client Secret is required for M2M OAuth")} > - @@ -117,7 +113,7 @@ const OAuthFormFields: React.FC = ({ name="token_url" rules={requiredWhenCreating("Token URL is required for M2M OAuth")} > - + = ({ } name={["credentials", "client_id"]} > - + = ({ } name={["credentials", "client_secret"]} > - + = ({ } name="issuer" > - + = ({ } name="authorization_url" > - + } name="token_url" > - + = ({ } name="registration_url" > - + = ({ }, ]} > - => @@ -335,10 +338,14 @@ export function ToolTestPanel({

Provider: {tool.mcp_info.server_name}

-
@@ -369,7 +376,7 @@ export function ToolTestPanel({ rules={[{ required: true, message: "Please enter input for this tool" }]} className="mb-3" > - @@ -466,7 +473,7 @@ export function ToolTestPanel({ )} {prop.type === "string" && !prop.enum && ( - form.submit()} disabled={isLoading} - variant="primary" + aria-busy={isLoading} className="w-full" - loading={isLoading} > + {isLoading && } {isLoading ? "Calling Tool..." : result || error ? "Call Again" : "Call Tool"}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.test.tsx new file mode 100644 index 00000000000..54a32e87b07 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.test.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, it, expect, vi } from "vitest"; +import MCPConnect from "./mcp_connect"; + +vi.mock("@/components/networking", () => ({ + getProxyBaseUrl: vi.fn().mockReturnValue("http://localhost:4000"), +})); + +const activePanel = () => screen.getByRole("tabpanel"); + +describe("MCPConnect (tab mount contract)", () => { + it("keeps the x-mcp-servers header toggle on after switching tabs away and back", async () => { + render(); + + await userEvent.click(screen.getByRole("tab", { name: "LiteLLM Proxy" })); + expect(within(activePanel()).queryByText(/"x-mcp-servers":/)).not.toBeInTheDocument(); + + await userEvent.click(screen.getByRole("switch")); + expect(within(activePanel()).getByText(/"x-mcp-servers": "Zapier_MCP,dev-group"/)).toBeInTheDocument(); + + await userEvent.click(screen.getByRole("tab", { name: "Cursor" })); + await userEvent.click(screen.getByRole("tab", { name: "LiteLLM Proxy" })); + + expect(screen.getByRole("switch")).toBeChecked(); + expect(within(activePanel()).getByText(/"x-mcp-servers": "Zapier_MCP,dev-group"/)).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.tsx index 74b77735377..e32b968c1d3 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { Card, Typography, Space, Alert, Button, Switch, Form } from "antd"; -import { TabPanel, TabPanels, TabGroup, TabList, Tab, Title as TremorTitle, Text as TremorText } from "@tremor/react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { CopyIcon, Code, Terminal, Globe, CheckIcon, ExternalLinkIcon, KeyIcon, ServerIcon, Zap } from "lucide-react"; import { getProxyBaseUrl } from "@/components/networking"; import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils"; @@ -458,57 +458,55 @@ const MCPConnect: React.FC = ({ currentServerAccessGroups = []
- Connect to your MCP client - +

Connect to your MCP client

+

Use tools directly from any MCP client with LiteLLM MCP. Enable your AI assistant to perform real-world tasks through a simple, secure connection. - +

- - -
- + + +
+ OpenAI API - - + + LiteLLM Proxy - - + + Cursor - - + + Streamable HTTP - +
- - - - - - - - - - - - - - - - +
+ + + + + + + + + + + + +
); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.test.tsx index 0aab19cb131..4e85aa41cfc 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.test.tsx @@ -2202,3 +2202,50 @@ describe("MCPServerEdit (dcr_bridge toggle)", () => { expect(payload.dcr_bridge).toBe(true); }); }); + +describe("MCPServerEdit (tab mount contract)", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + const plainServer = { + ...interactiveOAuthServer, + auth_type: "none", + max_concurrent_requests: 5, + }; + + it("carries pending server edits into a save triggered from the Cost Configuration tab", async () => { + vi.mocked(networking.updateMCPServer).mockResolvedValue({ + ...plainServer, + max_concurrent_requests: 2, + }); + + render( + , + ); + + fireEvent.change(screen.getByPlaceholderText("e.g. 10"), { target: { value: "2" } }); + + await userEvent.click(screen.getByRole("tab", { name: "Cost Configuration" })); + expect(await screen.findByTestId("mcp-cost-config")).toBeInTheDocument(); + + const costTabSaveButtons = screen.getAllByRole("button", { name: "Save Changes" }); + expect(costTabSaveButtons).toHaveLength(1); + await act(async () => { + fireEvent.click(costTabSaveButtons[0]); + }); + + await waitFor(() => { + expect(networking.updateMCPServer).toHaveBeenCalledTimes(1); + }); + + const [, payload] = vi.mocked(networking.updateMCPServer).mock.calls[0]; + expect(payload.max_concurrent_requests).toBe(2); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.tsx index 2bae6a7bfcc..5c3fe6d8cda 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.tsx @@ -1,7 +1,8 @@ import React, { useState, useEffect } from "react"; import { Form, Select, Button as AntdButton, Tooltip, Input, InputNumber, Alert } from "antd"; import { InfoCircleOutlined } from "@ant-design/icons"; -import { Button, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { AUTH_TYPE, isClientForwardedTokenMode, @@ -994,13 +995,17 @@ const MCPServerEdit: React.FC = ({ }; return ( - - - Server Configuration - Cost Configuration - - - + + + + Server Configuration + + + Cost Configuration + + +
+
= ({
-
+ - +
@@ -1458,9 +1463,9 @@ const MCPServerEdit: React.FC = ({
- - - + + + ); };