Skip to content

feat(mcp): route stateless tools/call through static broker catalog#340

Open
nerdalert wants to merge 1 commit into
praxis-proxy:mainfrom
nerdalert:brent-ai-mcp-tools-call-routing
Open

feat(mcp): route stateless tools/call through static broker catalog#340
nerdalert wants to merge 1 commit into
praxis-proxy:mainfrom
nerdalert:brent-ai-mcp-tools-call-routing

Conversation

@nerdalert

@nerdalert nerdalert commented Jul 15, 2026

Copy link
Copy Markdown
Member

Summary

Adds backend forwarding for stateless MCP tools/call requests using the static broker catalog.

The stateless broker can now receive a tools/call, validate the stateless MCP headers/body, look up the exposed tool name, select the configured backend cluster, rewrite the backend path, and forward the call with the backend tool name. This turns the static catalog from discovery-only into a working broker path for configured tools.

With this PR, the catalog becomes the routing table for stateless tools/call:

  1. The client calls an exposed tool name, for example weather_get_forecast.
  2. Praxis looks up that exposed name in the configured static catalog.
  3. The matched catalog entry tells Praxis which backend server/cluster owns the tool.
  4. Praxis selects that cluster, rewrites the upstream path, and rewrites params.name from the exposed name to the backend tool name, for
    example get_forecast.
  5. Praxis also updates the forwarded Mcp-Name header so the backend sees a consistent MCP request.
  6. The normal load-balancer path then forwards the request to the selected backend.

The temporary Praxis dependency pin is needed because this forwarding happens after the request body has been inspected and rewritten. The MCP filter sets the rewritten Mcp-Name header during the body-processing phase, and praxis-proxy/praxis#760 is the core fix that preserves those body-phase header mutations before the request is sent upstream.

Changes

  • Routes stateless tools/call by static catalog entry.
  • Rewrites params.name from exposed name to backend/original tool name.
  • Preserves params.arguments unchanged.
  • Sets the upstream Mcp-Name header to the backend tool name.
  • Encodes non-visible-ASCII MCP names with the existing =?base64?...?= sentinel format.
  • Rewrites the upstream request path to the selected server path.
  • Repairs Content-Length after request body mutation.
  • Returns -32602 Invalid Params for malformed tools/call params before header mismatch validation.
  • Keeps the current (2025-03-26) compatibility-profile tools/call behavior unchanged.
  • Updates docs and the stateless broker example to reflect working forwarding behavior.

Dependency Note

This draft temporarily pins praxis-proxy-* crates to praxis-proxy/praxis commit 2bbce4386e84f6c8a8a0e64c7cc47b52f6d34cf9 because the latest crates.io release, 0.4.0, predates praxis-proxy/praxis#760.

#760 is required for body-phase header mutations from StreamBuffer filters to reach upstream backends, which allows routed MCP tools/call requests to forward the rewritten Mcp-Name header.

This pin should be replaced with the next published praxis-proxy-* crate version once available.

Protocol Boundary

This PR completes base stateless tools/call routing.

The following are intentionally left to separate focused PRs:

  • Locked-RC method-set cleanup, including stateless ping removal
  • logging/setLevel and legacy notification conformance cleanup
  • Schema-driven x-mcp-header / Mcp-Param-* support
  • subscriptions/listen
  • MRTR / input_required
  • Dynamic catalog discovery
  • Legacy MCP sessions
  • Redis/Valkey state

Validation

  • cargo test -p praxis-ai-filters mcp — 265 passed
  • cargo test -p praxis-tests-integration --test suite mcp_broker — 30 passed
  • cargo test -p praxis-tests-schema — 90 passed
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo +nightly fmt --all --check
  • cargo xtask lint-filter-docs
  • cargo xtask lint-example-tests
  • cargo xtask sync-example-readme
  • git diff --check

Refs #148

@nerdalert
nerdalert force-pushed the brent-ai-mcp-tools-call-routing branch from 2da7dee to 9a92fb9 Compare July 15, 2026 03:27
@nerdalert
nerdalert force-pushed the brent-ai-mcp-tools-call-routing branch 4 times, most recently from da7e51c to 0802158 Compare July 23, 2026 02:35
@nerdalert
nerdalert marked this pull request as ready for review July 23, 2026 02:45
@nerdalert
nerdalert requested review from a team and crstrn13 July 23, 2026 02:45
@nerdalert
nerdalert marked this pull request as draft July 23, 2026 05:07
@nerdalert
nerdalert force-pushed the brent-ai-mcp-tools-call-routing branch 3 times, most recently from 192a2e8 to a7ef731 Compare July 23, 2026 14:57
@nerdalert
nerdalert marked this pull request as ready for review July 23, 2026 15:24

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review: feat(mcp): route stateless tools/call through static broker catalog

Clean, well-structured PR. The implementation correctly routes stateless tools/call requests through the broker catalog with proper name rewriting, cluster selection, path rewriting, and Mcp-Name header repair.

Correctness

  • Routing flow is sound: extract_tool_name validates params, catalog lookup selects the backend, body is mutated with the original (un-prefixed) tool name, and Mcp-Name header is rewritten for the upstream.
  • Error ordering is deliberate and correct: -32602 (invalid params) fires before -32020 (header mismatch), so a malformed tools/call body is rejected before header validation runs. The test stateless_header_body_mismatch_still_returns_32020 confirms the inverse case works too.
  • encode_mcp_name correctly handles both ASCII and non-ASCII tool names, falling back to the =?base64?...?= sentinel format. The triple guard (is_ascii + no control chars + HeaderValue::from_str) is defense-in-depth.
  • Content-Length repair is handled by the framework (praxis-proxy/praxis#760) after body mutation; the integration test mcp_stateless_tools_call_backend_sees_repaired_content_length verifies this end-to-end with a cross-check between echo and header-echo backends.
  • Current profile remains unchanged: tools/call still returns -32601.

Test Coverage

  • Unit tests (330 new lines): known tool routing, path rewriting, prefix stripping, Mcp-Name rewriting, argument preservation, encode_mcp_name (ASCII + non-ASCII), second server routing, unknown tool rejection, missing/null/array params, missing/non-string name, header-body mismatch ordering.
  • Integration tests (257 new lines): weather and calendar backend routing (verifying cross-isolation), path rewriting via URI-echo backend, Mcp-Name header at backend via header-echo, unknown tool 400, malformed params 400, Content-Length repair.
  • Example config test added for the stateless broker tools/call path.
  • Edge cases are well-covered: null params, array params, non-string name, missing name, unknown tool, header/body mismatch ordering.

Conventions & Style

  • #[expect] attributes are properly justified.
  • CatalogTool visibility widened from pub(super) to pub(crate) and dead_code expect removed -- both correct now that the struct fields are used for routing.
  • base64::Engine as _ import hoisted to module scope since it is now needed by both encode_mcp_name and decode_mcp_name.
  • Field reordering in apis/src/lib.rs and filters/src/lib.rs test utilities follows alphabetical convention.
  • Doc updates in docs/filters/mcp.md and the example config accurately reflect the new behavior.

Security

  • Tool names are validated via extract_tool_name and looked up against the pre-built catalog (trusted list), so injection via crafted tool names is not possible.
  • Body mutation uses serde_json (safe serialization), and the mutated body replaces the original atomically.
  • Non-ASCII tool names are base64-encoded for header safety.
Category Verdict
Correctness Pass
Test coverage Pass
Conventions Pass
Security Pass

No issues at Critical, Large, or Medium severity. Ship it.

@nerdalert
nerdalert force-pushed the brent-ai-mcp-tools-call-routing branch from a7ef731 to 4ef1b2b Compare July 24, 2026 04:53

@aslakknutsen aslakknutsen 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.

docs/architecture/agentic-protocols.md diagram line 95 still reads "tools/call: not yet supported (returns -32601)" for the broker path. Filter docs were updated but this architecture page contradicts merged behavior and will mislead operators.

@nerdalert
nerdalert force-pushed the brent-ai-mcp-tools-call-routing branch from 4ef1b2b to 0ba0843 Compare July 24, 2026 20:25
Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>
@nerdalert
nerdalert force-pushed the brent-ai-mcp-tools-call-routing branch from 0ba0843 to dc80d0a Compare July 24, 2026 20:26
@nerdalert

Copy link
Copy Markdown
Member Author

docs/architecture/agentic-protocols.md diagram line 95 still reads "tools/call: not yet supported (returns -32601)" for the broker path. Filter docs were updated but this architecture page contradicts merged behavior and will mislead operators.

@aslakknutsen ty!

  • Updated docs/architecture/agentic-protocols.md:95 so the broker diagram no longer
    says tools/call is unsupported.
  • It now says stateless tools/call routes by configured catalog tool, while current profile still returns -32601.

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.

3 participants