feat(proxy): allow llm_api_routes virtual keys to list MCP tools via /v1/mcp/tools (#31031)

* feat(proxy): allow llm_api_routes virtual keys to list MCP tools via /v1/mcp/tools

GET /v1/mcp/tools returns the MCP tools available to the calling key, the same
data already exposed through /mcp/tools/list and /mcp-rest/tools/list, both of
which are in llm_api_routes. The /v1/mcp/tools path was in no route group, so
virtual keys created from the UI (which default to allowed_routes=["llm_api_routes"])
got a 403 listing tools one way but not the other.

Add it to mcp_inference_routes. Unlike /v1/mcp/server, this path has no
management write counterpart, so it does not need the method-aware carve-out
used for server discovery.

* test(proxy): parametrize MCP inference route check over the full endpoint set
This commit is contained in:
ryan-crabbe-berri 2026-06-22 17:36:26 -07:00 • committed by GitHub
parent 26da56fbb6
commit 3615049071
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 43 additions and 0 deletions

View file

@ -461,6 +461,7 @@ class LiteLLMRoutes(enum.Enum):
"/mcp/tools/call",
"/mcp-rest/tools/list",
"/mcp-rest/tools/call",
"/v1/mcp/tools",
]
# MCP server CRUD routes — control-plane. Gated by DISABLE_ADMIN_ENDPOINTS.

View file

@ -403,6 +403,48 @@ def test_virtual_key_llm_api_routes_rejects_mcp_multi_segment_admin_subpaths(
assert exc_info.value.status_code == 403
@pytest.mark.parametrize(
"route, method",
[
("/mcp", "POST"),
("/mcp/", "POST"),
("/mcp/my-server", "POST"), # matches the /mcp/{subpath} pattern
("/mcp/tools", "GET"),
("/mcp/tools/list", "POST"),
("/mcp/tools/call", "POST"),
("/mcp-rest/tools/list", "GET"),
("/mcp-rest/tools/call", "POST"),
("/v1/mcp/tools", "GET"),
],
)
def test_virtual_key_llm_api_routes_allows_mcp_inference_endpoints(route, method):
"""Every MCP inference/discovery endpoint must be reachable by virtual keys
scoped to allowed_routes=["llm_api_routes"], the default the Create Key UI
applies.
/v1/mcp/tools is the most recent addition: before it joined this group a key
could list tools via /mcp/tools/list and /mcp-rest/tools/list but got a 403
on the equivalent /v1/mcp/tools. Unlike /v1/mcp/server, none of these paths
have a management write counterpart, so they live directly in
`mcp_inference_routes` rather than behind a method-aware carve-out.
"""
assert RouteChecks.is_llm_api_route(route=route) is True
valid_token = UserAPIKeyAuth(
user_id="test_user",
allowed_routes=["llm_api_routes"],
)
result = RouteChecks.is_virtual_key_allowed_to_call_route(
route=route,
valid_token=valid_token,
request=_mock_request(method),
)
assert result is True
def test_spend_logs_v2_classified_as_management_not_llm_api():
"""Paginated spend logs are a management/spend read route, not an LLM API."""