test(e2e-ui): check the MCP Tools tab against the upstream's own tools/list (#42397)

* test(e2e-ui): check the MCP Tools tab against the upstream's own tools/list

DeepWiki renamed ask_question to ask_wiki_question, and the spec hardcoded the old name, so
e2e_ui_testing went red on main for something that is not a litellm regression. The spec now asks
the upstream server for its tool list with the official MCP TypeScript SDK and expects the tab to
show exactly those cards, so a vendor rename cannot turn the job red again.

* test(e2e-ui): cite the pinned DeepWiki tool name and drop the helper docstring

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-09-21 20:35:30 -07:00 • committed by GitHub
parent 5cf17f9ce8
commit b96842f62c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 1309 additions and 7 deletions

View file

@ -1,8 +1,21 @@
import { expect, Page as PwPage } from "@playwright/test";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { navigateToPage } from "./navigation";
import { Page } from "../fixtures/pages";
import { masterKey } from "./traffic";
export async function listUpstreamToolNames(url: string): Promise<string[]> {
const client = new Client({ name: "litellm-ui-e2e", version: "0.0.0" });
await client.connect(new StreamableHTTPClientTransport(new URL(url)));
try {
const { tools } = await client.listTools();
return tools.map((tool) => tool.name);
} finally {
await client.close();
}
}
/** Creates an MCP server through the UI's discovery to custom-form flow and returns its name. */
export async function createMcpServer(page: PwPage, url: string): Promise<string> {
await navigateToPage(page, Page.McpServers);

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,7 @@
"e2e:migration:root": "playwright test --config migration.serverRootPath.config.ts"
},
"devDependencies": {
"@modelcontextprotocol/sdk": "1.30.0",
"@playwright/test": "1.58.1",
"@types/node": "20.19.37",
"typescript": "5.9.3"

View file

@ -1,17 +1,20 @@
import { test, expect, Locator } from "@playwright/test";
import { ADMIN_STORAGE_PATH } from "../../constants";
import { createMcpServer, deleteMcpServerByName, openMcpToolsTab } from "../../helpers/mcp";
import { createMcpServer, deleteMcpServerByName, listUpstreamToolNames, openMcpToolsTab } from "../../helpers/mcp";
// Listing and calling MCP tools, which needs a server that really answers; the create-only spec
// points at an unreachable URL on purpose.
//
// This spec makes a read-only network call to DeepWiki's public MCP server, from the proxy rather
// than the browser. It needs no credentials, so there is no secret to leak from a public repo.
// This spec makes read-only network calls to DeepWiki's public MCP server: from the proxy, and from
// the test runner to learn which tools the upstream advertises today, so the tool list is never
// pinned here. It needs no credentials, so there is no secret to leak from a public repo.
//
// A DeepWiki outage turns this red for something that is not a litellm regression. That is left
// visible rather than auto-skipped: skipping on connection trouble also skips when the proxy's own
// MCP client breaks, which is the regression this exists to catch. E2E_SKIP_EXTERNAL_MCP=1 opts out.
const MCP_SERVER_URL = "https://mcp.deepwiki.com/mcp";
// Read from DeepWiki's tools/list on 2026-09-22. One name has to be pinned so the call-tool test can
// fill a known input (repoName); the listing test checks it is still advertised before the UI checks.
const TOOL_NAME = "read_wiki_structure";
const TOOL_ARG_REPO = "BerriAI/litellm";
@ -36,14 +39,17 @@ test.describe("MCP Tools", () => {
});
test("MCP Tools tab lists the tools the upstream server advertises", async ({ page }) => {
const upstreamTools = await listUpstreamToolNames(MCP_SERVER_URL);
expect(upstreamTools).toContain(TOOL_NAME);
// Fetched through the proxy on mount, so allow for a cold upstream connection.
const toolList = page.locator(".mcp-tools-scrollable");
await expect(toolList).toBeVisible({ timeout: 30_000 });
// Non-empty would still pass if the proxy returned some other server's tools.
await expect(toolCard(toolList, TOOL_NAME)).toBeVisible();
await expect(toolCard(toolList, "ask_question")).toBeVisible();
await expect(toolCard(toolList, "read_wiki_contents")).toBeVisible();
for (const name of upstreamTools) {
await expect(toolCard(toolList, name)).toBeVisible();
}
await expect(toolList.locator("h4.font-mono")).toHaveCount(upstreamTools.length);
// No other tool's name or description contains this string, so exactly one card survives.
await page.getByPlaceholder("Search tools...").fill(TOOL_NAME);