mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
feat(ui): show tools per MCP server in chat panel (#23294)
- fix transport display: use handleTransport() instead of hardcoding HTTP/stdio based on server_url presence - show available tools list when clicking into a server detail view - preload tool counts per server card in parallel (one request per server, counts pop in independently) - add skeleton loading indicator on each card while its tool count is fetching - fix: pass server UUID (not name) to /mcp-rest/tools/list — name was always hitting access_denied
This commit is contained in:
parent
6faad2919a
commit
3b80b312a9
2 changed files with 180 additions and 35 deletions
|
|
@ -19553,7 +19553,8 @@
|
|||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_service_tier": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true
|
||||
},
|
||||
"gpt-4.1-2025-04-14": {
|
||||
"cache_read_input_token_cost": 5e-07,
|
||||
|
|
@ -19587,7 +19588,8 @@
|
|||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_service_tier": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true
|
||||
},
|
||||
"gpt-4.1-mini": {
|
||||
"cache_read_input_token_cost": 1e-07,
|
||||
|
|
@ -19624,7 +19626,8 @@
|
|||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_service_tier": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true
|
||||
},
|
||||
"gpt-4.1-mini-2025-04-14": {
|
||||
"cache_read_input_token_cost": 1e-07,
|
||||
|
|
@ -19658,7 +19661,8 @@
|
|||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_service_tier": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true
|
||||
},
|
||||
"gpt-4.1-nano": {
|
||||
"cache_read_input_token_cost": 2.5e-08,
|
||||
|
|
@ -20866,6 +20870,7 @@
|
|||
"supports_tool_choice": true,
|
||||
"supports_service_tier": true,
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_xhigh_reasoning_effort": false
|
||||
},
|
||||
|
|
@ -21505,6 +21510,7 @@
|
|||
"supports_tool_choice": true,
|
||||
"supports_service_tier": true,
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_xhigh_reasoning_effort": false
|
||||
},
|
||||
|
|
@ -21605,6 +21611,7 @@
|
|||
"supports_system_messages": false,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_xhigh_reasoning_effort": false
|
||||
},
|
||||
|
|
@ -21640,6 +21647,7 @@
|
|||
"supports_system_messages": false,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_xhigh_reasoning_effort": false
|
||||
},
|
||||
|
|
@ -21672,6 +21680,7 @@
|
|||
"supports_system_messages": false,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_xhigh_reasoning_effort": true
|
||||
},
|
||||
|
|
@ -21707,6 +21716,7 @@
|
|||
"supports_system_messages": false,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_xhigh_reasoning_effort": false
|
||||
},
|
||||
|
|
@ -21742,6 +21752,7 @@
|
|||
"supports_system_messages": false,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_xhigh_reasoning_effort": true
|
||||
},
|
||||
|
|
@ -21777,6 +21788,7 @@
|
|||
"supports_system_messages": false,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_xhigh_reasoning_effort": false
|
||||
},
|
||||
|
|
@ -21818,6 +21830,7 @@
|
|||
"supports_tool_choice": true,
|
||||
"supports_service_tier": true,
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_xhigh_reasoning_effort": false
|
||||
},
|
||||
|
|
@ -21859,6 +21872,7 @@
|
|||
"supports_tool_choice": true,
|
||||
"supports_service_tier": true,
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_xhigh_reasoning_effort": false
|
||||
},
|
||||
|
|
@ -21897,6 +21911,7 @@
|
|||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_xhigh_reasoning_effort": false
|
||||
},
|
||||
|
|
@ -21934,6 +21949,7 @@
|
|||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_xhigh_reasoning_effort": false
|
||||
},
|
||||
|
|
@ -25721,7 +25737,8 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_service_tier": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true
|
||||
},
|
||||
"o3-2025-04-16": {
|
||||
"cache_read_input_token_cost": 5e-07,
|
||||
|
|
@ -25753,7 +25770,8 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_service_tier": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true
|
||||
},
|
||||
"o3-deep-research": {
|
||||
"cache_read_input_token_cost": 2.5e-06,
|
||||
|
|
@ -25786,7 +25804,8 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true
|
||||
},
|
||||
"o3-deep-research-2025-06-26": {
|
||||
"cache_read_input_token_cost": 2.5e-06,
|
||||
|
|
@ -25819,7 +25838,8 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true
|
||||
},
|
||||
"o3-mini": {
|
||||
"cache_read_input_token_cost": 5.5e-07,
|
||||
|
|
@ -25883,7 +25903,8 @@
|
|||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true
|
||||
},
|
||||
"o3-pro-2025-06-10": {
|
||||
"input_cost_per_token": 2e-05,
|
||||
|
|
@ -25913,7 +25934,8 @@
|
|||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true
|
||||
},
|
||||
"o4-mini": {
|
||||
"cache_read_input_token_cost": 2.75e-07,
|
||||
|
|
@ -25938,7 +25960,8 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_service_tier": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true
|
||||
},
|
||||
"o4-mini-2025-04-16": {
|
||||
"cache_read_input_token_cost": 2.75e-07,
|
||||
|
|
@ -25957,7 +25980,8 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_service_tier": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true
|
||||
},
|
||||
"o4-mini-deep-research": {
|
||||
"cache_read_input_token_cost": 5e-07,
|
||||
|
|
@ -25990,7 +26014,8 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true
|
||||
},
|
||||
"o4-mini-deep-research-2025-06-26": {
|
||||
"cache_read_input_token_cost": 5e-07,
|
||||
|
|
@ -26023,7 +26048,8 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true
|
||||
},
|
||||
"oci/meta.llama-3.1-405b-instruct": {
|
||||
"input_cost_per_token": 1.068e-05,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Switch, Spin, Input, Button } from "antd";
|
||||
import { SearchOutlined, ArrowLeftOutlined, RightOutlined } from "@ant-design/icons";
|
||||
import { Spin, Input, Button, Skeleton } from "antd";
|
||||
import { SearchOutlined, ArrowLeftOutlined, RightOutlined, ToolOutlined } from "@ant-design/icons";
|
||||
import { fetchMCPServers, listMCPTools } from "../networking";
|
||||
import { AUTH_TYPE, MCPServer } from "../mcp_tools/types";
|
||||
import { AUTH_TYPE, MCPServer, MCPTool, handleTransport } from "../mcp_tools/types";
|
||||
import { message } from "antd";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -33,33 +33,65 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
const [activeTab, setActiveTab] = useState<TabKey>("all");
|
||||
const [togglingOn, setTogglingOn] = useState<Set<string>>(new Set());
|
||||
const [detailServer, setDetailServer] = useState<MCPServer | null>(null);
|
||||
const [detailTools, setDetailTools] = useState<MCPTool[]>([]);
|
||||
const [loadingTools, setLoadingTools] = useState(false);
|
||||
// tool counts per server name, preloaded in background
|
||||
const [toolCounts, setToolCounts] = useState<Record<string, number>>({});
|
||||
const [loadingCounts, setLoadingCounts] = useState(false);
|
||||
|
||||
const nameOf = (s: MCPServer) => s.server_name ?? s.alias ?? s.server_id;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
|
||||
// 1. Load servers first — show the list immediately
|
||||
fetchMCPServers(accessToken)
|
||||
.then((data) => {
|
||||
.then((serverData) => {
|
||||
if (cancelled) return;
|
||||
const list: MCPServer[] = Array.isArray(data) ? data : (data?.data ?? []);
|
||||
const list: MCPServer[] = Array.isArray(serverData) ? serverData : (serverData?.data ?? []);
|
||||
setServers(list);
|
||||
setLoading(false);
|
||||
|
||||
// 2. Fetch tools per server in parallel — each resolves independently and updates counts one by one
|
||||
setLoadingCounts(true);
|
||||
let remaining = list.length;
|
||||
if (remaining === 0) { setLoadingCounts(false); return; }
|
||||
list.forEach((s) => {
|
||||
listMCPTools(accessToken, s.server_id)
|
||||
.then((toolsData) => {
|
||||
if (cancelled) return;
|
||||
const tools: MCPTool[] = Array.isArray(toolsData?.tools) ? toolsData.tools : [];
|
||||
const sname = nameOf(s);
|
||||
setToolCounts((prev) => ({ ...prev, [sname]: tools.length }));
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
if (cancelled) return;
|
||||
remaining -= 1;
|
||||
if (remaining === 0) setLoadingCounts(false);
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setServers([]);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
if (!cancelled) {
|
||||
setServers([]);
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [accessToken]);
|
||||
|
||||
const handleToggle = async (serverName: string, checked: boolean) => {
|
||||
const handleToggle = async (serverName: string, checked: boolean, serverId?: string) => {
|
||||
if (!checked) {
|
||||
onChange(selectedServers.filter((s) => s !== serverName));
|
||||
return;
|
||||
}
|
||||
setTogglingOn((prev) => new Set(prev).add(serverName));
|
||||
try {
|
||||
const result = await listMCPTools(accessToken, serverName);
|
||||
// Use UUID if available, fall back to name (for connectivity check only)
|
||||
const idToFetch = serverId ?? serverName;
|
||||
const result = await listMCPTools(accessToken, idToFetch);
|
||||
if (result?.error) {
|
||||
message.warning(`Could not load tools for ${serverName}`);
|
||||
return;
|
||||
|
|
@ -76,7 +108,29 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
}
|
||||
};
|
||||
|
||||
const nameOf = (s: MCPServer) => s.server_name ?? s.alias ?? s.server_id;
|
||||
// Fetch tools for the detail view — server_id must be the UUID
|
||||
useEffect(() => {
|
||||
if (!detailServer) {
|
||||
setDetailTools([]);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
setLoadingTools(true);
|
||||
listMCPTools(accessToken, detailServer.server_id)
|
||||
.then((result) => {
|
||||
if (cancelled) return;
|
||||
// API returns { tools: [...], error: null }
|
||||
const tools: MCPTool[] = Array.isArray(result?.tools) ? result.tools : [];
|
||||
setDetailTools(tools);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setDetailTools([]);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoadingTools(false);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [detailServer, accessToken]);
|
||||
|
||||
const filtered = servers.filter((s) => {
|
||||
const name = nameOf(s);
|
||||
|
|
@ -89,6 +143,9 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
|
||||
const connectedCount = servers.filter((s) => selectedServers.includes(nameOf(s))).length;
|
||||
|
||||
// Total tools available across all servers (based on preloaded counts)
|
||||
const totalTools = Object.values(toolCounts).reduce((sum, n) => sum + n, 0);
|
||||
|
||||
// ── Detail view ──
|
||||
if (detailServer) {
|
||||
const name = nameOf(detailServer);
|
||||
|
|
@ -128,7 +185,7 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
<Button
|
||||
type={isConnected ? "default" : "primary"}
|
||||
loading={isTogglingOn}
|
||||
onClick={() => handleToggle(name, !isConnected)}
|
||||
onClick={() => handleToggle(name, !isConnected, detailServer.server_id)}
|
||||
style={{ borderRadius: 8, fontWeight: 600, height: 38, minWidth: 110 }}
|
||||
>
|
||||
{isConnected ? "Disconnect" : "Connect"}
|
||||
|
|
@ -137,10 +194,10 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
|
||||
{/* Info table */}
|
||||
<h3 style={{ margin: "0 0 12px", fontSize: 15, fontWeight: 600, color: "#111827" }}>Information</h3>
|
||||
<div style={{ border: "1px solid #e5e7eb", borderRadius: 10, overflow: "hidden" }}>
|
||||
<div style={{ border: "1px solid #e5e7eb", borderRadius: 10, overflow: "hidden", marginBottom: 28 }}>
|
||||
{[
|
||||
["Server ID", detailServer.server_id],
|
||||
["Transport", (detailServer as MCPServer & { mcp_info?: { server_url?: string } }).mcp_info?.server_url ? "HTTP" : "stdio"],
|
||||
["Transport", handleTransport(detailServer.transport, detailServer.spec_path)],
|
||||
["Status", isConnected ? "Connected" : "Not connected"],
|
||||
].filter(([, v]) => v).map(([label, value], i, arr) => (
|
||||
<div key={label} style={{
|
||||
|
|
@ -154,6 +211,43 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tools section */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
|
||||
<h3 style={{ margin: 0, fontSize: 15, fontWeight: 600, color: "#111827" }}>Available Tools</h3>
|
||||
{!loadingTools && (
|
||||
<span style={{
|
||||
fontSize: 11, fontWeight: 600, color: "#6b7280",
|
||||
background: "#f3f4f6", borderRadius: 4, padding: "1px 6px",
|
||||
}}>{detailTools.length}</span>
|
||||
)}
|
||||
</div>
|
||||
{loadingTools ? (
|
||||
<div style={{ display: "flex", justifyContent: "center", padding: "24px 0" }}>
|
||||
<Spin size="small" />
|
||||
</div>
|
||||
) : detailTools.length === 0 ? (
|
||||
<div style={{ color: "#9ca3af", fontSize: 13, padding: "8px 0" }}>
|
||||
No tools available
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{detailTools.map((tool) => (
|
||||
<div key={tool.name} style={{
|
||||
border: "1px solid #e5e7eb", borderRadius: 8,
|
||||
padding: "10px 14px", background: "#fafafa",
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: tool.description ? 4 : 0 }}>
|
||||
<ToolOutlined style={{ fontSize: 13, color: "#6b7280" }} />
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: "#111827", fontFamily: "monospace" }}>{tool.name}</span>
|
||||
</div>
|
||||
{tool.description && (
|
||||
<p style={{ margin: 0, fontSize: 12, color: "#6b7280", paddingLeft: 21 }}>{tool.description}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -165,7 +259,7 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
{/* Header row */}
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 20, gap: 16, flexWrap: "wrap" }}>
|
||||
<div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 2 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
|
||||
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600, color: "#111827" }}>MCP Servers</h2>
|
||||
<span style={{
|
||||
fontSize: 10, fontWeight: 600, color: "#1677ff",
|
||||
|
|
@ -173,9 +267,22 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
letterSpacing: "0.05em", textTransform: "uppercase",
|
||||
}}>Beta</span>
|
||||
</div>
|
||||
<p style={{ margin: 0, fontSize: 13, color: "#6b7280" }}>
|
||||
Browse tools, authenticate once, use in chat — no setup needed.
|
||||
</p>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
<p style={{ margin: 0, fontSize: 13, color: "#6b7280" }}>
|
||||
Browse tools, authenticate once, use in chat — no setup needed.
|
||||
</p>
|
||||
{loadingCounts ? (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 5, fontSize: 12, color: "#9ca3af" }}>
|
||||
<Spin size="small" style={{ transform: "scale(0.7)" }} />
|
||||
Loading tools...
|
||||
</span>
|
||||
) : totalTools > 0 ? (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 4, fontSize: 12, color: "#6b7280" }}>
|
||||
<ToolOutlined style={{ fontSize: 11 }} />
|
||||
{totalTools} tool{totalTools !== 1 ? "s" : ""} available
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
prefix={<SearchOutlined style={{ color: "#9ca3af", fontSize: 13 }} />}
|
||||
|
|
@ -227,6 +334,7 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
const isConnected = selectedServers.includes(name);
|
||||
const color = getAvatarColor(name);
|
||||
const isLeftCol = idx % 2 === 0;
|
||||
const count = toolCounts[name];
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -254,8 +362,19 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
|
|||
<div style={{ fontSize: 14, fontWeight: 500, color: "#111827", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{name}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "#9ca3af", marginTop: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{server.description ?? "MCP server"}
|
||||
<div style={{ fontSize: 12, color: "#9ca3af", marginTop: 1, display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{server.description ?? "MCP server"}
|
||||
</span>
|
||||
{count !== undefined ? (
|
||||
count > 0 ? (
|
||||
<span style={{ flexShrink: 0, display: "flex", alignItems: "center", gap: 3, color: "#9ca3af" }}>
|
||||
· <ToolOutlined style={{ fontSize: 10 }} /> {count}
|
||||
</span>
|
||||
) : null
|
||||
) : loadingCounts ? (
|
||||
<Skeleton.Input active size="small" style={{ width: 28, height: 12, minWidth: 28, flexShrink: 0 }} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{isConnected && (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue