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:
ryan-crabbe-berri 2026-08-18 14:07:36 -07:00 • committed by GitHub
parent 5997ef0423
commit fca470a64b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 187 additions and 105 deletions

View file

@ -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

View file

@ -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>

View file

@ -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>
)}

View file

@ -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"

View file

@ -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>

View file

@ -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();
});
});

View file

@ -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>
);

View file

@ -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);
});
});

View file

@ -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>
);
};