feat: v0.21.0 MCP proxy (transparent governance for upstream MCP servers)#100
Conversation
PyPI page already shows supported Python versions. Trims badge row to keep it on one line. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ers) VaaraMCPProxy sits between an MCP client (Claude Code, Cursor, any MCP-capable host) and an upstream MCP server (SAP ADT MCP, SAP Graph API MCP, SAP Cloud ALM MCP, any community-built MCP server). Every tools/call request from the client routes through Vaara's interception pipeline before reaching the upstream. Allowed calls forward transparently and report the upstream outcome back to the scorer. Blocked calls return an MCP isError response with the block reason. Other MCP methods (initialize, tools/list, resources, notifications) forward unchanged. Adds src/vaara/integrations/mcp_proxy.py with the proxy and CLI entry, src/vaara/integrations/_mcp_upstream.py with the subprocess client and JSON-RPC id demultiplexer on a background reader thread, and tests/test_integrations_mcp_proxy.py with six smoke tests covering blocked tool calls, allowed forward, severity mapping, the _vaara_agent_id strip, non-tools/call passthrough, and invalid request handling. Distinct from mcp_server.py, which exposes Vaara itself as an MCP server for agents that consult Vaara as a tool. The proxy is the other direction: Vaara in the middle of a client-upstream pair, intercepting tool calls in flight. TS client bumped lockstep to 0.21.0. 718 tests passing.
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis PR releases version 0.21.0 with an MCP-aware proxy that transparently intercepts and classifies model tool calls through Vaara's policy engine. It includes upstream subprocess client infrastructure, the proxy request routing and interception logic, CLI wiring, and integration test coverage. ChangesMCP proxy feature and release
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
tests/test_integrations_mcp_proxy.py (1)
46-139: ⚡ Quick winAdd a regression test for upstream-close mid-request behavior.
Please add one smoke test where upstream request waiting is interrupted by upstream shutdown, and assert
_handle_requestreturns a JSON-RPC internal error response instead of raising.🤖 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 `@tests/test_integrations_mcp_proxy.py` around lines 46 - 139, Add a smoke test that ensures _handle_request handles an upstream shutdown by making p._upstream.request raise an exception (e.g., ConnectionError or the project's upstream-closed exception) while processing a request and asserting that _handle_request returns a JSON-RPC internal error response (error.code == -32603) instead of letting the exception propagate; create the test in the same style as existing tests, use the proxy fixture to get p and pipeline, set p._upstream.request.side_effect to the exception, call p._handle_request with any request that causes an upstream.request (e.g., a tools/list or tools/call request), and assert the returned value is a dict with "error" and code -32603 and that p._upstream.request was invoked.
🤖 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.
Inline comments:
In `@src/vaara/integrations/_mcp_upstream.py`:
- Around line 104-107: The code currently uses "assert pending.response is not
None" after "if not pending.event.wait(timeout=timeout):" which can raise
AssertionError and bypass caller error handling; change this to explicitly raise
ProxyError when pending.response is None (e.g., raise ProxyError("Upstream MCP
server shut down or closed connection before responding")) so callers get a
predictable ProxyError instead of AssertionError; apply the same replacement in
the other similar block that checks pending.response (the second occurrence
around the 159-164 area) and include any useful context (timeout value or
request id) in the error message if available.
---
Nitpick comments:
In `@tests/test_integrations_mcp_proxy.py`:
- Around line 46-139: Add a smoke test that ensures _handle_request handles an
upstream shutdown by making p._upstream.request raise an exception (e.g.,
ConnectionError or the project's upstream-closed exception) while processing a
request and asserting that _handle_request returns a JSON-RPC internal error
response (error.code == -32603) instead of letting the exception propagate;
create the test in the same style as existing tests, use the proxy fixture to
get p and pipeline, set p._upstream.request.side_effect to the exception, call
p._handle_request with any request that causes an upstream.request (e.g., a
tools/list or tools/call request), and assert the returned value is a dict with
"error" and code -32603 and that p._upstream.request was invoked.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 04af745f-3f3a-44bd-baaa-587bf04b90de
📒 Files selected for processing (8)
CHANGELOG.mdREADME.mdclients/ts/package.jsonpyproject.tomlsrc/vaara/__init__.pysrc/vaara/integrations/_mcp_upstream.pysrc/vaara/integrations/mcp_proxy.pytests/test_integrations_mcp_proxy.py
| if not pending.event.wait(timeout=timeout): | ||
| raise ProxyError(f"Upstream MCP server did not respond within {timeout}s") | ||
| assert pending.response is not None | ||
| return pending.response |
There was a problem hiding this comment.
Return ProxyError on upstream shutdown instead of asserting.
If the reader thread exits, it wakes pending requests without a response. This path hits the assert at Line 106, which can raise AssertionError (or be removed with -O) and escape caller error handling. Convert this to an explicit ProxyError when no response is available.
💡 Proposed fix
try:
self._write(payload)
if not pending.event.wait(timeout=timeout):
raise ProxyError(f"Upstream MCP server did not respond within {timeout}s")
- assert pending.response is not None
- return pending.response
+ if pending.response is None:
+ raise ProxyError("Upstream MCP server closed before responding")
+ if not isinstance(pending.response, dict):
+ raise ProxyError("Upstream MCP server returned non-object JSON-RPC")
+ return pending.response
finally:
with self._lock:
self._pending.pop(payload["id"], None)Also applies to: 159-164
🤖 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 `@src/vaara/integrations/_mcp_upstream.py` around lines 104 - 107, The code
currently uses "assert pending.response is not None" after "if not
pending.event.wait(timeout=timeout):" which can raise AssertionError and bypass
caller error handling; change this to explicitly raise ProxyError when
pending.response is None (e.g., raise ProxyError("Upstream MCP server shut down
or closed connection before responding")) so callers get a predictable
ProxyError instead of AssertionError; apply the same replacement in the other
similar block that checks pending.response (the second occurrence around the
159-164 area) and include any useful context (timeout value or request id) in
the error message if available.
Reader-thread exit during a pending request was setting the event without populating response, so request() hit assert pending.response is not None. AssertionError escapes the caller's ProxyError handler and is optimized out under python -O. Raise ProxyError explicitly with a regression test that simulates the reader-exit race deterministically. Flagged by CodeRabbit on PR #100.
| try: | ||
| if self._proc.stdin is not None: | ||
| self._proc.stdin.close() | ||
| except Exception: |
| self._proc.wait(timeout=5) | ||
| except subprocess.TimeoutExpired: | ||
| self._proc.kill() | ||
| except Exception: |
…ME section (#105) Two problems shipped in #100 and #101 that were fixed here: 1. README's MCP proxy code example used `npx --upstream-arg @sap/adt-mcp-server`, a fictional npm package. The MCP proxy block was also buried as a third-tier subsection under Framework integrations, despite v0.21.0 being the headline ship and having two worked demos. 2. SAP demo's example config used `npx -y @mario-andreschak/mcp-abap-abap-adt-api`, which is not published on npm. The real install path for that server is git clone + npm install + npm run build, then run with `node /path/to/dist/index.js`. Fixes: - README: removed the buried `### MCP proxy` subsection under Framework integrations and added a top-level `## MCP proxy` section between TypeScript client and OVERT. Uses `@sap/mdk-mcp-server` (verified on npm at version 0.3.7) as the one-line example. Links both demo folders. - examples/sap-mcp-proxy-demo/claude_code_config.example.json: primary `sap-mdk-via-vaara` block now uses the real `@sap/mdk-mcp-server` invocation. Sibling `_alternative_abap_adt` block uses the real `node /PATH_TO_YOUR/mcp-abap-abap-adt-api/dist/index.js` invocation. Sibling `_alternative_btp_odata` block clarified as clone+build. - examples/sap-mcp-proxy-demo/README.md: prereqs section names each upstream's real install path. Before/After config blocks now show the real MDK invocation. Note steering ABAP/OData readers to the alternative blocks in the example config. Verified upstreams via npm registry and GitHub API: `@sap/mdk-mcp-server` is published, official, Apache-2.0; `mario-andreschak/mcp-abap-abap-adt-api` is on GitHub (134 stars, MIT) but not on npm; `SAP/mdk-mcp-server` repo is Apache-2.0, 31 stars. Co-authored-by: vaaraio <267591518+vaaraio@users.noreply.github.com>
Summary
VaaraMCPProxy sits between an MCP client (Claude Code, Cursor, any MCP-capable host) and an upstream MCP server (SAP ADT MCP, SAP Graph API MCP, SAP Cloud ALM MCP, any community-built MCP server). Every
tools/callrequest from the client routes through Vaara's interception pipeline before reaching the upstream. Allowed calls forward transparently and report the upstream outcome back to the scorer. Blocked calls return an MCPisError: trueresponse with the block reason. Other MCP methods (initialize, tools/list, resources, notifications) forward unchanged.Distinct from
mcp_server.py, which exposes Vaara itself as an MCP server for agents that consult Vaara as a tool. The proxy is the other direction: Vaara in the middle of a client-upstream pair, intercepting tool calls in flight.What's in
src/vaara/integrations/mcp_proxy.py:VaaraMCPProxy+ CLI. Invoke aspython -m vaara.integrations.mcp_proxy --upstream <cmd> [--upstream-arg ...].src/vaara/integrations/_mcp_upstream.py:UpstreamMCPClientwith subprocess lifecycle and JSON-RPC id demultiplexing on a background reader thread. Internal module, not part of the public surface.tests/test_integrations_mcp_proxy.py: six smoke tests covering blocked tool calls, allowed forward, severity mapping, the_vaara_agent_idstrip, non-tools/call passthrough, and invalid request handling.pyproject.toml,src/vaara/__init__.py,clients/ts/package.json(0.20.0 → 0.21.0).Strategic frame
The community SAP MCP servers shipped at SAP Sapphire 2026 plus the Anthropic-SAP partnership announcement put SAP ABAP / Graph / Cloud ALM behind Claude Code in enterprise developer workflows. None of the parties (SAP, Anthropic, the community MCP server authors) ships the runtime governance layer the EU AI Act high-risk obligations require for those tool calls. The proxy is that layer in OSS today.
Test plan
pytest tests/passes (718 passed, 12 skipped)tests/test_integrations_mcp_proxy.pyall greenexamples/sap-mcp-proxy-demo/lands in a follow-up PRSummary by CodeRabbit
Release Notes v0.21.0
New Features
Documentation