From 68aaeaf5273f320548d0cec0d983e4aed4edf6ee Mon Sep 17 00:00:00 2001 From: Jonas Friedmann Date: Wed, 21 Oct 2026 13:07:34 +0200 Subject: [PATCH] BE+TESTS: relative MCP SSE endpoint path #1854 The SSE endpoint event advertised the absolute path /mcp/messages, which drops the /server prefix when clients connect through the bundled nginx proxy on port 20211. Clients then POST to /mcp/messages, nginx serves it as a static file and returns 404, so MCP clients can't connect via the proxy. A relative path resolves against whichever SSE URL the client used, so it works both directly and behind the prefix. --- server/api_server/mcp_endpoint.py | 5 ++- test/api_endpoints/test_mcp_sse_endpoint.py | 46 +++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 test/api_endpoints/test_mcp_sse_endpoint.py diff --git a/server/api_server/mcp_endpoint.py b/server/api_server/mcp_endpoint.py index 424484fbd..64280d8aa 100644 --- a/server/api_server/mcp_endpoint.py +++ b/server/api_server/mcp_endpoint.py @@ -1057,8 +1057,9 @@ def mcp_sse(): def stream(): """Generator for SSE stream.""" - # Send endpoint event with session ID - yield f"event: endpoint\ndata: /mcp/messages?session_id={session_id}\n\n" + # Send endpoint event with session ID. Relative path, so clients resolve it against + # the SSE URL they used (works directly and behind the nginx /server prefix). + yield f"event: endpoint\ndata: messages?session_id={session_id}\n\n" try: while True: diff --git a/test/api_endpoints/test_mcp_sse_endpoint.py b/test/api_endpoints/test_mcp_sse_endpoint.py new file mode 100644 index 000000000..44555b511 --- /dev/null +++ b/test/api_endpoints/test_mcp_sse_endpoint.py @@ -0,0 +1,46 @@ +from urllib.parse import urljoin, urlparse + +import pytest + +from api_server.api_server_start import app +from helper import get_setting_value + + +@pytest.fixture(scope="session") +def api_token(): + """Return the API token from the loaded config.""" + return get_setting_value("API_TOKEN") + + +@pytest.fixture +def client(): + """Yield a Flask test client for the API server.""" + with app.test_client() as client: + yield client + + +def read_endpoint_event(client, api_token): + """Open the MCP SSE stream and return the data of its first (endpoint) event.""" + response = client.get("/mcp/sse", headers={"Authorization": f"Bearer {api_token}"}, buffered=False) + assert response.status_code == 200 + try: + first_event = next(response.response) + finally: + response.close() + if isinstance(first_event, bytes): + first_event = first_event.decode() + lines = first_event.strip().splitlines() + assert lines[0] == "event: endpoint" + return lines[1].removeprefix("data: ") + + +@pytest.mark.parametrize("sse_url, expected_path", [ + ("http://host:20212/mcp/sse", "/mcp/messages"), + ("http://host:20211/server/mcp/sse", "/server/mcp/messages"), +]) +def test_sse_endpoint_resolves_to_messages_route(client, api_token, sse_url, expected_path): + """The advertised endpoint must resolve to the messages route both directly and behind the /server proxy prefix.""" + endpoint = read_endpoint_event(client, api_token) + resolved = urlparse(urljoin(sse_url, endpoint)) + assert resolved.path == expected_path + assert resolved.query.startswith("session_id=")