Skip to content

feat(server): harden HTTP transport config and add a health route - #25

Merged
SelerityMichael merged 3 commits into
mainfrom
fix/http-transport-hardening
Oct 14, 2026
Merged

SelerityMichael merged 3 commits into
mainfrom
fix/http-transport-hardening

Conversation

@SelerityMichael

Copy link
Copy Markdown
Member

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

  1. Strict boolean env parsing. _env_bool() now accepts a fixed vocabulary (1/true/yes/on vs 0/false/no/off, case-insensitive) and raises ValueError naming the variable on anything else — so ACCELO_MCP_STATELESS=ture no longer silently runs stateful. Unset still uses the default. Mirrors the existing _env_port(). Docstring corrected (it omitted on).
  2. argparse CLI. Transport selection moved from a sys.argv substring scan 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 text; --help works. --htpp no longer silently starts stdio (a container hang); --sse --http is now a usage error rather than silently picking HTTP.
  3. ACCELO_MCP_PATH validation. Rejected at startup via a new _env_path helper if it lacks a leading /, instead of being passed unchecked into Starlette routing.
  4. Startup security log. The HTTP and SSE startup lines now report the effective DNS-rebinding protection state and the Host/Origin allow-lists, so a 421 Misdirected Request from behind a proxy is diagnosable. No secrets logged.
  5. Docs applicability. README and .env.example now make clear which HTTP variables apply to --sse vs --http (run_sse ignores ACCELO_MCP_PATH, ACCELO_MCP_JSON_RESPONSE, ACCELO_MCP_STATELESS).
  6. Health route. GET /healthz returns 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_PATH rejection, /healthz 200 + Host-exemption, the startup security log (via caplog), and run_streamable_http/run_sse kwarg pinning (SSE asserts it does NOT receive path/json/stateless). test_bool_parsing relocated into TestHttpTransportSettings.
  • ruff check and ruff format --check both 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.

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).
@SelerityMichael
SelerityMichael requested a review from a team as a code owner October 14, 2026 06:23

@amazon-q-developer amazon-q-developer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread tests/test_server.py
class TestRunTransports:
"""Pins the kwargs each transport passes to the MCP library."""

async def test_streamable_http_passes_full_kwargs(self, monkeypatch):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 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.

Suggested change
async def test_streamable_http_passes_full_kwargs(self, monkeypatch):
@pytest.mark.asyncio
async def test_streamable_http_passes_full_kwargs(self, monkeypatch):

Comment thread tests/test_server.py
is ts.enable_dns_rebinding_protection
)

async def test_sse_passes_security_only(self, monkeypatch):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.
@SelerityMichael

Copy link
Copy Markdown
Member Author

Thanks for the flag. The repo sets asyncio_mode = "auto" under [tool.pytest.ini_options] in pyproject.toml, so pytest-asyncio collects and awaits every async def test regardless of the marker — these two tests do execute and their assertions do run (verified: injecting a deliberately-failing assertion fails the test, so the body is genuinely awaited, not vacuously passed).

That said, the rest of the suite carries @pytest.mark.asyncio explicitly, so I've added it to both tests in 482b2c6 for consistency and to keep them robust if auto mode is ever turned off. No behaviour change; 83 server tests still pass.

@SelerityMichael
SelerityMichael merged commit 0daf4ea into main Oct 14, 2026
6 checks passed
@SelerityMichael
SelerityMichael deleted the fix/http-transport-hardening branch October 14, 2026 08:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTTP transport config hardening: strict env parsing, argparse, path validation, health route

1 participant