fix(aws-api-mcp-server): pass allowed hosts and origins to the FastMCP HTTP guard by Om-singhaI · Pull Request #4523 · awslabs/mcp · GitHub
Skip to content

fix(aws-api-mcp-server): pass allowed hosts and origins to the FastMCP HTTP guard - #4523

Merged
arnewouters merged 2 commits into
awslabs:mainfrom
Om-singhaI:fix/api-mcp-allowed-hosts
Aug 28, 2026
Merged

arnewouters merged 2 commits into
awslabs:mainfrom
Om-singhaI:fix/api-mcp-allowed-hosts

Conversation

@Om-singhaI

Copy link
Copy Markdown
Contributor

Fixes #4507

Summary

Changes

aws-api-mcp-server builds its FastMCP instance without any host information and only passes host and port to server.run(). FastMCP 3.4.3 (locked since release 1.4.0) wraps the streamable-http app in its own HostOriginGuardMiddleware, added in PrefectHQ/fastmcp#4405. That guard accepts loopback Host headers, anything in its allowed_hosts list, and the bound address when that address is not the unspecified one. Nothing in this server populated allowed_hosts, so a container bound to 0.0.0.0 behind an ALB or ingress answered every real request with 421 Misdirected Request, and only Host: localhost got through. AWS_API_MCP_ALLOWED_HOSTS and AWS_API_MCP_ALLOWED_ORIGINS had no effect on this because they only feed the project's own HTTPHeaderValidationMiddleware, which runs after the FastMCP guard.

The fix is deliberately small, in keeping with the end of development notice on this server:

  • server.py now imports ALLOWED_HOSTS and ALLOWED_ORIGINS from config.py and passes allowed_hosts=ALLOWED_HOSTS.split(',') and allowed_origins=ALLOWED_ORIGINS.split(',') to server.run() on the HTTP branch. FastMCP.run() forwards extra keyword arguments to run_http_async(), whose signature in fastmcp 3.4.3 accepts exactly allowed_hosts: list[str] | None and allowed_origins: list[str] | None and installs them in the guard. The guard treats * as a wildcard, matching the semantics this project already documents for these variables. The stdio branch is untouched because run_stdio_async() does not accept these arguments.
  • pyproject.toml and uv.lock: the fastmcp floor moves from >=3.2.0 to >=3.4.3. The two keyword arguments do not exist before 3.4.3 (I checked the v3.2.0 and v3.4.2 tags of PrefectHQ/fastmcp: neither server/http.py nor server/mixins/transport.py mentions allowed_hosts or HostOriginGuard; both appear in v3.4.3). Without the bump, an install that resolved any permitted fastmcp between 3.2.0 and 3.4.2 would pass allowed_hosts to a run_http_async() that does not accept it and crash at startup with TypeError: run_http_async() got an unexpected keyword argument 'allowed_hosts', even though those versions never had the 421 problem. Raising the floor keeps server.py and the dependency contract in step, in the same spirit as fix(deps): raise fastmcp floor to >=3.2.0 to exclude a CRITICAL advisory #4454. The lock was regenerated with uv lock (uv 0.12.3, the version that wrote the current lock) and the only change is the requires-dist specifier line; the resolved version was already 3.4.3, so nothing installed moves.
  • tests/test_server.py: test_main_success_with_http_transport now asserts the two new keyword arguments, and a new test test_main_http_transport_allowed_hosts_reach_fastmcp_guard builds the real HTTP app with exactly the keyword arguments main() hands to server.run() and sends an initialize request through FastMCP's guard with Host: my-mcp-server.example.com while the app is bound to 0.0.0.0:8080. Without the two keyword arguments that test fails with {'my-mcp-server.example.com': 421} != {'my-mcp-server.example.com': 200}. The starlette.testclient import lives inside that one test so its StarletteDeprecationWarning (httpx versus httpx2 under starlette 1.3.1) stays scoped to the test that needs it.
  • CHANGELOG.md: entry under Unreleased / Fixed, mentioning the floor bump.

A note on the suggestion in the issue: passing host to the FastMCP() constructor does not work on fastmcp 3.x; the constructor rejects host with a message pointing at run_http_async() or FASTMCP_HOST. Passing allowed_hosts and allowed_origins to run() is the supported path.

Default behaviour is unchanged for local use: ALLOWED_HOSTS defaults to AWS_API_MCP_HOST, which defaults to 127.0.0.1, a value the guard already allows. With AWS_API_MCP_HOST=0.0.0.0 and no explicit allow list, both layers now agree on allowing Host: 0.0.0.0 and loopback names and rejecting everything else, so operators still have to opt in to a real DNS name or * as the README describes. The issue's expected behaviour section asks for AWS_API_MCP_HOST=0.0.0.0 alone to accept real DNS names; I left that as an explicit opt in because the project's own middleware already rejects such hosts by default and silently widening the default felt like a separate decision for the maintainers.

One difference between the two layers worth knowing about: the README says port numbers are stripped during validation. That is still true of the project's middleware for both headers, and of the FastMCP guard for Host, but the guard compares Origin values including any explicit port. A browser origin such as https://x.example.com:8443 therefore needs https://x.example.com:8443 in AWS_API_MCP_ALLOWED_ORIGINS for the guard and https://x.example.com for the project middleware (or *). Such requests were rejected with 421 before this change, so this is not a regression, and I have not touched the README so the maintainers can decide how they want that documented.

User experience

Before (published image 1.5.1, AWS_API_MCP_TRANSPORT=streamable-http, AWS_API_MCP_HOST=0.0.0.0, AWS_API_MCP_ALLOWED_HOSTS=*):

POST /mcp  Host: localhost                   -> 200 OK
POST /mcp  Host: my-mcp-server.example.com   -> 421 Misdirected Request

After, same configuration:

POST /mcp  Host: localhost                   -> 200 OK
POST /mcp  Host: my-mcp-server.example.com   -> 200 OK

Reproduced in process on main with starlette.testclient against the real fastmcp 3.4.3 guard, building the app with the same keyword arguments main() uses, and confirmed fixed with the change.

Test run in src/aws-api-mcp-server with a venv pinned to the lock file versions (fastmcp 3.4.3, mcp 1.29.0, starlette 1.3.1):

  • before: tests/test_server.py 71 passed, 1 failed
  • after: 72 passed, 1 failed

The one failure, test_call_aws_ecs_deploy_error_sanitization, is present on pristine main in this environment (pytest reports "async def functions are not natively supported" for that single test) and is unrelated to this change.

ruff check and ruff format --check pass on the changed files. pip check passes with fastmcp 3.4.3 installed against the new floor.

  • One precedence detail worth knowing: FastMCP only consults its own FASTMCP_HTTP_ALLOWED_HOSTS and FASTMCP_HTTP_ALLOWED_ORIGINS settings when the keyword arguments are None. After this change they are always passed, so AWS_API_MCP_ALLOWED_HOSTS and AWS_API_MCP_ALLOWED_ORIGINS become the single source of truth for the guard. Anyone who worked around this issue with the FASTMCP_* variables already had to set the AWS_API_MCP_* ones for the project's own header middleware to pass, so in practice nothing that worked before stops working. FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=false still disables the guard entirely since host_origin_protection is not passed.

Checklist

If your change doesn't seem to apply, please leave them unchecked.

  • I have reviewed the contributing guidelines
  • I have performed a self-review of this change
  • Changes have been tested
  • Changes are documented

Is this a breaking change? (Y/N) N

The fastmcp floor moves to >=3.4.3. Installs from the lock file or via uvx already resolve 3.4.3, so nothing changes for them; only an environment that pinned fastmcp between 3.2.0 and 3.4.2 would need to upgrade.

RFC issue number: N/A

Checklist:

  • Migration process documented
  • Implement warnings (if it can live side by side)

Acknowledgment

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of the project license.

Copilot AI lite review requested due to automatic review settings August 22, 2026 07:02
@Om-singhaI Om-singhaI changed the title <!-- Suggested PR title: fix(aws-api-mcp-server): pass allowed hosts and origins to the FastMCP HTTP guard --> fix(aws-api-mcp-server): pass allowed hosts and origins to the FastMCP HTTP guard Aug 22, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes FastMCP host validation for AWS API MCP Server HTTP deployments.

Changes:

  • Forwards configured host and origin allowlists to FastMCP.
  • Raises the FastMCP minimum version to 3.4.3.
  • Adds coverage and changelog documentation.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Summary
src/aws-api-mcp-server/uv.lock Updates dependency constraint metadata.
src/aws-api-mcp-server/tests/test_server.py Tests allowlist forwarding and host validation.
src/aws-api-mcp-server/pyproject.toml Raises the FastMCP dependency floor.
src/aws-api-mcp-server/CHANGELOG.md Documents the fix.
src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py Forwards HTTP host and origin allowlists.
Suppressed comments (1)

src/aws-api-mcp-server/tests/test_server.py:952

  • This integration test never sends an Origin header, and ALLOWED_ORIGINS='*' is therefore not exercised at runtime. A regression that drops or miswires the new allowed_origins argument would still pass; add a request with a non-same-origin value so the FastMCP origin guard is actually verified.
            for host_header in ('localhost', 'my-mcp-server.example.com'):
                response = client.post(
                    '/mcp',
                    json=initialize_request,
                    headers={
                        'Host': host_header,
                        'Accept': 'application/json, text/event-stream',
                    },

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@codecov

codecov Bot commented Aug 27, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.21%. Comparing base (302788b) to head (6f40e39).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4523   +/-   ##
=======================================
  Coverage   93.21%   93.21%           
=======================================
  Files        1044     1044           
  Lines       88387    88387           
  Branches    14274    14274           
=======================================
  Hits        82392    82392           
  Misses       3623     3623           
  Partials     2372     2372           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@arnewouters

Copy link
Copy Markdown
Contributor

LGTM, I have made some changes to CHANGELOG.md so you'll have to rebase

…P HTTP guard

FastMCP 3.x wraps the streamable HTTP app in its own HostOriginGuardMiddleware.
That guard only accepts loopback Host headers, the explicit allowed_hosts list,
and the bound address when that address is not unspecified. main() bound the
server with host=HOST but never passed allowed_hosts or allowed_origins to
server.run(), so a container listening on 0.0.0.0 behind a load balancer or
ingress answered every real request with 421 Misdirected Request while only
requests carrying Host: localhost got through. AWS_API_MCP_ALLOWED_HOSTS and
AWS_API_MCP_ALLOWED_ORIGINS only fed the project's own header validation
middleware, which runs after the FastMCP guard and therefore never saw those
requests. The regression appeared when the lock file moved from fastmcp 3.0.1
to 3.4.3, which matches the 1.3.46 to 1.4.0 bisection in the report.

Hand the already parsed allowed host and origin lists to server.run() for the
HTTP transport. run() forwards them to run_http_async(), which installs them in
the guard, so a wildcard or an explicit host list now works and the two
validation layers agree. The stdio path is untouched because run_stdio_async()
does not accept these keyword arguments.

Those keyword arguments first appeared in fastmcp 3.4.3, the release that
introduced the guard. The package accepted any fastmcp from 3.2.0, and on
3.2.0 through 3.4.2 the new call would have failed at startup with a TypeError
even though those versions never had the 421 problem. Raise the fastmcp floor
to 3.4.3 in pyproject.toml and regenerate uv.lock with uv lock. The resolved
version was already 3.4.3, so only the requires dist specifier line in the
lock changes and nothing installed moves.

Extend the existing run kwargs test and add a regression test that builds the
HTTP app with exactly the keyword arguments main() passes and sends an
initialize request with a real DNS name in the Host header through the FastMCP
guard.

Fixes awslabs#4507
…MCP guard

The guard test only varied the Host header, so a change that dropped or
miswired the allowed_origins argument would still have passed. Every
request in the test now carries an Origin from another site, which the
guard rejects with 403 unless allowed_origins reaches FastMCP.

Raised in review.
@Om-singhaI
Om-singhaI force-pushed the fix/api-mcp-allowed-hosts branch from f35c7d5 to 6f40e39 Compare August 27, 2026 17:30
@Om-singhaI

Copy link
Copy Markdown
Contributor Author

@arnewouters
arnewouters added this pull request to the merge queue Aug 28, 2026
Merged via the queue into awslabs:main with commit 666aa48 Aug 28, 2026
137 checks passed
@github-project-automation github-project-automation Bot moved this from To triage to Done in awslabs/mcp Project Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

4 participants