mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
refactor(ui): move the MCP server forms and detail tabs off tremor (#37329)
* refactor(ui): move the MCP server forms and detail tabs off tremor Swaps the tremor Button, TextInput, Title, Text and Tab primitives in the six MCP server components for the shadcn layer, and leaves the antd Modals, Forms and Selects alone for the antd pass. In the two files that mix both input libraries, antd's Input is imported as AntdInput so the shadcn Input keeps its canonical name. Two behaviours needed care. Base UI's Button forces type="button", so the create button in CreateMCPServer now carries an explicit type="submit"; Cancel and the OAuth authorize button stay non submitting, which also drops the accidental implicit submit they inherited from tremor. Every TabsContent gets keepMounted, because Base UI unmounts inactive panels while tremor only hid them, and a save started from the Cost Configuration tab reads fields that live in the Server Configuration panel. mcp_server_edit.test.tsx gains a regression test for that: drop keepMounted and the pending edit never reaches the update payload. One affordance is gone: password fields no longer draw tremor's built in reveal toggle, since the shadcn Input is a plain native input. Prunes the six no-restricted-imports suppressions these files no longer need. * fix(ui): keep the reveal toggle on the MCP secret fields tremor's TextInput drew its own show/hide button whenever the type was password, and the shadcn Input is a plain native input, so the straight prop pass-through silently deleted that affordance from five fields: the create modal's authentication value and the OAuth client id and secret in both the M2M and the interactive flow. Puts them on antd's Input.Password instead, which is what every sibling secret field in this directory already uses (TokenExchangeFormFields, IdJagFormFields, AwsSigV4Fields and the edit form), so the reveal survives and the five fields now match their neighbours instead of behaving differently inside the same form. Both files already import antd, so this adds no import and no suppression. * test(ui): pin the connect tab mount contract mcp_connect's per-card "limit tools to specific MCP servers" toggle lives in panel local state that feeds the rendered header block, so the panels have to stay mounted across a tab switch. Base UI unmounts an inactive panel unless keepMounted is set, and unlike the edit form there was no test holding that down. Toggles the header on from the LiteLLM Proxy panel, switches to Cursor and back, and asserts both the switch and the x-mcp-servers line in the curl example survived. Dropping keepMounted from that panel fails it. * fix(ui): keep the MCP tab strips underlined instead of segmented A bare tremor TabList is variant="line", so the connect strip and the server settings strip both drew an underlined tab on a full width divider. Converting them bare turned each into a filled segmented control, because the shadcn TabsList defaults to the pill. Both strips now use variant="line" with the divider recipe, and the connect strip gets back the grey rounded box tremor drew around its four tabs.
This commit is contained in:
parent
5997ef0423
commit
fca470a64b
9 changed files with 187 additions and 105 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<CreateMCPServerProps> = ({
|
|||
{ validator: (_, value) => validateMCPServerName(value) },
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
<Input
|
||||
placeholder="e.g., GitHub_MCP, Zapier_MCP, etc."
|
||||
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
|
|
@ -681,7 +683,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
name="alias"
|
||||
rules={[{ required: false }, { validator: (_, value) => validateMCPServerName(value) }]}
|
||||
>
|
||||
<TextInput
|
||||
<Input
|
||||
placeholder="e.g., GitHub_MCP, Zapier_MCP, etc."
|
||||
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
onChange={() => setAliasManuallyEdited(true)}
|
||||
|
|
@ -698,7 +700,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
},
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
<Input
|
||||
placeholder="Brief description of what this server does"
|
||||
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
|
|
@ -710,7 +712,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
label={<span className="text-sm font-medium text-gray-700">GitHub / Source URL</span>}
|
||||
name="source_url"
|
||||
>
|
||||
<TextInput
|
||||
<Input
|
||||
placeholder="https://github.com/org/mcp-server"
|
||||
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
|
|
@ -745,7 +747,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
{ validator: (_, value) => validateMCPServerUrl(value) },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
<AntdInput
|
||||
placeholder="https://your-mcp-server.com"
|
||||
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
|
|
@ -852,8 +854,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
},
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
type="password"
|
||||
<AntdInput.Password
|
||||
placeholder="Enter token or secret"
|
||||
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
|
|
@ -957,7 +958,8 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
<Button variant="secondary" onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" loading={isLoading}>
|
||||
<Button type="submit" disabled={isLoading} aria-busy={isLoading}>
|
||||
{isLoading && <UiLoadingSpinner className="size-4" />}
|
||||
{isLoading ? "Creating..." : "Add MCP Server"}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Text className="text-sm font-semibold text-gray-700">Available Tools</Text>
|
||||
<p className="text-sm font-semibold text-gray-700">Available Tools</p>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Search MCP servers..."
|
||||
|
|
@ -223,9 +223,9 @@ function CreateToolsetModal({ open, onClose, onSave, accessToken, initialToolset
|
|||
/>
|
||||
<div className="space-y-2 overflow-y-auto" style={{ maxHeight: 300 }}>
|
||||
{filteredServers.length === 0 ? (
|
||||
<Text className="text-gray-400 text-sm">
|
||||
<p className="text-gray-400 text-sm">
|
||||
{mcpServers.length === 0 ? "No MCP servers configured" : "No servers match your search"}
|
||||
</Text>
|
||||
</p>
|
||||
) : (
|
||||
filteredServers.map((server) => (
|
||||
<MCPToolList
|
||||
|
|
@ -246,12 +246,12 @@ function CreateToolsetModal({ open, onClose, onSave, accessToken, initialToolset
|
|||
|
||||
{/* Right panel: Your Toolset */}
|
||||
<div className="w-72 shrink-0">
|
||||
<Text className="text-sm font-semibold text-gray-700 mb-2 block">
|
||||
<p className="text-sm font-semibold text-gray-700 mb-2 block">
|
||||
Your Toolset <span className="text-xs font-normal text-gray-400">({selectedTools.length} tools)</span>
|
||||
</Text>
|
||||
</p>
|
||||
<div className="space-y-1 overflow-y-auto" style={{ maxHeight: 340 }}>
|
||||
{selectedTools.length === 0 ? (
|
||||
<Text className="text-gray-400 text-sm">No tools added yet</Text>
|
||||
<p className="text-gray-400 text-sm">No tools added yet</p>
|
||||
) : (
|
||||
selectedTools.map((tool, idx) => (
|
||||
<button
|
||||
|
|
@ -278,7 +278,8 @@ function CreateToolsetModal({ open, onClose, onSave, accessToken, initialToolset
|
|||
<Button variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} loading={saving}>
|
||||
<Button onClick={handleSubmit} disabled={saving} aria-busy={saving}>
|
||||
{saving && <UiLoadingSpinner className="size-4" />}
|
||||
{initialToolset ? "Save Changes" : "Create Toolset"}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -406,14 +407,15 @@ export function MCPToolsetsTab({ accessToken, userRole }: MCPToolsetsTabProps) {
|
|||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<Title>MCP Toolsets</Title>
|
||||
<Text className="text-gray-500 text-sm">
|
||||
<h3 className="text-lg font-medium text-gray-900">MCP Toolsets</h3>
|
||||
<p className="text-gray-500 text-sm">
|
||||
Curated collections of tools from one or more MCP servers. Assign toolsets to keys and teams via the MCP
|
||||
permissions dropdown.
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<Button icon={PlusIcon} onClick={() => setCreateOpen(true)}>
|
||||
<Button onClick={() => setCreateOpen(true)}>
|
||||
<Plus />
|
||||
New Toolset
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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={<FieldLabel label="Resource Indicator (optional)" tooltip={UPSTREAM_RESOURCE_TOOLTIP} />}
|
||||
name={["credentials", "upstream_resource"]}
|
||||
>
|
||||
<TextInput placeholder="auto, or https://mcp.example.com/mcp" className={fieldClassName} />
|
||||
<Input placeholder="auto, or https://mcp.example.com/mcp" className={fieldClassName} />
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
|
|
@ -93,11 +94,7 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
|
|||
name={["credentials", "client_id"]}
|
||||
rules={requiredWhenCreating("Client ID is required for M2M OAuth")}
|
||||
>
|
||||
<TextInput
|
||||
type="password"
|
||||
placeholder={`Enter OAuth client ID${placeholderSuffix}`}
|
||||
className={fieldClassName}
|
||||
/>
|
||||
<AntdInput.Password placeholder={`Enter OAuth client ID${placeholderSuffix}`} className={fieldClassName} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
|
|
@ -106,8 +103,7 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
|
|||
name={["credentials", "client_secret"]}
|
||||
rules={requiredWhenCreating("Client Secret is required for M2M OAuth")}
|
||||
>
|
||||
<TextInput
|
||||
type="password"
|
||||
<AntdInput.Password
|
||||
placeholder={`Enter OAuth client secret${placeholderSuffix}`}
|
||||
className={fieldClassName}
|
||||
/>
|
||||
|
|
@ -117,7 +113,7 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
|
|||
name="token_url"
|
||||
rules={requiredWhenCreating("Token URL is required for M2M OAuth")}
|
||||
>
|
||||
<TextInput placeholder="https://auth.example.com/oauth/token" className={fieldClassName} />
|
||||
<Input placeholder="https://auth.example.com/oauth/token" className={fieldClassName} />
|
||||
</Form.Item>
|
||||
<TokenEndpointAuthMethodField isEditing={isEditing} />
|
||||
<Form.Item
|
||||
|
|
@ -157,7 +153,7 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
|
|||
}
|
||||
name={["credentials", "client_id"]}
|
||||
>
|
||||
<TextInput type="password" placeholder={`Enter client ID${placeholderSuffix}`} className={fieldClassName} />
|
||||
<AntdInput.Password placeholder={`Enter client ID${placeholderSuffix}`} className={fieldClassName} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
|
|
@ -168,11 +164,7 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
|
|||
}
|
||||
name={["credentials", "client_secret"]}
|
||||
>
|
||||
<TextInput
|
||||
type="password"
|
||||
placeholder={`Enter client secret${placeholderSuffix}`}
|
||||
className={fieldClassName}
|
||||
/>
|
||||
<AntdInput.Password placeholder={`Enter client secret${placeholderSuffix}`} className={fieldClassName} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
|
|
@ -195,7 +187,7 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
|
|||
}
|
||||
name="issuer"
|
||||
>
|
||||
<TextInput placeholder="https://issuer.example.com" className={fieldClassName} />
|
||||
<Input placeholder="https://issuer.example.com" className={fieldClassName} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
|
|
@ -206,13 +198,13 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
|
|||
}
|
||||
name="authorization_url"
|
||||
>
|
||||
<TextInput placeholder="https://example.com/oauth/authorize" className={fieldClassName} />
|
||||
<Input placeholder="https://example.com/oauth/authorize" className={fieldClassName} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={<FieldLabel label="Token URL (optional)" tooltip="Optional override for the token endpoint." />}
|
||||
name="token_url"
|
||||
>
|
||||
<TextInput placeholder="https://example.com/oauth/token" className={fieldClassName} />
|
||||
<Input placeholder="https://example.com/oauth/token" className={fieldClassName} />
|
||||
</Form.Item>
|
||||
<TokenEndpointAuthMethodField isEditing={isEditing} />
|
||||
<Form.Item
|
||||
|
|
@ -224,7 +216,7 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
|
|||
}
|
||||
name="registration_url"
|
||||
>
|
||||
<TextInput placeholder="https://example.com/oauth/register" className={fieldClassName} />
|
||||
<Input placeholder="https://example.com/oauth/register" className={fieldClassName} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
|
|
@ -248,7 +240,7 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
|
|||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea
|
||||
<AntdInput.TextArea
|
||||
placeholder={'{\n "organization": "my-org",\n "team.id": "123"\n}'}
|
||||
rows={4}
|
||||
className="font-mono text-sm rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import React from "react";
|
||||
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 { MCPTool, InputSchema, InputSchemaProperty } from "@/components/mcp_tools/types";
|
||||
import { resolveLogoSrc } from "@/lib/assetPaths";
|
||||
import { Form, Select, Tooltip } from "antd";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { X } from "lucide-react";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
||||
const isPlainObject = (value: unknown): value is Record<string, any> =>
|
||||
|
|
@ -335,10 +338,14 @@ export function ToolTestPanel({
|
|||
<p className="text-xs text-gray-500">Provider: {tool.mcp_info.server_name}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={onClose} variant="light" size="sm" className="text-gray-500 hover:text-gray-700">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Close"
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -369,7 +376,7 @@ export function ToolTestPanel({
|
|||
rules={[{ required: true, message: "Please enter input for this tool" }]}
|
||||
className="mb-3"
|
||||
>
|
||||
<TextInput
|
||||
<Input
|
||||
placeholder="Enter input for this tool"
|
||||
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
|
|
@ -466,7 +473,7 @@ export function ToolTestPanel({
|
|||
)}
|
||||
|
||||
{prop.type === "string" && !prop.enum && (
|
||||
<TextInput
|
||||
<Input
|
||||
placeholder={prop.description || `Enter ${key}`}
|
||||
defaultValue={(initialValue as string) ?? ""}
|
||||
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
|
|
@ -525,10 +532,10 @@ export function ToolTestPanel({
|
|||
type="button"
|
||||
onClick={() => form.submit()}
|
||||
disabled={isLoading}
|
||||
variant="primary"
|
||||
aria-busy={isLoading}
|
||||
className="w-full"
|
||||
loading={isLoading}
|
||||
>
|
||||
{isLoading && <UiLoadingSpinner className="size-4" />}
|
||||
{isLoading ? "Calling Tool..." : result || error ? "Call Again" : "Call Tool"}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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(<MCPConnect />);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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<MCPConnectProps> = ({ currentServerAccessGroups = []
|
|||
<div>
|
||||
<Space direction="vertical" size="large" className="w-full">
|
||||
<div>
|
||||
<TremorTitle className="text-3xl font-bold text-gray-900 mb-3">Connect to your MCP client</TremorTitle>
|
||||
<TremorText className="text-lg text-gray-600">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-3">Connect to your MCP client</h2>
|
||||
<p className="text-lg text-gray-600">
|
||||
Use tools directly from any MCP client with LiteLLM MCP. Enable your AI assistant to perform real-world
|
||||
tasks through a simple, secure connection.
|
||||
</TremorText>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<TabGroup className="w-full">
|
||||
<TabList className="flex justify-start mt-8 mb-6">
|
||||
<div className="flex bg-gray-100 p-1 rounded-lg">
|
||||
<Tab className="px-6 py-3 rounded-md transition-all duration-200">
|
||||
<Tabs defaultValue="openai" className="w-full">
|
||||
<TabsList variant="line" className="mt-8 mb-6 h-auto w-full justify-start rounded-none border-b p-0">
|
||||
<div className="flex rounded-lg bg-gray-100 p-1">
|
||||
<TabsTrigger value="openai" className="flex-none px-6 py-3">
|
||||
<span className="flex items-center gap-2 font-medium">
|
||||
<Code size={18} />
|
||||
OpenAI API
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab className="px-6 py-3 rounded-md transition-all duration-200">
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="litellm" className="flex-none px-6 py-3">
|
||||
<span className="flex items-center gap-2 font-medium">
|
||||
<Zap size={18} />
|
||||
LiteLLM Proxy
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab className="px-6 py-3 rounded-md transition-all duration-200">
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="cursor" className="flex-none px-6 py-3">
|
||||
<span className="flex items-center gap-2 font-medium">
|
||||
<Terminal size={18} />
|
||||
Cursor
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab className="px-6 py-3 rounded-md transition-all duration-200">
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="http" className="flex-none px-6 py-3">
|
||||
<span className="flex items-center gap-2 font-medium">
|
||||
<Globe size={18} />
|
||||
Streamable HTTP
|
||||
</span>
|
||||
</Tab>
|
||||
</TabsTrigger>
|
||||
</div>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel className="mt-6">
|
||||
<OpenAITab />
|
||||
</TabPanel>
|
||||
<TabPanel className="mt-6">
|
||||
<LiteLLMProxyTab />
|
||||
</TabPanel>
|
||||
<TabPanel className="mt-6">
|
||||
<CursorTab />
|
||||
</TabPanel>
|
||||
<TabPanel className="mt-6">
|
||||
<StreamableHTTPTab />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</TabsList>
|
||||
<TabsContent value="openai" keepMounted className="mt-6">
|
||||
<OpenAITab />
|
||||
</TabsContent>
|
||||
<TabsContent value="litellm" keepMounted className="mt-6">
|
||||
<LiteLLMProxyTab />
|
||||
</TabsContent>
|
||||
<TabsContent value="cursor" keepMounted className="mt-6">
|
||||
<CursorTab />
|
||||
</TabsContent>
|
||||
<TabsContent value="http" keepMounted className="mt-6">
|
||||
<StreamableHTTPTab />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
<MCPServerEdit
|
||||
mcpServer={plainServer}
|
||||
accessToken="access-token"
|
||||
onCancel={vi.fn()}
|
||||
onSuccess={vi.fn()}
|
||||
availableAccessGroups={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<MCPServerEditProps> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<TabGroup>
|
||||
<TabList className="grid w-full grid-cols-2">
|
||||
<Tab>Server Configuration</Tab>
|
||||
<Tab>Cost Configuration</Tab>
|
||||
</TabList>
|
||||
<TabPanels className="mt-6">
|
||||
<TabPanel>
|
||||
<Tabs defaultValue="server">
|
||||
<TabsList variant="line" className="grid h-auto w-full grid-cols-2 rounded-none border-b p-0">
|
||||
<TabsTrigger value="server" className="rounded-none py-2">
|
||||
Server Configuration
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="cost" className="rounded-none py-2">
|
||||
Cost Configuration
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="mt-6">
|
||||
<TabsContent value="server" keepMounted>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleSave}
|
||||
|
|
@ -1447,9 +1452,9 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
<Button type="submit">Save Changes</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</TabPanel>
|
||||
</TabsContent>
|
||||
|
||||
<TabPanel>
|
||||
<TabsContent value="cost" keepMounted>
|
||||
<div className="space-y-6">
|
||||
<MCPServerCostConfig value={costConfig} onChange={setCostConfig} tools={tools} disabled={isLoadingTools} />
|
||||
|
||||
|
|
@ -1458,9 +1463,9 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
<Button onClick={() => form.submit()}>Save Changes</Button>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue