Skip to content

Fix kai-api 421 behind ingress: pin fastmcp <3 and build image from lockfile (#934)#935

Merged
fabianvf merged 2 commits into
konveyor:mainfrom
fabianvf:fix-934-mcp-host-421
Jul 9, 2026
Merged

Fix kai-api 421 behind ingress: pin fastmcp <3 and build image from lockfile (#934)#935
fabianvf merged 2 commits into
konveyor:mainfrom
fabianvf:fix-934-mcp-host-421

Conversation

@fabianvf

@fabianvf fabianvf commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #934. The kai-api MCP solution server started returning HTTP 421 Misdirected Request for every request routed through an nginx ingress (breaking all editor-extensions @requires-minikube CI) after the #932 merge triggered a latest image rebuild.

The dependency bump in #932 is not the cause. The real cause is a latent packaging bug that the rebuild exposed:

  • tools/deploy/Containerfile installed dependencies with pip install ., resolving fresh from pyproject.toml's unbounded fastmcp>=2.8.0. The pinned requirements.txt / uv.lock were never even copied into the image.
  • The rebuild floated fastmcp up to 3.x, whose mcp SDK enables DNS-rebinding protection by default and rejects any request whose Host/Origin header doesn't match the bind host (0.0.0.0) with 421.
  • On the pinned fastmcp 2.14.3 / mcp 1.25.0, TransportSecurityMiddleware receives settings=None and leaves that protection off (per the SDK's backwards-compat default) — which is exactly why this repo's own tests never reproduced it: they use stdio, or connect HTTP directly to localhost (matching Host), and CI installs from the lock while the image resolved fresh.

Changes

  • Bound fastmcp>=2.8.0,<3 in pyproject.toml (uv.lock regenerated) so an image rebuild can no longer jump a major version incidentally.
  • Build the image from the pinned requirements.txt, then install the project with --no-deps, so the deployed image uses exactly the versions CI tests.
  • Document at the FastMCP(...) construction site why DNS-rebinding protection must stay disabled for ingress deployments, and how to configure it explicitly if fastmcp is ever bumped to 3.x.
  • Add a regression test (tests/test_host_header.py) that drives the streamable-http transport with a mismatched Host: 192.168.49.2 / Origin header (mirroring the Unbounded fastmcp constraint: image rebuilds pick up FastMCP 3.4.3 host-origin protection, kai-api returns 421 for all proxied requests #934 repro) and asserts the response is not 421.

Verification

  • Full suite: 7 passed, 5 skipped, including the new regression test.
  • Manual repro on pinned fastmcp 2.14.3: mismatched Host (192.168.49.2, evil.example.com) → 200, not 421.
  • pip install --no-deps . builds the package via the uv_build backend.
  • requirements.txt is unchanged (adding <3 doesn't alter resolution), so verify-requirements-txt stays green.

Note: release-0.9 produces the same broken image and needs the same fix; will backport after this merges.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved request handling so valid connections are less likely to be rejected in ingress-style setups with mismatched headers.
    • Added safeguards to keep runtime behavior aligned with the tested major dependency version.
  • Tests

    • Added regression coverage for header-mismatch scenarios to prevent HTTP 421 responses from reappearing.
  • Chores

    • Updated deployment packaging to use the locked dependency set during image builds for more consistent releases.

…ockfile (konveyor#934)

The MCP solution server began returning HTTP 421 Misdirected Request for
every request routed through an nginx ingress (breaking editor-extensions
@requires-minikube CI) after the konveyor#932 merge triggered a `latest` image
rebuild.

Root cause is not the dependency bump itself but an unbounded fastmcp
constraint combined with an image build that ignores the pins:

- tools/deploy/Containerfile installed deps via `pip install .`, resolving
  fresh from pyproject.toml's unbounded `fastmcp>=2.8.0`. The pinned
  requirements.txt/uv.lock were never even copied into the image.
- The rebuild floated fastmcp to 3.x, whose mcp SDK enables DNS-rebinding
  protection by default and rejects any request whose Host/Origin header
  does not match the bind host (0.0.0.0) with 421.
- On the pinned fastmcp 2.14.3 / mcp 1.25.0, TransportSecurityMiddleware
  receives settings=None and leaves that protection off, which is why the
  repo's own tests (stdio, or HTTP to localhost) never reproduced it.

Fixes:
- Bound `fastmcp>=2.8.0,<3` in pyproject.toml (uv.lock regenerated) so an
  image rebuild can no longer jump a major version incidentally.
- Build the image from the pinned requirements.txt, then install the
  project with --no-deps, so the deployed image uses exactly the versions
  CI tests.
- Document at the FastMCP construction site why DNS-rebinding protection
  must stay disabled for ingress deployments, and how to configure it
  explicitly if fastmcp is ever bumped to 3.x.
- Add a regression test that drives the streamable-http transport with a
  mismatched Host/Origin header and asserts the response is not 421.

Signed-off-by: Fabian von Feilitzsch <fabian@fabianism.us>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@fabianvf, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 39 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4afd4126-ba8e-4c75-b39e-2b7d7b28dcd1

📥 Commits

Reviewing files that changed from the base of the PR and between c4c90b1 and fc86cb3.

📒 Files selected for processing (1)
  • .github/workflows/test-image-mcp-server.yml
📝 Walkthrough

Walkthrough

This PR fixes an issue where fastmcp 3.x's default DNS-rebinding host protection caused HTTP 421 responses behind proxies. It pins fastmcp to <3, documents the rationale in server.py, updates the Containerfile to install from pinned requirements, and adds a regression test for mismatched Host headers.

Changes

fastmcp version pin and 421 fix

Layer / File(s) Summary
Pin fastmcp dependency and document rationale
kai_mcp_solution_server/pyproject.toml, kai_mcp_solution_server/src/kai_mcp_solution_server/server.py
fastmcp constraint changed to >=2.8.0,<3 with explanatory comment; a comment is added above FastMCP initialization explaining why DNS-rebinding protection is disabled, plus an unrelated type annotation reformat.
Install pinned dependencies in container build
kai_mcp_solution_server/tools/deploy/Containerfile
Build now copies requirements.txt and installs pinned dependencies, then installs the project with --no-deps instead of resolving unpinned versions.
Regression test for mismatched Host header
kai_mcp_solution_server/tests/test_host_header.py
New test launches the server as a subprocess, waits for readiness, then sends a request with mismatched Host/Origin headers and asserts the response is not 421 and initialize succeeds with 200.

Estimated code review effort: 2 (Simple) | ~12 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Test as TestHostHeaderValidation
  participant Server as MCP Server (subprocess)
  Test->>Server: launch via subprocess.Popen (--host/--port)
  Test->>Server: POST initialize (matching Host)
  Server-->>Test: 200 OK (ready)
  Test->>Server: POST initialize (mismatched Host/Origin)
  Server-->>Test: response (status asserted != 421, == 200)
  Test->>Server: terminate process (tearDown)
Loading

Suggested reviewers: djzager

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main fix: preventing FastMCP 3.x upgrades and rebuilding the image from pinned dependencies.
Linked Issues check ✅ Passed The changes match the issue: the image now uses pinned requirements, fastmcp is capped below 3, and a regression test covers mismatched Host headers.
Out of Scope Changes check ✅ Passed No unrelated code changes are evident; the edits all support the ingress 421 fix and reproducible dependency build.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
kai_mcp_solution_server/tests/test_host_header.py (1)

95-122: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Readiness check accepts any HTTP response as "ready".

_wait_until_ready returns as soon as httpx.post doesn't raise, without checking the status code. A 500 or other error response would be treated as "ready", leading to a confusing assertion failure in the test itself rather than a clear "server not ready" message. Consider checking for a 200 status in the readiness probe.

♻️ Proposed refactor for readiness check
             try:
-                httpx.post(url, json=body, headers=headers, timeout=2.0)
-                return
+                resp = httpx.post(url, json=body, headers=headers, timeout=2.0)
+                if resp.status_code == 200:
+                    return
             except httpx.HTTPError:
                 time.sleep(0.5)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@kai_mcp_solution_server/tests/test_host_header.py` around lines 95 - 122, The
readiness probe in _wait_until_ready currently treats any successful httpx.post
call as ready even if the server returns an error status. Update the probe to
verify the response indicates success, specifically requiring a 200 OK before
returning, and treat non-2xx responses as not ready so the loop keeps polling
until the server is actually healthy.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@kai_mcp_solution_server/tests/test_host_header.py`:
- Around line 95-122: The readiness probe in _wait_until_ready currently treats
any successful httpx.post call as ready even if the server returns an error
status. Update the probe to verify the response indicates success, specifically
requiring a 200 OK before returning, and treat non-2xx responses as not ready so
the loop keeps polling until the server is actually healthy.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6711c130-cf3a-4daa-a6a1-42bda005f647

📥 Commits

Reviewing files that changed from the base of the PR and between 25c8372 and c4c90b1.

⛔ Files ignored due to path filters (1)
  • kai_mcp_solution_server/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • kai_mcp_solution_server/pyproject.toml
  • kai_mcp_solution_server/src/kai_mcp_solution_server/server.py
  • kai_mcp_solution_server/tests/test_host_header.py
  • kai_mcp_solution_server/tools/deploy/Containerfile

…ssion

Every existing test runs the server from source against the pinned lockfile,
so none of them exercised the deployed container image — which resolves
dependencies at build time and is where issue konveyor#934 actually manifested.

Add a workflow that builds tools/deploy/Containerfile, asserts the image
resolved a 2.x fastmcp (a 3.x jump re-enables DNS-rebinding protection by
default), starts the container on streamable-http, and POSTs an `initialize`
request with a mismatched Host/Origin header (the nginx-ingress case from the
issue). The build must not return HTTP 421. This would have caught the
original regression, which the source-level tests could not.

Signed-off-by: Fabian von Feilitzsch <fabian@fabianism.us>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

Unbounded fastmcp constraint: image rebuilds pick up FastMCP 3.4.3 host-origin protection, kai-api returns 421 for all proxied requests

2 participants