Command:
diff --git a/ui/litellm-dashboard/src/components/mcp_hub_table_columns.test.tsx b/ui/litellm-dashboard/src/components/mcp_hub_table_columns.test.tsx
new file mode 100644
index 000000000000..ae48f140abdf
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/mcp_hub_table_columns.test.tsx
@@ -0,0 +1,81 @@
+import { render, screen } from "@testing-library/react";
+import { vi } from "vitest";
+import { flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
+import { mcpHubColumns, MCPServerData } from "./mcp_hub_table_columns";
+
+const SERVER_URL = "https://mcp.exa.ai/mcp";
+
+const mockServer: MCPServerData = {
+ server_id: "server-1",
+ server_name: "exa_test",
+ description: "Fast, intelligent web search and web crawling",
+ url: SERVER_URL,
+ transport: "http",
+ auth_type: "none",
+ created_at: "2026-01-01T00:00:00Z",
+ created_by: "admin",
+ updated_at: "2026-01-01T00:00:00Z",
+ updated_by: "admin",
+ teams: [],
+ mcp_access_groups: [],
+ allowed_tools: [],
+ extra_headers: [],
+ mcp_info: {},
+ static_headers: {},
+ status: "active",
+ args: [],
+ env: {},
+};
+
+function TestTable({ data }: { data: MCPServerData[] }) {
+ const columns = mcpHubColumns(vi.fn(), vi.fn(), false);
+ const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() });
+
+ return (
+
+
+ {table.getHeaderGroups().map((hg) => (
+
+ {hg.headers.map((h) => (
+ | {flexRender(h.column.columnDef.header, h.getContext())} |
+ ))}
+
+ ))}
+
+
+ {table.getRowModel().rows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => (
+ | {flexRender(cell.column.columnDef.cell, cell.getContext())} |
+ ))}
+
+ ))}
+
+
+ );
+}
+
+describe("mcpHubColumns", () => {
+ it("renders the server row", () => {
+ render(
);
+ expect(screen.getByText("exa_test")).toBeInTheDocument();
+ });
+
+ it("keeps the non-sensitive columns", () => {
+ render(
);
+ expect(screen.getByText("Server Name")).toBeInTheDocument();
+ expect(screen.getByText("Transport")).toBeInTheDocument();
+ expect(screen.getByText("Auth Type")).toBeInTheDocument();
+ });
+
+ it("does not expose a URL column header", () => {
+ render(
);
+ expect(screen.queryByText("URL")).not.toBeInTheDocument();
+ expect(mcpHubColumns(vi.fn(), vi.fn(), false).some((c) => c.header === "URL")).toBe(false);
+ });
+
+ it("does not render the server url anywhere in the table", () => {
+ render(
);
+ expect(screen.queryByText(SERVER_URL)).not.toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/mcp_hub_table_columns.tsx b/ui/litellm-dashboard/src/components/mcp_hub_table_columns.tsx
index 528780b4ef50..7cf0d48a49f3 100644
--- a/ui/litellm-dashboard/src/components/mcp_hub_table_columns.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_hub_table_columns.tsx
@@ -78,30 +78,6 @@ export const mcpHubColumns = (
className: "hidden md:table-cell",
},
},
- {
- header: "URL",
- accessorKey: "url",
- enableSorting: true,
- sortingFn: "alphanumeric",
- cell: ({ row }) => {
- const server = row.original;
-
- return (
-
- {server.url}
-
- copyToClipboard(server.url)}
- className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs flex-shrink-0"
- />
-
-
- );
- },
- meta: {
- className: "hidden lg:table-cell",
- },
- },
{
header: "Transport",
accessorKey: "transport",
diff --git a/ui/litellm-dashboard/src/components/public_model_hub.test.tsx b/ui/litellm-dashboard/src/components/public_model_hub.test.tsx
index 9979fce5ebd4..9d1e31804ef9 100644
--- a/ui/litellm-dashboard/src/components/public_model_hub.test.tsx
+++ b/ui/litellm-dashboard/src/components/public_model_hub.test.tsx
@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeAll, beforeEach } from "vitest";
-import { render, screen, waitFor } from "@testing-library/react";
-import PublicModelHub from "./public_model_hub";
+import { render, screen, waitFor, fireEvent } from "@testing-library/react";
+import { flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
+import PublicModelHub, { publicMCPHubColumns, MCPServerData } from "./public_model_hub";
vi.mock("next/navigation", () => ({
useRouter: vi.fn(() => ({
@@ -186,3 +187,80 @@ describe("PublicModelHub", () => {
});
});
});
+
+const PUBLIC_SERVER_URL = "https://mcp.exa.ai/mcp";
+
+const mockMcpServer: MCPServerData = {
+ server_id: "server-1",
+ name: "exa_test",
+ server_name: "exa_test",
+ url: PUBLIC_SERVER_URL,
+ transport: "http",
+ auth_type: "none",
+ mcp_info: { server_name: "exa_test", description: "Fast, intelligent web search and web crawling" },
+};
+
+function PublicMcpTestTable({ data }: { data: MCPServerData[] }) {
+ const columns = publicMCPHubColumns(vi.fn());
+ const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() });
+
+ return (
+
+
+ {table.getHeaderGroups().map((hg) => (
+
+ {hg.headers.map((h) => (
+ | {flexRender(h.column.columnDef.header, h.getContext())} |
+ ))}
+
+ ))}
+
+
+ {table.getRowModel().rows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => (
+ | {flexRender(cell.column.columnDef.cell, cell.getContext())} |
+ ))}
+
+ ))}
+
+
+ );
+}
+
+describe("publicMCPHubColumns", () => {
+ it("keeps the non-sensitive columns", () => {
+ render(
);
+ expect(screen.getByText("Server Name")).toBeInTheDocument();
+ expect(screen.getByText("Transport")).toBeInTheDocument();
+ expect(screen.getByText("Auth Type")).toBeInTheDocument();
+ });
+
+ it("does not expose a URL column header", () => {
+ render(
);
+ expect(screen.queryByText("URL")).not.toBeInTheDocument();
+ expect(publicMCPHubColumns(vi.fn()).some((c) => c.header === "URL")).toBe(false);
+ });
+
+ it("does not render the server url anywhere in the table", () => {
+ render(
);
+ expect(screen.queryByText(PUBLIC_SERVER_URL)).not.toBeInTheDocument();
+ });
+});
+
+describe("public hub MCP details modal", () => {
+ it("does not show the upstream url when a server is opened", async () => {
+ const networkingModule = await import("./networking");
+ vi.mocked(networkingModule.mcpHubPublicServersCall).mockResolvedValue([mockMcpServer]);
+
+ render(
);
+
+ fireEvent.click(await screen.findByRole("tab", { name: /MCP Hub/i }));
+ fireEvent.click(await screen.findByRole("button", { name: "exa_test" }));
+
+ // "Server Overview" only exists inside the opened MCP details modal,
+ // so finding it proves the modal rendered and the url assertion is not vacuous.
+ await screen.findByText("Server Overview");
+ expect(screen.queryByText(PUBLIC_SERVER_URL)).not.toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/public_model_hub.tsx b/ui/litellm-dashboard/src/components/public_model_hub.tsx
index 62d8f2644ece..c91b783b0473 100644
--- a/ui/litellm-dashboard/src/components/public_model_hub.tsx
+++ b/ui/litellm-dashboard/src/components/public_model_hub.tsx
@@ -74,12 +74,11 @@ interface AgentCard {
[key: string]: any;
}
-interface MCPServerData {
+export interface MCPServerData {
server_id: string;
name: string;
alias?: string | null;
server_name: string;
- url: string;
transport: string;
spec_path?: string | null;
auth_type: string;
@@ -96,6 +95,73 @@ interface PublicModelHubProps {
isEmbedded?: boolean; // When true, hides navbar and adjusts layout for embedding in dashboard
}
+export const publicMCPHubColumns = (showMcpModal: (server: MCPServerData) => void): ColumnDef
[] => [
+ {
+ header: "Server Name",
+ accessorKey: "server_name",
+ enableSorting: true,
+ cell: ({ row }) => (
+
+
+
+
+
+ ),
+ size: 150,
+ },
+ {
+ header: "Description",
+ accessorKey: "mcp_info.description",
+ enableSorting: false,
+ cell: ({ row }) => {
+ const description = String(row.original.mcp_info?.description ?? "-");
+ const truncated = description.length > 80 ? description.substring(0, 80) + "..." : description;
+ return (
+
+ {truncated}
+
+ );
+ },
+ size: 250,
+ },
+ {
+ header: "Transport",
+ accessorKey: "transport",
+ enableSorting: true,
+ cell: ({ row }) => {
+ const transport = row.original.transport;
+ return (
+
+ {transport}
+
+ );
+ },
+ size: 100,
+ },
+ {
+ header: "Auth Type",
+ accessorKey: "auth_type",
+ enableSorting: true,
+ cell: ({ row }) => {
+ const authType = row.original.auth_type;
+ const color = authType === "none" ? "gray" : "green";
+ return (
+
+ {authType}
+
+ );
+ },
+ size: 100,
+ },
+];
+
const PublicModelHub: React.FC = ({ accessToken, isEmbedded = false }) => {
const [modelHubData, setModelHubData] = useState(null);
const [agentHubData, setAgentHubData] = useState(null);
@@ -889,94 +955,6 @@ const PublicModelHub: React.FC = ({ accessToken, isEmbedded
},
];
- const publicMCPHubColumns = (): ColumnDef[] => [
- {
- header: "Server Name",
- accessorKey: "server_name",
- enableSorting: true,
- cell: ({ row }) => (
-
-
-
-
-
- ),
- size: 150,
- },
- {
- header: "Description",
- accessorKey: "mcp_info.description",
- enableSorting: false,
- cell: ({ row }) => {
- const description = String(row.original.mcp_info?.description ?? "-");
- const truncated = description.length > 80 ? description.substring(0, 80) + "..." : description;
- return (
-
- {truncated}
-
- );
- },
- size: 250,
- },
- {
- header: "URL",
- accessorKey: "url",
- enableSorting: false,
- cell: ({ row }) => {
- const url = row.original.url ?? "";
- const truncated = url.length > 40 ? url.substring(0, 40) + "..." : url;
- return (
-
-
- {truncated}
- copyToClipboard(url)}
- className="cursor-pointer text-gray-500 hover:text-blue-500 w-3 h-3"
- />
-
-
- );
- },
- size: 200,
- },
- {
- header: "Transport",
- accessorKey: "transport",
- enableSorting: true,
- cell: ({ row }) => {
- const transport = row.original.transport;
- return (
-
- {transport}
-
- );
- },
- size: 100,
- },
- {
- header: "Auth Type",
- accessorKey: "auth_type",
- enableSorting: true,
- cell: ({ row }) => {
- const authType = row.original.auth_type;
- const color = authType === "none" ? "gray" : "green";
- return (
-
- {authType}
-
- );
- },
- size: 100,
- },
- ];
-
return (
@@ -1286,7 +1264,7 @@ const PublicModelHub: React.FC
= ({ accessToken, isEmbedded
Description:
{selectedMcpServer.mcp_info?.description || "-"}