diff --git a/ui/litellm-dashboard/src/components/chat/ChatMessages.tsx b/ui/litellm-dashboard/src/components/chat/ChatMessages.tsx index 10bf6b61927..dc35707062b 100644 --- a/ui/litellm-dashboard/src/components/chat/ChatMessages.tsx +++ b/ui/litellm-dashboard/src/components/chat/ChatMessages.tsx @@ -8,6 +8,7 @@ import remarkGfm from "remark-gfm"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { coy } from "react-syntax-highlighter/dist/esm/styles/prism"; import ReasoningContent from "../playground/chat_ui/ReasoningContent"; +import MCPEventsDisplay, { MCPEvent } from "../playground/chat_ui/MCPEventsDisplay"; import { ChatMessage } from "./types"; const { Panel } = Collapse; @@ -237,6 +238,7 @@ interface AssistantBubbleProps { isLastMessage: boolean; isStreaming: boolean; isTypingIndicator: boolean; + mcpEvents?: MCPEvent[]; } function AssistantBubble({ @@ -244,6 +246,7 @@ function AssistantBubble({ isLastMessage, isStreaming, isTypingIndicator, + mcpEvents, }: AssistantBubbleProps) { // Ref to control ReasoningContent collapse on streaming end. // ReasoningContent manages its own expanded state; we use a key to @@ -299,6 +302,13 @@ function AssistantBubble({ ) )} + {/* MCP tool events (list tools, tool calls, results) */} + {mcpEvents && mcpEvents.length > 0 && ( +
+ +
+ )} +
void; + mcpEvents?: MCPEvent[]; } -const ChatMessages: React.FC = ({ messages, isStreaming, onEditMessage }) => { +const ChatMessages: React.FC = ({ messages, isStreaming, onEditMessage, mcpEvents }) => { // Scrolling is managed by ChatPage.tsx (scroll lock during streaming, // scroll-to-bottom on new message). No auto-scroll here. @@ -566,6 +577,7 @@ const ChatMessages: React.FC = ({ messages, isStreaming, onEditMessage }) isLastMessage={isLastMessage} isStreaming={isStreaming} isTypingIndicator={isLastMessage && isTypingIndicator} + mcpEvents={isLastMessage ? mcpEvents : undefined} /> ); })} diff --git a/ui/litellm-dashboard/src/components/chat/ChatPage.tsx b/ui/litellm-dashboard/src/components/chat/ChatPage.tsx index 4f40b2bf37b..830fb128a8e 100644 --- a/ui/litellm-dashboard/src/components/chat/ChatPage.tsx +++ b/ui/litellm-dashboard/src/components/chat/ChatPage.tsx @@ -26,7 +26,9 @@ import MCPConnectPicker from "./MCPConnectPicker"; import MCPAppsPanel from "./MCPAppsPanel"; import { fetchAvailableModels } from "../playground/llm_calls/fetch_models"; import { makeOpenAIChatCompletionRequest } from "../playground/llm_calls/chat_completion"; -import { serverRootPath, getProxyBaseUrl } from "@/components/networking"; +import { MCPEvent } from "../playground/chat_ui/MCPEventsDisplay"; +import { serverRootPath, getProxyBaseUrl, fetchMCPServers } from "@/components/networking"; +import { MCPServer } from "../mcp_tools/types"; import { getProviderLogoAndName } from "@/components/provider_info_helpers"; interface ChatPageProps { @@ -100,6 +102,8 @@ async function streamToModel( signal: AbortSignal, onChunk: (model: string, chunk: string) => void, onDone: (model: string) => void, + mcpServerObjects?: MCPServer[], + onMCPEvent?: (event: MCPEvent) => void, ): Promise { try { await makeOpenAIChatCompletionRequest( @@ -112,9 +116,12 @@ async function streamToModel( undefined, // onReasoningContent undefined, undefined, undefined, undefined, undefined, undefined, // positions 8-13 mcpServers.length > 0 ? mcpServers : undefined, // position 14: selectedMCPServers + undefined, undefined, undefined, undefined, undefined, undefined, // positions 15-20 + mcpServerObjects, // position 21: mcpServers (MCPServer[]) + undefined, // position 22: mcpServerToolRestrictions + onMCPEvent, // position 23: onMCPEvent ); } catch (err: unknown) { - // Surface real errors in the response card; ignore user-triggered aborts if (!(err instanceof Error && err.name === "AbortError")) { const msg = err instanceof Error ? err.message : String(err); onChunk(model, `\n\n_Error: ${msg}_`); @@ -137,6 +144,8 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user const [modelSearchText, setModelSearchText] = useState(""); const [selectedMCPServers, setSelectedMCPServers] = useState([]); + const [mcpServerObjects, setMcpServerObjects] = useState([]); + const [mcpEvents, setMcpEvents] = useState([]); const [isStreaming, setIsStreaming] = useState(false); const [inputText, setInputText] = useState(""); const [mcpPopoverOpen, setMcpPopoverOpen] = useState(false); @@ -201,6 +210,27 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user .finally(() => setIsLoadingModels(false)); }, [accessToken]); + // Load MCP server objects for tool resolution + useEffect(() => { + if (!accessToken) return; + fetchMCPServers(accessToken) + .then((data) => { + const list: MCPServer[] = Array.isArray(data) ? data : (data?.data ?? []); + setMcpServerObjects(list); + }) + .catch(() => setMcpServerObjects([])); + }, [accessToken]); + + const handleMCPEvent = useCallback((event: MCPEvent) => { + setMcpEvents((prev) => { + const isDuplicate = event.item_id + ? prev.some((e) => e.item_id === event.item_id && e.type === event.type) + : false; + if (isDuplicate) return prev; + return [...prev, event]; + }); + }, []); + useEffect(() => { if (staleId) router.replace(getChatUrl()); }, [staleId, router]); @@ -254,6 +284,7 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user let accumulatedContent = ""; let accumulatedReasoning = ""; + setMcpEvents([]); try { await makeOpenAIChatCompletionRequest( @@ -272,6 +303,10 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user }, undefined, undefined, undefined, undefined, undefined, undefined, selectedMCPServers.length > 0 ? selectedMCPServers : undefined, + undefined, undefined, undefined, undefined, undefined, undefined, + mcpServerObjects.length > 0 ? mcpServerObjects : undefined, + undefined, + handleMCPEvent, ); } catch (err: unknown) { if (err instanceof Error && err.name === "AbortError") { @@ -289,7 +324,8 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user } }, [activeConversationId, activeConversation, selectedModels, selectedMCPServers, accessToken, - createConversation, appendMessage, updateLastAssistantMessage, router, isStreaming], + createConversation, appendMessage, updateLastAssistantMessage, router, isStreaming, + mcpServerObjects, handleMCPEvent], ); const handleSendComparison = useCallback( @@ -297,6 +333,7 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user const trimmed = text.trim(); if (!trimmed || selectedModels.length === 0 || isAnyStreaming) return; setInputText(""); + setMcpEvents([]); // Append a new exchange with empty responses const newExchange: ComparisonExchange = { userMessage: trimmed, responses: {} }; @@ -326,7 +363,7 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user accessToken, selectedMCPServers, controllers[model].signal, - (m, chunk) => setComparisonExchanges((prev) => { + (m, chunk: string) => setComparisonExchanges((prev) => { const updated = [...prev]; const ex = { ...updated[newExchangeIdx] }; ex.responses = { ...ex.responses, [m]: (ex.responses[m] ?? "") + chunk }; @@ -1111,6 +1148,7 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user messages={activeConversation!.messages} isStreaming={isStreaming} onEditMessage={handleEditAndResend} + mcpEvents={mcpEvents} /> )}