Repository navigation
feat(server): harden HTTP transport config and add a health route - #25
Conversation
Addresses the seven follow-ups from #16. None affect the default stdio setup; each turns a networked-deployment misconfiguration from silent misbehaviour into a clear startup error. - _env_bool now accepts a fixed true/false vocabulary (1/true/yes/on vs 0/false/no/off, case-insensitive) and raises ValueError naming the variable on anything else, including a blank value; unset still uses the default. Mirrors _env_port. Docstring corrected to include 'on'. - CLI transport selection moved to argparse with a mutually-exclusive group (--http/--streamable-http, --sse; stdio default). Unknown flags, stray positionals and conflicting flags now exit non-zero with usage; --help works. Replaces the old sys.argv substring scan. - ACCELO_MCP_PATH is validated via a new _env_path helper and rejected if it lacks a leading slash, instead of being passed unchecked to routing. - The HTTP and SSE startup logs now report the effective DNS-rebinding protection state and Host/Origin allow-lists, so a 421 is diagnosable. - Added GET /healthz (200) as a sibling custom route, exempt from the Host allow-list, for load-balancer and container liveness probes. - Docs: README and .env.example now show which HTTP variables apply to --sse vs --http (run_sse ignores PATH/JSON_RESPONSE/STATELESS), and document the /healthz route, the strict env parsing, and the argparse flags. Tests: strict-bool parsing, argparse conflict/unknown/positional/help, path rejection, /healthz exemption, startup-log security settings, and run_streamable_http/run_sse kwarg-pinning. test_bool_parsing moved into TestHttpTransportSettings. 83 server tests pass; ruff check and format clean. No version bump (release batched).
There was a problem hiding this comment.
Review Summary
The hardening changes in server.py are well-structured: the strict _env_bool/_env_path helpers, argparse migration, transport_security() always returning explicit settings, the exposure gate ordering, and the /healthz health route are all sound. The logic is correct and the security intent is clear.
Critical Issue — Async Tests Never Execute
TestRunTransports.test_streamable_http_passes_full_kwargs and test_sse_passes_security_only are defined as async def in a plain class with no @pytest.mark.asyncio decorator. Without pytest-asyncio support, pytest collects these as regular callables, calls them (receiving a coroutine object back), and marks them passed — the function body and all assertions are never awaited or executed. This means the kwarg-pinning coverage for both transport implementations is entirely absent despite the tests appearing green. Both methods need @pytest.mark.asyncio added (see inline comments).
You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.
| class TestRunTransports: | ||
| """Pins the kwargs each transport passes to the MCP library.""" | ||
|
|
||
| async def test_streamable_http_passes_full_kwargs(self, monkeypatch): |
There was a problem hiding this comment.
🛑 Logic Error — Silent Test No-ops: Both test_streamable_http_passes_full_kwargs and test_sse_passes_security_only are defined as async def inside a plain class with no @pytest.mark.asyncio marker and no asyncio_mode = "auto" configuration. Without pytest-asyncio collecting them, pytest treats each coroutine function as a regular callable that returns a coroutine object — it does not await it, marks the test as passed, and silently discards all assertions. The kwarg-pinning coverage these tests are supposed to provide does not actually execute.
| async def test_streamable_http_passes_full_kwargs(self, monkeypatch): | |
| @pytest.mark.asyncio | |
| async def test_streamable_http_passes_full_kwargs(self, monkeypatch): |
| is ts.enable_dns_rebinding_protection | ||
| ) | ||
|
|
||
| async def test_sse_passes_security_only(self, monkeypatch): |
There was a problem hiding this comment.
Same issue as test_streamable_http_passes_full_kwargs — this async test requires @pytest.mark.asyncio to actually be awaited by pytest; without it the assertions are never evaluated and the test always passes vacuously.
| async def test_sse_passes_security_only(self, monkeypatch): | |
| @pytest.mark.asyncio | |
| async def test_sse_passes_security_only(self, monkeypatch): |
Add the explicit @pytest.mark.asyncio marker to the two TestRunTransports async tests for consistency with the rest of the suite. These tests already execute correctly under the repo's asyncio_mode = "auto" setting (verified: a deliberately broken assertion fails the test, so the body is genuinely awaited), but the surrounding tests carry the marker explicitly; adding it keeps the style consistent and keeps the tests robust if auto mode is ever disabled. No behaviour change.
|
Thanks for the flag. The repo sets That said, the rest of the suite carries |
Summary
Addresses the seven HTTP/SSE transport hardening follow-ups from #16. None of these change the default stdio setup. Each one turns a networked-deployment misconfiguration that previously failed silently into a clear startup error or a diagnosable log line.
What changed
_env_bool()now accepts a fixed vocabulary (1/true/yes/onvs0/false/no/off, case-insensitive) and raisesValueErrornaming the variable on anything else — soACCELO_MCP_STATELESS=tureno longer silently runs stateful. Unset still uses the default. Mirrors the existing_env_port(). Docstring corrected (it omittedon).sys.argvsubstring scan toargparsewith a mutually-exclusive group (--http/--streamable-http,--sse; stdio default). Unknown flags, stray positionals and conflicting flags now exit non-zero with usage text;--helpworks.--htppno longer silently starts stdio (a container hang);--sse --httpis now a usage error rather than silently picking HTTP.ACCELO_MCP_PATHvalidation. Rejected at startup via a new_env_pathhelper if it lacks a leading/, instead of being passed unchecked into Starlette routing.421 Misdirected Requestfrom behind a proxy is diagnosable. No secrets logged..env.examplenow make clear which HTTP variables apply to--ssevs--http(run_sseignoresACCELO_MCP_PATH,ACCELO_MCP_JSON_RESPONSE,ACCELO_MCP_STATELESS).GET /healthzreturns 200 as a sibling custom route, exempt from the Host allow-list (verified against the mcp library: custom routes mount outside the session manager's Host/Origin check on both transports), so a load balancer can probe by instance IP.Behaviour change for operators
Parts 1–3 mean a previously-tolerated misconfiguration now fails at startup instead of running in a surprising mode. Specifically: a blank or misspelled boolean env var, a path without a leading slash, and unknown/conflicting CLI flags now error out. This is intentional (surfacing operator mistakes), consistent with
_env_port's existing behaviour, and documented in the README/.env.example.Testing
tests/test_server.py: 83 passed. New coverage for strict-bool parsing (raise on typo/blank, accept full vocabulary), argparse conflict/unknown/positional/--help,ACCELO_MCP_PATHrejection,/healthz200 + Host-exemption, the startup security log (via caplog), andrun_streamable_http/run_ssekwarg pinning (SSE asserts it does NOT receive path/json/stateless).test_bool_parsingrelocated intoTestHttpTransportSettings.ruff checkandruff format --checkboth clean.Release
No version bump in this PR — the release is batched. A single consolidated version bump + release note will be cut once #16, #22 and #23 have all merged.
Closes #16.