From 3615049071bbf45ff4029e2ecb59962dc1521c3c Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Mon, 22 Jun 2026 17:36:26 -0700 Subject: [PATCH] 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 --- litellm/proxy/_types.py | 1 + .../proxy/auth/test_route_checks.py | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 2d7e1b72cf1..ac90302fdaa 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -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. diff --git a/tests/test_litellm/proxy/auth/test_route_checks.py b/tests/test_litellm/proxy/auth/test_route_checks.py index 52ba1dbcfbd..d623149ff6a 100644 --- a/tests/test_litellm/proxy/auth/test_route_checks.py +++ b/tests/test_litellm/proxy/auth/test_route_checks.py @@ -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."""