Skip to content

fix(mcp): apply the upstream.* connection settings to the rmcp transport - #846

Merged
jarvis9443 merged 2 commits into
mainfrom
fix/mcp-transport-upstream-config
Jul 30, 2026
Merged

fix(mcp): apply the upstream.* connection settings to the rmcp transport#846
jarvis9443 merged 2 commits into
mainfrom
fix/mcp-transport-upstream-config

Conversation

@jarvis9443

@jarvis9443 jarvis9443 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Problem

The rmcp streamable-http transport's HTTP client was outside the upstream.* connection-settings contract: rmcp pins its own reqwest line (0.13, vs the workspace's 0.12), so the from_uri/from_config constructors built rmcp's default_http_client() — which sets no connect timeout. A black-holed MCP upstream (SYN swallowed) was bounded only by the coarse per-operation McpServer.timeout_ms (default 30 s), instead of failing at upstream.connect_timeout_ms like every other outbound path. This is the same class the no-bare-client guard exists for (#1122/#1126); it missed this site because the client is built inside rmcp.

Change

  • New shared_http_client() in aisix-mcp: one client shared by every MCP upstream, built against rmcp's reqwest line with the upstream_http::config() values applied — connect_timeout_ms, the TCP keepalive triple, pool_idle_timeout_secs, pool_max_idle_per_host. All four transport constructions (none / bearer / api-key / oauth2) now go through StreamableHttpClientTransport::with_client. Auth stays per-request (transport-injected), so sharing one client across differently-authenticated upstreams is safe — same posture as the provider bridges.
  • The no_production_code_builds_a_bare_reqwest_client guard names rmcp_reqwest::Client:: in aisix-mcp/src/bridge.rs as the sanctioned second construction site (scoped to that file; a bare rmcp client anywhere else still gets flagged).

Behaviour changes (the honest delta vs rmcp's default client)

  1. A connect timeout exists at all (default 5 s) — the actual fix.
  2. Connection pooling turns ON. rmcp deliberately disables idle pooling (pool_max_idle_per_host(0)) to dodge ~40 ms delayed-ACK stalls on reused connections. Here reuse wins: every other outbound path pools under upstream.* management (30 s idle expiry), per-call TCP+TLS handshakes to a remote MCP server cost far more than the stall rmcp avoids, and the reference proxy's MCP path (python-sdk over httpx) pools by default too. upstream.pool_max_idle_per_host: 0 restores rmcp's exact behaviour.
  3. The TCP keepalive triple moves from reqwest's default 15 s/15 s/3 to the deployment's 60 s/30 s/5.

Operation-level deadlines are unchanged: McpServer.timeout_ms still bounds every MCP operation end-to-end.

Tests

connect_timeout_bounds_an_unreachable_upstream dials a black-holed upstream (TEST-NET-3) with a 12 s operation timeout and asserts the dial is cut well below it (the 5 s connect timeout). Verified to fail without the fix: the dial hangs the full 12 s and surfaces the outer-timeout error instead.

The rmcp streamable-http transport was the last outbound path running on
a bare default client: rmcp pins its own reqwest line (0.13), so the
from_uri/from_config constructors built a client with no connect timeout
(kernel SYN retries, worst case ≈127 s), a 90 s pool idle lifetime and
TCP keepalive off — the exact gaps the upstream.* config block exists to
close, and the reason the no-bare-client guard test was added
(AISIX-Cloud#1122/#1126). Only the per-operation deadline
(McpServer.timeout_ms, outer tokio wrapper) bounded anything.

- New shared_http_client() in aisix-mcp: one client for every MCP
  upstream, built from upstream_http::config() (connect_timeout, TCP
  keepalive triple, pool idle, pool max-idle-per-host) against rmcp's
  reqwest line; all four transport constructions now go through
  StreamableHttpClientTransport::with_client.
- The no_production_code_builds_a_bare_reqwest_client guard now names
  rmcp_reqwest::Client:: as the sanctioned second construction site
  instead of (coincidentally, via substring) flagging it.

The regression test dials a black-holed upstream (TEST-NET-3) with a
12 s operation timeout: on the default client the dial hung the full
12 s; with the shared client the connect timeout (default 5 s) cuts it.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 53 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cfb16f5c-3195-4b16-8a49-2606e94eff11

📥 Commits

Reviewing files that changed from the base of the PR and between 80467af and 0c086d8.

📒 Files selected for processing (3)
  • crates/aisix-gateway/src/upstream_http.rs
  • crates/aisix-mcp/Cargo.toml
  • crates/aisix-mcp/src/bridge.rs

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

…mption

Audit follow-ups — the code is unchanged, the story around it was wrong
in two places:

- rmcp's default_http_client() is not a bare default client: it
  deliberately DISABLES idle pooling (pool_max_idle_per_host(0)) to
  dodge ~40 ms delayed-ACK stalls, and reqwest 0.13 has TCP keepalive
  (15 s/15 s/3) on by default. The real delta of shared_http_client()
  is: a connect timeout exists at all (the actual fix), pooling turns
  ON under upstream.* management (reuse beats per-call TCP+TLS to a
  remote MCP server; upstream.pool_max_idle_per_host: 0 restores rmcp's
  behaviour), and the keepalive triple moves to the deployment's
  60/30/5. Comments now say exactly that.
- The Cargo.toml note claiming the rmcp-reqwest dep is 'never a client'
  is now false; updated.
- The guard-test exemption for rmcp_reqwest::Client:: is scoped to
  aisix-mcp/src/bridge.rs so a bare rmcp client anywhere else still
  gets flagged.
@jarvis9443

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@jarvis9443
jarvis9443 merged commit 22b8353 into main Jul 30, 2026
12 checks passed
@jarvis9443
jarvis9443 deleted the fix/mcp-transport-upstream-config branch July 30, 2026 06:51
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.

1 participant