Skip to content

Add transport abstraction for MCP proxy, fix no-params DLP bypass#103

Merged
luckyPipewrench merged 10 commits into
mainfrom
feat/mcp-http-proxy
Feb 16, 2026
Merged

Add transport abstraction for MCP proxy, fix no-params DLP bypass#103
luckyPipewrench merged 10 commits into
mainfrom
feat/mcp-http-proxy

Conversation

@luckyPipewrench
Copy link
Copy Markdown
Owner

@luckyPipewrench luckyPipewrench commented Feb 16, 2026

Summary

  • Introduce MessageReader/MessageWriter interfaces decoupling MCP scanning from stdio transport framing
  • Fix no-params bypass: ScanRequest now scans full raw message when params is empty/null
  • Harden test servers to use explicit IPv4 loopback (avoids IPv6 sandbox failures)
  • Fix syncWriter atomicity (single Write call prevents partial framing)

Test plan

  • 5 new bypass tests verify DLP/injection caught in result/error/unknown fields
  • 12 new transport tests cover read/write/round-trip/oversized/error paths
  • Pen test: 98/100 pass (2 pre-existing allowlist)
  • Red team: 17 creative attack vectors tested, all findings addressed

Summary by CodeRabbit

  • Bug Fixes

    • More reliable framed message I/O: safer handling of empty/oversized messages, single-write framing, and improved read/write error paths.
    • Avoided duplicate forwards and improved forwarding/error handling.
    • Logs and responses now show fully decoded request URLs for clearer diagnostics.
    • Broader DLP/injection detection including multi-layer decoding and encoded-secret checks.
  • Tests

    • Expanded coverage for edge-case scans, transport framing, IPv4 backends, and DNS-restricted environments.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 16, 2026

Warning

Rate limit exceeded

@luckyPipewrench has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 27 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

📝 Walkthrough

Walkthrough

Refactors MCP stdio I/O to framed MessageReader/MessageWriter abstractions with StdioReader/StdioWriter, updates forwarding and proxy logic to use those interfaces, expands DLP/URL-decoding and scanner checks, and adapts tests (IPv4-only test servers, new test wrappers and additional unit tests).

Changes

Cohort / File(s) Summary
Transport Abstraction
internal/mcp/transport.go, internal/mcp/transport_test.go
Add MessageReader/MessageWriter interfaces and StdioReader/StdioWriter implementations (newline-framed messages, max-size enforcement, copy-on-return); comprehensive unit tests for read/write semantics and edge cases.
Input Scanning
internal/mcp/input.go, internal/mcp/input_test.go
Replace bufio line-scanner with message-based ReadMessage/WriteMessage flow; change ForwardScannedInput signature to use interfaces; add test wrapper fwdScannedInput, new no-params DLP/injection tests, and update many tests to use stdio wrappers.
Proxy Core
internal/mcp/proxy.go, internal/mcp/proxy_test.go
Change ForwardScanned to accept MessageReader/MessageWriter; add syncWriter compile-time assertion and single-write semantics; centralize strip-or-block logic; replace legacy helpers; add tests (oversized messages, strip/block cases) and test helper fwdScanned.
Stdio/Test Plumbing
internal/mcp/tools_test.go, internal/mcp/transport_test.go, internal/mcp/*_test.go
Replace many direct ForwardScanned/stdio calls with fwdScanned wrappers; exercise StdioReader/Writer round-trips and update assertions to align with new write paths.
Proxy & CLI Tests (IPv4/DNS hardening)
internal/proxy/proxy_test.go, internal/cli/cli_test.go
Add newIPv4Server helper to bind backends to IPv4 loopback; skip DNS-dependent tests in restricted environments; adjust httptest server usage across tests.
Scanner / DLP Enhancements
internal/scanner/scanner.go, internal/scanner/text_dlp.go
Increase iterative URL-decode ceiling and broaden iterativeDecode/checkDLP targets (query keys/values, decoded path, dot-collapsed hostnames, env-var checks); update comments to reflect multi-layer decoding.
Minor docs & tests
internal/mcp/scan.go, internal/config/config_test.go, other small test updates
Add explanatory comment for ScanStream behavior; replace literal "audit" with ModeAudit in tests; small import/test helper adjustments.

Sequence Diagram(s)

sequenceDiagram
participant Client
participant StdioReader as "StdioReader\n(MessageReader)"
participant Proxy as "Proxy\n(ForwardScanned / RunProxy)"
participant StdioWriter as "StdioWriter\n(MessageWriter)"
participant Backend

Client->>StdioReader: send newline-delimited message
StdioReader->>Proxy: ReadMessage() -> []byte
Proxy->>Proxy: scan/process (DLP, injection, approver)
alt clean → forward
    Proxy->>StdioWriter: WriteMessage(msg)
    StdioWriter->>Backend: single Write(msg + "\n")
else block/strip/warn
    Proxy->>Proxy: block/strip/log, optionally WriteMessage(strip/block) 
end
Backend-->>StdioWriter: response message
StdioWriter->>Proxy: WriteMessage(response)
Proxy->>StdioReader: (route back) WriteMessage -> Client/stdout
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • DeathCamel58

Poem

🐰 Hop, I frame the line with care,

Reader nibbles, Writer shares the fare.
Proxy hums and skips a trap,
Clean messages take a happy lap.
Hooray — carrot code to spare! 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.20% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically identifies the two main changes: introducing a transport abstraction for MCP proxy and fixing a no-params DLP bypass vulnerability. It directly maps to the core objectives and is concise without unnecessary detail.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/mcp-http-proxy

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 and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 16, 2026

Codecov Report

❌ Patch coverage is 97.70992% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/proxy/proxy.go 89.28% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

Comment thread internal/mcp/proxy.go Fixed
Comment thread internal/mcp/transport.go Fixed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@internal/mcp/proxy.go`:
- Around line 37-45: The allocation math (len(msg)+1) in syncWriter.WriteMessage
can trigger a CodeQL overflow warning; replace the manual make+copy with a safe
append-based copy that avoids explicit size arithmetic: create a fresh slice
copy via buf := append(append([]byte(nil), msg...), '\n') (so you don't mutate
the caller's backing array), then write buf as before in
syncWriter.WriteMessage; keep the mutex locking/unlocking and return the write
error as currently implemented.

In `@internal/mcp/transport.go`:
- Around line 51-74: Wrap the scanner and write errors and add a size guard:
when checking sr.scanner.Err() return a wrapped error like fmt.Errorf("scanner
error: %w", err) instead of returning err directly; in StdioWriter.WriteMessage
enforce the same maxLineSize used by StdioReader (compare len(msg) to
maxLineSize and return a wrapped error such as fmt.Errorf("message too large: %d
> %d", len(msg), maxLineSize)) before writing; and wrap the underlying write
error from sw.w.Write with fmt.Errorf("writing message: %w", err). Use the
existing symbols sr.scanner.Err, StdioWriter.WriteMessage, and maxLineSize to
locate and apply these changes.
🧹 Nitpick comments (2)
internal/cli/cli_test.go (1)

146-151: Add a timeout to the DNS probe to avoid test hangs.

A blocked resolver can stall indefinitely; a short timeout keeps the skip guard safe.

♻️ Suggested change
-	if _, err := net.DefaultResolver.LookupHost(context.Background(), "example.com"); err != nil {
+	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+	defer cancel()
+	if _, err := net.DefaultResolver.LookupHost(ctx, "example.com"); err != nil {
 		t.Skip("DNS unavailable (restricted environment)")
 	}
internal/mcp/transport_test.go (1)

11-107: Consider consolidating the StdioReader cases into a table-driven test.

Several tests differ only by input/expected output, which fits the project’s test style guideline.

As per coding guidelines: Table-driven tests should be used where applicable in Go test files.

Comment thread internal/mcp/proxy.go
Comment thread internal/mcp/transport.go Outdated
Introduce MessageReader/MessageWriter interfaces to decouple MCP
scanning from stdio framing, enabling future HTTP transport support.
Refactor ForwardScanned and ForwardScannedInput to use the new
interfaces with StdioReader/StdioWriter concrete implementations.

Fix a bypass where ScanRequest returned clean when JSON-RPC params
was empty/null, allowing secrets in result/error/unknown fields to
evade DLP and injection scanning. Now scans the full raw message
when params is absent.

Harden test infrastructure: force IPv4 loopback listeners in
httptest servers to avoid failures in sandboxed environments
without IPv6 support.
Address CodeQL overflow warning on len(msg)+1 allocation by adding
maxLineSize bounds check to both syncWriter and StdioWriter. Wrap
scanner errors with context. Add DNS timeout to test skip guard.
Replace string literal "audit" with ModeAudit constant to satisfy
goconst linter (v2.9.0 detects existing constant).
Test the maxLineSize overflow guard on both syncWriter and
StdioWriter to cover the error branch added in the previous
commit.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
internal/mcp/transport_test.go (1)

181-196: Consider testing the wrapped error message from WriteMessage.

TestStdioWriter_TooLarge checks err.Error() contains "message too large" — good. But TestStdioWriter_PropagatesError (line 141) only checks err != nil. Since the past review recommended wrapping write errors in transport.go, you may want to verify the wrapped error message here too, or at minimum use errors.Is/errors.As to confirm the underlying error is propagated.

- TestRunProxy_WithToolConfig: exercises tool scanning config init path
- TestRunProxy_InputScanningBlocksNotification: verifies notifications
  with DLP matches are silently dropped (no error response sent)

Improves RunProxy coverage from 91.1% to 95.6%.
- Use errors.Is for http.ErrServerClosed comparison (wrapped error safety)
- Extract stripOrBlock helper to deduplicate strip-failure fallback logic
- Increase maxDecodeRounds from 3 to 10 for multi-layer encoding
- Add direct unit tests for stripOrBlock (valid strip + invalid JSON paths)
Red team finding: base64/hex-encoded secrets in individual MCP tool
arguments bypassed DLP because ScanTextForDLP was only called on the
joined string (not valid base64). Now scans each extracted string
individually for encoded patterns.

Display fix (review feedback): fetch proxy responses and logs now show
the fully-decoded URL via normalizeDisplayURL, so operators see the
actual resolved URL instead of partially-decoded intermediates.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@internal/mcp/proxy.go`:
- Around line 34-48: In syncWriter.WriteMessage, after writing the buffer with
sw.w.Write(buf) wrap and return the underlying error using fmt.Errorf("writing
message: %w", err) instead of returning err directly; keep the existing
maxLineSize check and mutex behavior, and ensure fmt is imported if not already
for error wrapping.
🧹 Nitpick comments (1)
internal/proxy/proxy.go (1)

502-514: normalizeDisplayURL duplicates iterativeDecode from scanner.go — use a shared constant instead of hardcoded 500.

Both functions have identical logic: iteratively QueryUnescape until the string stabilizes. iterativeDecode in internal/scanner/scanner.go uses maxDecodeRounds = 500, but normalizeDisplayURL hardcodes 500. Since proxy.go already imports the scanner package, the cleanest fix is to export maxDecodeRounds (or create an exported IterativeDecode helper) and reuse it to prevent drift.

At minimum, define a named constant in proxy.go:

 func normalizeDisplayURL(raw string) string {
-	for range 500 {
+	for range maxDisplayDecodeRounds {

Comment thread internal/mcp/proxy.go
Addresses CodeRabbit review: use fmt.Errorf wrapping on write
errors to match codebase error-handling convention.
Export scanner.IterativeDecode and remove duplicate
normalizeDisplayURL from proxy. Revert unrelated ScanStream
comment addition.
@luckyPipewrench luckyPipewrench merged commit ccdcd50 into main Feb 16, 2026
9 checks passed
@luckyPipewrench luckyPipewrench deleted the feat/mcp-http-proxy branch February 16, 2026 17:46
luckyPipewrench added a commit that referenced this pull request Feb 16, 2026
Update CHANGELOG with all changes since v0.2.2 (PRs #99-#103):
transport abstraction, demo improvements, framework guides,
strict-mode allowlist fix, DLP bypass fixes, static analysis fixes.

Update test count (1,580+) and version refs in compliance mapping
and LangGraph guide.
luckyPipewrench added a commit that referenced this pull request Feb 16, 2026
Update CHANGELOG with all changes since v0.2.2 (PRs #99-#103):
transport abstraction, demo improvements, framework guides,
strict-mode allowlist fix, DLP bypass fixes, static analysis fixes.

Update test count (1,580+) and version refs in compliance mapping
and LangGraph guide.
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