From a90989d43eca952c3c053483e27b8e00f2ac2b91 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Sun, 6 Sep 2026 12:59:00 +0000 Subject: [PATCH] fix(e2e): keep MCP admin routes on the data plane /v1/mcp/* is a lazily mounted feature, so a gateway registers it on the first matching request, which happens after the startup route trim that drops management endpoints. Routing it to the control plane therefore sent every MCP call to the one backend process: the new lifecycle read-backs proved a single process rather than every replica, and mcp_client's await_registered barrier waited on a registry that does not serve the tools/list call it guards, so the existing MCP suites polled a gateway that had not synced yet until poll_timeout Verified against a two-gateway split stack (backend on 4001, gateways on 4010 and 4011, one postgres): both gateways answer /v1/mcp/server and /v1/mcp/toolset, and each served 6 server reads and 7 toolset reads over the run --- tests/e2e/test_proxy_client.py | 17 +++++++++++++++-- tests/e2e/transport.py | 1 - 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/e2e/test_proxy_client.py b/tests/e2e/test_proxy_client.py index 474aaf210c2..82cebb975e8 100644 --- a/tests/e2e/test_proxy_client.py +++ b/tests/e2e/test_proxy_client.py @@ -148,7 +148,7 @@ class TestReplicasFor: control_plane_base_url="http://backend", replica_urls=("http://gateway-1", "http://gateway-2"), ) - assert set(client.replicas_for("/v1/mcp/server/abc")) == {"http://backend"} + assert set(client.replicas_for("/key/info")) == {"http://backend"} assert set(client.replicas_for("/v1/models")) == {"http://gateway-1", "http://gateway-2"} def test_monolith_reads_management_routes_back_from_every_replica(self) -> None: @@ -157,7 +157,20 @@ class TestReplicasFor: control_plane_base_url="http://lb", replica_urls=("http://pod-1", "http://pod-2"), ) - assert set(client.replicas_for("/v1/mcp/server/abc")) == {"http://pod-1", "http://pod-2"} + assert set(client.replicas_for("/key/info")) == {"http://pod-1", "http://pod-2"} + + def test_mcp_admin_routes_read_back_from_every_data_plane_replica(self) -> None: + """/v1/mcp/* is a lazily mounted feature, so a data-plane replica serves it + too and answers from its own in-memory registry. Routing it to the control + plane would leave every replica but that one unproven, and would move the + tools/list barrier in mcp_client off the plane that serves tools/list.""" + client: Final = build_proxy_client( + base_url="http://lb", + control_plane_base_url="http://backend", + replica_urls=("http://gateway-1", "http://gateway-2"), + ) + assert set(client.replicas_for("/v1/mcp/server/abc")) == {"http://gateway-1", "http://gateway-2"} + assert set(client.replicas_for("/v1/mcp/toolset/abc")) == {"http://gateway-1", "http://gateway-2"} def test_a_route_no_replica_serves_is_refused_rather_than_read_back_vacuously(self) -> None: """A read-back over zero replicas would satisfy every predicate and assert diff --git a/tests/e2e/transport.py b/tests/e2e/transport.py index be32304e682..44fdbaa3e41 100644 --- a/tests/e2e/transport.py +++ b/tests/e2e/transport.py @@ -291,7 +291,6 @@ class HttpTransport: # (/chat, /embeddings, and native passthrough like /gemini, /anthropic) are NOT # here and fall through to the data plane. Matched as path prefixes. CONTROL_PLANE_PREFIXES: tuple[str, ...] = ( - "/v1/mcp/", "/key", "/user", "/team",