fix(ui): make playground MCP work on all tool-capable endpoints

Route chat, responses, anthropic, and interactions through the shared
buildMcpToolBlocks helper so MCP server_url/label resolution is correct
for any model. Chat was labeling every server as "litellm" and preferring
aliases; responses used absolute /mcp URLs that broke the all-servers path
This commit is contained in:
mubashir1osmani 2026-08-06 15:18:31 -07:00
parent 609f7beddf
commit e99ef9d5dd
7 changed files with 78 additions and 107 deletions

View file

@ -249,6 +249,30 @@ describe("ChatUI", () => {
});
});
it("should enable the MCP tools selector for interactions", async () => {
render(
<ChatUI
accessToken="1234567890"
token="1234567890"
userRole="user"
userID="1234567890"
disabledPersonalKeyCreation={false}
/>,
);
await waitFor(() => {
expect(screen.getByText("Test Key")).toBeInTheDocument();
});
const mcpInput = () => screen.getByLabelText("Select MCP servers");
await selectComboboxOption("Select an endpoint", "/v1beta/interactions");
await waitFor(() => {
expect(mcpInput()).not.toBeDisabled();
});
});
it("should show Simulate failure to test fallbacks in Model Settings when chat endpoint is selected", async () => {
const user = userEvent.setup();
render(

View file

@ -103,6 +103,7 @@ const MCP_SUPPORTED_ENDPOINTS = new Set<EndpointType>([
EndpointType.RESPONSES,
EndpointType.MCP,
EndpointType.ANTHROPIC_MESSAGES,
EndpointType.INTERACTIONS,
]);
const CUSTOM_MODEL_DEBOUNCE_WAIT_MS = 500;
@ -1070,6 +1071,11 @@ const ChatUI: React.FC<ChatUIProps> = ({
selectedTags,
signal,
customProxyBaseUrl || undefined,
undefined,
selectedMCPServers,
mcpServers,
mcpServerToolRestrictions,
mcpToolsets,
);
}
}

View file

@ -1,5 +1,7 @@
import NotificationManager from "@/components/molecules/notifications_manager";
import { getGlobalLitellmHeaderName, getProxyBaseUrl } from "@/components/networking";
import { buildMcpToolBlocks } from "@/components/llm_calls/mcp_tool_blocks";
import type { MCPServer, MCPToolset } from "@/components/mcp_tools/types";
export async function makeInteractionsRequest(
input: string,
@ -10,6 +12,10 @@ export async function makeInteractionsRequest(
signal?: AbortSignal,
customBaseUrl?: string,
previousInteractionId?: string,
selectedMCPServers?: string[],
mcpServers?: MCPServer[],
mcpServerToolRestrictions?: Record<string, string[]>,
mcpToolsets?: MCPToolset[],
): Promise<void> {
if (!accessToken) {
throw new Error("Virtual Key is required");
@ -32,10 +38,18 @@ export async function makeInteractionsRequest(
headers["x-litellm-tags"] = tags.join(",");
}
const tools = buildMcpToolBlocks({
selectedMCPServers,
mcpServers,
mcpToolsets,
mcpServerToolRestrictions,
});
const body: Record<string, unknown> = {
model: selectedModel,
input,
stream: true,
...(tools.length > 0 ? { tools } : {}),
};
if (previousInteractionId) {
body.previous_interaction_id = previousInteractionId;

View file

@ -172,23 +172,22 @@ describe("chat_completion", () => {
const callArgs = mockCreate.mock.calls[0][0];
expect(callArgs.tool_choice).toBe("auto");
expect(callArgs.tools).toHaveLength(2);
// Check first tool
const firstTool = callArgs.tools[0];
expect(firstTool.type).toBe("mcp");
expect(firstTool.server_label).toBe("litellm");
expect(firstTool.server_url).toBe("litellm_proxy/mcp/alpha");
expect(firstTool.require_approval).toBe("never");
expect(firstTool.allowed_tools).toEqual(["toolA", "toolB"]);
// Check second tool
const secondTool = callArgs.tools[1];
expect(secondTool.type).toBe("mcp");
expect(secondTool.server_label).toBe("litellm");
expect(secondTool.server_url).toBe("litellm_proxy/mcp/Beta");
expect(secondTool.require_approval).toBe("never");
expect(secondTool.allowed_tools).toEqual(["toolC"]);
expect(callArgs.tools).toEqual([
{
type: "mcp",
server_label: "Alpha",
server_url: "litellm_proxy/mcp/Alpha",
require_approval: "never",
allowed_tools: ["toolA", "toolB"],
},
{
type: "mcp",
server_label: "Beta",
server_url: "litellm_proxy/mcp/Beta",
require_approval: "never",
allowed_tools: ["toolC"],
},
]);
});
it("should include mock_testing_fallbacks in request body when mockTestFallbacks is true", async () => {

View file

@ -4,6 +4,7 @@ import { TokenUsage } from "../chat_ui/ResponseMetrics";
import { VectorStoreSearchResponse } from "../chat_ui/types";
import { getProxyBaseUrl } from "@/components/networking";
import { MCPServer, MCPToolset, type MCPEvent } from "@/components/mcp_tools/types";
import { buildMcpToolBlocks } from "./mcp_tool_blocks";
const completionAsSingleChunk = (completion: ChatCompletion): ChatCompletionChunk =>
({
@ -81,48 +82,12 @@ export async function makeOpenAIChatCompletionRequest(
} = {};
let mcpListToolsProcessed = false;
// Build tools array
const tools: any[] = [];
// Add MCP servers if selected
if (selectedMCPServers && selectedMCPServers.length > 0) {
if (selectedMCPServers.includes("__all__")) {
// All MCP Servers selected
tools.push({
type: "mcp",
server_label: "litellm",
server_url: "litellm_proxy/mcp",
require_approval: "never",
});
} else {
// Individual servers/toolsets selected - create one entry per item
selectedMCPServers.forEach((serverId) => {
if (serverId.startsWith("toolset:")) {
const toolsetId = serverId.slice("toolset:".length);
const toolset = mcpToolsets?.find((t) => t.toolset_id === toolsetId);
const toolsetName = toolset?.toolset_name || toolsetId;
tools.push({
type: "mcp",
server_label: toolsetName,
server_url: `litellm_proxy/mcp/${encodeURIComponent(toolsetName)}`,
require_approval: "never",
});
} else {
const server = mcpServers?.find((s) => s.server_id === serverId);
const serverName = server?.alias || server?.server_name || serverId;
const allowedTools = mcpServerToolRestrictions?.[serverId] || [];
tools.push({
type: "mcp",
server_label: "litellm",
server_url: `litellm_proxy/mcp/${serverName}`,
require_approval: "never",
...(allowedTools.length > 0 ? { allowed_tools: allowedTools } : {}),
});
}
});
}
}
const tools = buildMcpToolBlocks({
selectedMCPServers,
mcpServers,
mcpToolsets,
mcpServerToolRestrictions,
});
const requestBody = {
model: selectedModel,

View file

@ -280,14 +280,14 @@ describe("responses_api", () => {
{
type: "mcp",
server_label: "Alpha",
server_url: "https://example.com/mcp/Alpha",
server_url: "litellm_proxy/mcp/Alpha",
require_approval: "never",
allowed_tools: ["toolA"],
},
{
type: "mcp",
server_label: "Beta",
server_url: "https://example.com/mcp/Beta",
server_url: "litellm_proxy/mcp/Beta",
require_approval: "never",
allowed_tools: ["toolB", "toolC"],
},

View file

@ -11,6 +11,7 @@ import {
handleCodeInterpreterCall,
handleCodeInterpreterOutput,
} from "./code_interpreter_handler";
import { buildMcpToolBlocks } from "./mcp_tool_blocks";
export type { CodeInterpreterResult } from "./code_interpreter_handler";
@ -134,53 +135,15 @@ export async function makeOpenAIResponsesRequest(
};
});
// Build tools array
const tools: any[] = [];
const tools: Array<Record<string, unknown>> = [
...buildMcpToolBlocks({
selectedMCPServers,
mcpServers,
mcpToolsets,
mcpServerToolRestrictions,
}),
];
// Add MCP servers if selected
if (selectedMCPServers && selectedMCPServers.length > 0) {
if (selectedMCPServers.includes("__all__")) {
// All MCP Servers selected
tools.push({
type: "mcp",
server_label: "litellm",
server_url: `${proxyBaseUrl}/mcp`,
require_approval: "never",
});
} else {
// Individual servers/toolsets selected - create one entry per item
selectedMCPServers.forEach((serverId) => {
if (serverId.startsWith("toolset:")) {
// Toolset: same /{name}/mcp pattern as individual servers
const toolsetId = serverId.slice("toolset:".length);
const toolset = mcpToolsets?.find((t) => t.toolset_id === toolsetId);
const toolsetName = toolset?.toolset_name || toolsetId;
tools.push({
type: "mcp",
server_label: toolsetName,
server_url: `${proxyBaseUrl}/mcp/${encodeURIComponent(toolsetName)}`,
require_approval: "never",
});
} else {
const server = mcpServers?.find((s) => s.server_id === serverId);
// Use server_name for both routing and labelling. server_name is the
// unique registered identifier; aliases can collide across servers.
const routeName = server?.server_name || serverId;
const allowedTools = mcpServerToolRestrictions?.[serverId] || [];
tools.push({
type: "mcp",
server_label: routeName, // unique per request — collisions cause silent tool-routing failures
server_url: `${proxyBaseUrl}/mcp/${encodeURIComponent(routeName)}`,
require_approval: "never",
...(allowedTools.length > 0 ? { allowed_tools: allowedTools } : {}),
});
}
});
}
}
// Add code_interpreter tool if enabled (OpenAI auto-creates container)
if (codeInterpreterEnabled) {
tools.push({
type: "code_interpreter",