mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-05 08:07:05 +00:00
feat: add MCP tool call/result rendering to Chat UI
Port MCP event handling from Playground to the standalone Chat UI: - ChatPage: add mcpEvents state, handleMCPEvent callback, fetch MCPServer[] objects, pass onMCPEvent + mcpServers to makeOpenAIChatCompletionRequest and streamToModel - ChatMessages: accept mcpEvents prop, render MCPEventsDisplay (list tools, tool calls with request/response) inside the last assistant bubble, reusing the existing Playground component - Reset mcpEvents on each new send Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>
This commit is contained in:
parent
bc9a48d407
commit
efe079de23
2 changed files with 55 additions and 5 deletions
|
|
@ -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 && (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<MCPEventsDisplay events={mcpEvents} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: 14,
|
||||
|
|
@ -531,9 +541,10 @@ interface Props {
|
|||
messages: ChatMessage[];
|
||||
isStreaming: boolean;
|
||||
onEditMessage?: (messageId: string, newContent: string) => void;
|
||||
mcpEvents?: MCPEvent[];
|
||||
}
|
||||
|
||||
const ChatMessages: React.FC<Props> = ({ messages, isStreaming, onEditMessage }) => {
|
||||
const ChatMessages: React.FC<Props> = ({ 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<Props> = ({ messages, isStreaming, onEditMessage })
|
|||
isLastMessage={isLastMessage}
|
||||
isStreaming={isStreaming}
|
||||
isTypingIndicator={isLastMessage && isTypingIndicator}
|
||||
mcpEvents={isLastMessage ? mcpEvents : undefined}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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<ChatPageProps> = ({ accessToken, userRole, userId, user
|
|||
const [modelSearchText, setModelSearchText] = useState("");
|
||||
|
||||
const [selectedMCPServers, setSelectedMCPServers] = useState<string[]>([]);
|
||||
const [mcpServerObjects, setMcpServerObjects] = useState<MCPServer[]>([]);
|
||||
const [mcpEvents, setMcpEvents] = useState<MCPEvent[]>([]);
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const [inputText, setInputText] = useState("");
|
||||
const [mcpPopoverOpen, setMcpPopoverOpen] = useState(false);
|
||||
|
|
@ -201,6 +210,27 @@ const ChatPage: React.FC<ChatPageProps> = ({ 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<ChatPageProps> = ({ accessToken, userRole, userId, user
|
|||
|
||||
let accumulatedContent = "";
|
||||
let accumulatedReasoning = "";
|
||||
setMcpEvents([]);
|
||||
|
||||
try {
|
||||
await makeOpenAIChatCompletionRequest(
|
||||
|
|
@ -272,6 +303,10 @@ const ChatPage: React.FC<ChatPageProps> = ({ 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<ChatPageProps> = ({ 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<ChatPageProps> = ({ 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<ChatPageProps> = ({ 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<ChatPageProps> = ({ accessToken, userRole, userId, user
|
|||
messages={activeConversation!.messages}
|
||||
isStreaming={isStreaming}
|
||||
onEditMessage={handleEditAndResend}
|
||||
mcpEvents={mcpEvents}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue