diff --git a/ui/litellm-dashboard/src/components/mcp_tools/constants.ts b/ui/litellm-dashboard/src/components/mcp_tools/constants.ts index 7115fa1e8c5..95974aae58d 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/constants.ts +++ b/ui/litellm-dashboard/src/components/mcp_tools/constants.ts @@ -1,2 +1,5 @@ // Must match the backend SpecialMCPServerNames.no_mcp_servers enum value. export const NO_MCP_SERVERS_SENTINEL = "no-mcp-servers"; + +export const MCP_TOOLS_PREVIEW_FORBIDDEN_MESSAGE = + "Tool preview is not available for submissions. Tools will be verified by an admin during review."; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx index b45c902b9d7..85fe51c1263 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx @@ -91,7 +91,7 @@ const CreateMCPServer: React.FC = ({ const [oauthDocsUrl, setOauthDocsUrl] = useState(null); // Single hook call shared by MCPConnectionStatus and MCPToolConfiguration to avoid duplicate requests. - const { tools, isLoadingTools, toolsError, toolsErrorStackTrace, canFetchTools, fetchTools, clearTools } = + const { tools, isLoadingTools, toolsError, toolsErrorStatus, toolsErrorStackTrace, canFetchTools, fetchTools, clearTools } = useTestMCPConnection({ accessToken, oauthAccessToken, @@ -1088,6 +1088,7 @@ const CreateMCPServer: React.FC = ({ tools={tools} isLoadingTools={isLoadingTools} toolsError={toolsError} + toolsErrorStatus={toolsErrorStatus} toolsErrorStackTrace={toolsErrorStackTrace} canFetchTools={canFetchTools} fetchTools={fetchTools} @@ -1112,6 +1113,7 @@ const CreateMCPServer: React.FC = ({ externalTools={tools} externalIsLoading={isLoadingTools} externalError={toolsError} + externalErrorStatus={toolsErrorStatus} externalCanFetch={canFetchTools} /> diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_connection_status.test.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_connection_status.test.tsx index b9a33a91422..15e5d886d37 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_connection_status.test.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_connection_status.test.tsx @@ -41,6 +41,21 @@ describe("MCPConnectionStatus", () => { expect(screen.getByText("Connecting...")).toBeInTheDocument(); }); + it("should show info message without retry when tool preview returns 403", () => { + render( + , + ); + + expect(screen.getByRole("alert")).toHaveTextContent(/Tools will be verified by an admin during review/i); + expect(screen.queryByText("Connection Failed")).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /retry/i })).not.toBeInTheDocument(); + }); + it("should show error state with retry button when toolsError is set", async () => { const fetchTools = vi.fn(); const user = userEvent.setup(); diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_connection_status.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_connection_status.tsx index 422cb7273ef..469f372409c 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_connection_status.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_connection_status.tsx @@ -8,6 +8,7 @@ interface MCPConnectionStatusProps { tools: any[]; isLoadingTools: boolean; toolsError: string | null; + toolsErrorStatus?: number | null; toolsErrorStackTrace: string | null; canFetchTools: boolean; fetchTools: () => Promise; @@ -18,10 +19,12 @@ const MCPConnectionStatus: React.FC = ({ tools, isLoadingTools, toolsError, + toolsErrorStatus = null, toolsErrorStackTrace, canFetchTools, fetchTools, }) => { + const isPreviewForbidden = toolsErrorStatus === 403; // Don't show anything if required fields aren't filled if (!canFetchTools && !formValues.url && !formValues.spec_path) { return null; @@ -54,7 +57,9 @@ const MCPConnectionStatus: React.FC = ({ : tools.length > 0 ? "Connection successful" : toolsError - ? "Connection failed" + ? isPreviewForbidden + ? "Ready to submit" + : "Connection failed" : "Ready to test connection"}
@@ -75,7 +80,7 @@ const MCPConnectionStatus: React.FC = ({ )} - {toolsError && ( + {toolsError && !isPreviewForbidden && (
Failed @@ -90,7 +95,11 @@ const MCPConnectionStatus: React.FC = ({
)} - {toolsError && ( + {toolsError && isPreviewForbidden && ( + + )} + + {toolsError && !isPreviewForbidden && ( = ({ externalTools, externalIsLoading, externalError, + externalErrorStatus = null, externalCanFetch, isEditMode = false, }) => { @@ -165,6 +167,7 @@ const MCPToolConfiguration: React.FC = ({ const hasInitializedRef = useRef(false); const previousSuggestedToolNamesRef = useRef(""); const [expandedTools, setExpandedTools] = useState>(new Set()); + const isPreviewForbidden = externalErrorStatus === 403; // Tool list is fetched by the parent (create/edit flow) and passed in. This // component renders that state; it never fetches on its own, so there is a @@ -429,7 +432,13 @@ const MCPToolConfiguration: React.FC = ({ )} {/* Error state */} - {toolsError && !isLoadingTools && ( + {toolsError && !isLoadingTools && isPreviewForbidden && ( +
+ {toolsError} +
+ )} + + {toolsError && !isLoadingTools && !isPreviewForbidden && (
Unable to load tools diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index d982956d7a3..a59c046dfe4 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -31,6 +31,7 @@ import type { SkillRegisterRequest } from "./claude_code_plugins/types"; import { jsonFields } from "./common_components/check_openapi_schema"; import NotificationsManager from "./molecules/notifications_manager"; import type { MCPUserEnvVarsStatus } from "./mcp_tools/types"; +import { MCP_TOOLS_PREVIEW_FORBIDDEN_MESSAGE } from "./mcp_tools/constants"; import { createApiClient, deriveErrorMessage } from "@/lib/http/client"; import { resolveApiBase } from "@/lib/http/resolveApiBase"; import { serverRootPath, setServerRootPath } from "@/lib/serverRootPath"; @@ -6782,17 +6783,25 @@ export const testMCPToolsListRequest = async ( const data = await response.json(); if (!response.ok || data.error) { + if (response.status === 403) { + return { + tools: [], + error: true, + status: 403, + message: MCP_TOOLS_PREVIEW_FORBIDDEN_MESSAGE, + }; + } // Return the error response instead of throwing an error // This allows the caller to handle the error format properly if (data.error) { - return data; // Return the full error response - } else { - return { - tools: [], - error: "request_failed", - message: data.message || `MCP tools list failed: ${response.status} ${response.statusText}`, - }; + return { ...data, status: response.status }; } + return { + tools: [], + error: "request_failed", + status: response.status, + message: data.message || `MCP tools list failed: ${response.status} ${response.statusText}`, + }; } return data; diff --git a/ui/litellm-dashboard/src/hooks/useTestMCPConnection.tsx b/ui/litellm-dashboard/src/hooks/useTestMCPConnection.tsx index b160c90224c..055e15350ca 100644 --- a/ui/litellm-dashboard/src/hooks/useTestMCPConnection.tsx +++ b/ui/litellm-dashboard/src/hooks/useTestMCPConnection.tsx @@ -33,6 +33,7 @@ interface UseTestMCPConnectionReturn { tools: any[]; isLoadingTools: boolean; toolsError: string | null; + toolsErrorStatus: number | null; toolsErrorStackTrace: string | null; hasShownSuccessMessage: boolean; canFetchTools: boolean; @@ -49,6 +50,7 @@ export const useTestMCPConnection = ({ const [tools, setTools] = useState([]); const [isLoadingTools, setIsLoadingTools] = useState(false); const [toolsError, setToolsError] = useState(null); + const [toolsErrorStatus, setToolsErrorStatus] = useState(null); const [toolsErrorStackTrace, setToolsErrorStackTrace] = useState(null); const [hasShownSuccessMessage, setHasShownSuccessMessage] = useState(false); @@ -85,6 +87,7 @@ export const useTestMCPConnection = ({ setIsLoadingTools(true); setToolsError(null); + setToolsErrorStatus(null); try { // Prepare the MCP server config from form values @@ -155,6 +158,7 @@ export const useTestMCPConnection = ({ if (toolsResponse.tools && !toolsResponse.error) { setTools(toolsResponse.tools); setToolsError(null); + setToolsErrorStatus(null); setToolsErrorStackTrace(null); if (toolsResponse.tools.length > 0 && !hasShownSuccessMessage) { setHasShownSuccessMessage(true); @@ -162,13 +166,15 @@ export const useTestMCPConnection = ({ } else { const errorMessage = toolsResponse.message || "Failed to retrieve tools list"; setToolsError(errorMessage); - setToolsErrorStackTrace(toolsResponse.stack_trace || null); + setToolsErrorStatus(typeof toolsResponse.status === "number" ? toolsResponse.status : null); + setToolsErrorStackTrace(toolsResponse.status === 403 ? null : toolsResponse.stack_trace || null); setTools([]); setHasShownSuccessMessage(false); } } catch (error) { console.error("Tools fetch error:", error); setToolsError(error instanceof Error ? error.message : String(error)); + setToolsErrorStatus(null); setToolsErrorStackTrace(null); setTools([]); setHasShownSuccessMessage(false); @@ -180,6 +186,7 @@ export const useTestMCPConnection = ({ const clearTools = useCallback(() => { setTools([]); setToolsError(null); + setToolsErrorStatus(null); setToolsErrorStackTrace(null); setHasShownSuccessMessage(false); }, []); @@ -213,6 +220,7 @@ export const useTestMCPConnection = ({ tools, isLoadingTools, toolsError, + toolsErrorStatus, toolsErrorStackTrace, hasShownSuccessMessage, canFetchTools,