test(e2e): cover MCP api_key tools/list + tools/call over a virtual key

Adds a live e2e for the MCP gateway's api_key auth family: a LiteLLM virtual
key discovers a config-registered upstream server's tools and invokes one, with
the gateway injecting the upstream credential. The upstream is incidental (the
hugging_face server today); the test runs hub_repo_search and asserts a Hub
search for GPT-2 under openai-community surfaces openai-community/gpt2, so a
broken grant, a dropped upstream credential, or a tool that never ran fails
rather than passing on a bare 200.

The server is owned by gateway config: the test looks it up by name, hard-fails
(never skips) when absent, and tears down only the key. Adds server_name to
McpServerRow so the lookup works.

Covers mcp.list_tools.api_key.succeeds and mcp.call_tool.api_key.succeeds.
This commit is contained in:
mubashir1osmani 2026-07-20 17:11:36 -07:00
parent 46a80e1ef5
commit 86e52148c8
2 changed files with 88 additions and 0 deletions

View file

@ -44,6 +44,7 @@ class McpServerNewResponse(BaseModel):
class McpServerRow(BaseModel):
server_id: str
server_name: str | None = None
alias: str | None = None
url: str | None = None

View file

@ -0,0 +1,87 @@
"""Live e2e: the MCP gateway brokers a config-registered upstream server over a virtual key.
Exercises the api_key auth family end to end: a LiteLLM virtual key (x-litellm-api-key)
discovers an upstream MCP server's tools (tools/list) and invokes one (tools/call), with
the gateway injecting the upstream credential itself. The upstream here is the
config-registered "hugging_face" server; it is only the vehicle. The call runs
hub_repo_search and asserts a Hub search for GPT-2 under the openai-community namespace
surfaces openai-community/gpt2, so a broken grant, a dropped upstream credential, or a
tool that never ran fails rather than passing on a bare 200. The server is owned by
gateway config, so the test looks it up by name, hard-fails (never skips) when absent,
and tears down only the virtual key.
"""
from __future__ import annotations
import pytest
from e2e_config import unique_marker
from e2e_http import unwrap
from lifecycle import ResourceManager
from mcp_client import McpClient
pytestmark = pytest.mark.e2e
UPSTREAM_SERVER_NAME = "hugging_face"
SEARCH_TOOL = "hub_repo_search"
SEARCH_QUERY = "gpt2"
SEARCH_AUTHOR = "openai-community"
CANONICAL_REPO_ID = "openai-community/gpt2"
def _find_server(client: McpClient, server_name: str) -> str:
"""Return the server_id of a config-registered MCP server, or hard-fail.
Live e2e never skips for a missing upstream: a gateway without the server is a red
run so the operator knows the suite cannot prove the product path.
"""
for row in client.registered_servers():
if row.server_name == server_name:
return row.server_id
pytest.fail(
f"gateway has no {server_name!r} MCP server registered; add it to the proxy "
"config and restart"
)
class TestMcpApiKeyToolAccess:
@pytest.mark.covers("mcp.list_tools.api_key.succeeds", "mcp.call_tool.api_key.succeeds")
def test_virtual_key_lists_and_calls_upstream_tool(
self,
client: McpClient,
resources: ResourceManager,
) -> None:
server_id = _find_server(client, UPSTREAM_SERVER_NAME)
key = client.generate_key(
user_id=f"e2e-mcp-api-key-{unique_marker()}",
mcp_servers=[server_id],
)
resources.defer(lambda: client.proxy.delete_key(key))
tools = unwrap(client.list_tools(key))
tool_name = tools.tool_name_containing(server_id, SEARCH_TOOL)
assert tool_name is not None, (
f"granted key never saw {SEARCH_TOOL} on server {UPSTREAM_SERVER_NAME!r}; "
f"tools={tools.tool_names_for_server(server_id)}"
)
call = unwrap(
client.call_tool(
key,
server_id=server_id,
name=tool_name,
arguments={
"query": SEARCH_QUERY,
"author": SEARCH_AUTHOR,
"repo_types": ["model"],
"limit": 10,
},
)
)
assert call.is_error is not True, f"{SEARCH_TOOL} errored: {call}"
body = call.all_text
assert CANONICAL_REPO_ID in body, (
f"{SEARCH_TOOL} for {SEARCH_QUERY!r} under {SEARCH_AUTHOR!r} must surface "
f"{CANONICAL_REPO_ID!r}; got: {body[:800]!r}"
)