Skip to content

fix(mcp): reject a server-initiated request as a streamable-HTTP response - #1054

Open
chiliec wants to merge 2 commits into
Gitlawb:mainfrom
chiliec:fix/mcp-http-reject-request-as-response
Open

fix(mcp): reject a server-initiated request as a streamable-HTTP response#1054
chiliec wants to merge 2 commits into
Gitlawb:mainfrom
chiliec:fix/mcp-http-reject-request-as-response

Conversation

@chiliec

@chiliec chiliec commented Sep 13, 2026

Copy link
Copy Markdown

Summary

networkClient.request (streamable HTTP) matched the decoded POST body to the in-flight call on id alone. A server that answers a tools/call POST with a method-bearing frame such as {"jsonrpc":"2.0","id":1,"method":"roots/list","params":{}} therefore returned err = nil with an empty result, so the caller saw a tool that returned nothing instead of a protocol error.

This applies the same isRequestOrNotification() guard that deliverEventMessage and decodeSSERPCMessage already use on the SSE paths, before the id match, and returns an error naming the unexpected method:

MCP tools/call expected a response from server plain, got "roots/list" request

Regression test TestNetworkClientRejectsServerInitiatedRequestAsResponse drives an httptest server that answers tools/call with the frame from the issue and asserts CallTool returns an error mentioning "roots/list". Same shape as TestStdioClientIgnoresServerInitiatedRequestsInPendingResponses, adapted to the HTTP client.

Linked issue

Fixes #1050

Checklist

  • The linked issue already has the issue-approved label.
  • go build ./..., go vet ./..., and go test ./... pass locally.
  • gofmt clean.
  • Tests added/updated for the change (and run under -race where relevant).
  • UI changes include screenshots or a short recording where possible. (no UI change)

Validation

Go 1.26.6 (from go.mod), Linux.

  • RED→GREEN: with the network_client.go change stashed, the new test fails with CallTool() error = nil, result = mcp.CallToolResult{Content:[]mcp.Content(nil), IsError:false}, want protocol error; with it restored, it passes.
  • go test ./internal/mcp/... -count=1 → ok; go test ./internal/mcp/ -race -count=1 → ok.
  • go build ./..., go vet ./..., make fmt-check, git diff HEAD --check → clean.
  • go test ./...: every package passes except internal/sandbox, internal/tools, internal/tui, which fail identically on a clean main checkout in my environment (the clone lives under /tmp, and those tests treat /tmp as a write root). Unrelated to this diff.

Summary by CodeRabbit

  • Bug Fixes
    • Improved error handling when a server sends an unexpected request or notification where a response is expected.
    • Error messages now identify the unexpected method, making connection issues easier to diagnose.

…onse

networkClient.request matched the decoded POST body to the in-flight call on
id alone, so a server that answered tools/call with a method-bearing frame
produced an empty, error-free result. Apply the same isRequestOrNotification
guard the SSE paths already use and return a protocol error naming the method.

Fixes Gitlawb#1050

Signed-off-by: Vladimir Babin <vovababin@gmail.com>
@greptile-apps

greptile-apps Bot commented Sep 13, 2026

Copy link
Copy Markdown

Greptile Summary

This PR prevents a server-initiated JSON-RPC request from being accepted as the response to an in-flight streamable-HTTP call.

  • Adds method-bearing message validation before response-ID matching.
  • Adds an end-to-end regression test covering a roots/list request returned from tools/call.
  • Leaves SSE handling unchanged, where method-bearing events are filtered while awaiting the actual response.

Confidence Score: 4/5

The PR appears safe to merge; the only identified concern is a non-blocking diagnostic accuracy issue.

The protocol guard correctly prevents method-bearing frames from being accepted as responses, and the regression test reaches the intended behavior. The error wording is misleading only when the method-bearing frame is a notification rather than a request.

Files Needing Attention: internal/mcp/network_client.go

Important Files Changed

Filename Overview
internal/mcp/network_client.go Rejects method-bearing plain HTTP response frames correctly, although the new diagnostic inaccurately labels notifications as requests.
internal/mcp/network_client_test.go Adds a focused regression test that exercises the public CallTool path and verifies the unexpected method is reported.

Reviews (1): Last reviewed commit: "fix(mcp): reject a server-initiated requ..." | Re-trigger Greptile

Comment on lines +219 to +221
if message.isRequestOrNotification() {
return fmt.Errorf("MCP %s expected a response from server %s, got %q request", method, client.server.Name, message.Method)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Notification mislabeled as request

A method-bearing notification without an ID also satisfies isRequestOrNotification(), but this error always calls the frame a “request.” When a server returns a notification where an HTTP response is expected, callers receive a misleading protocol diagnostic. Describe it as a “request or notification,” or more generally as a method-bearing message.

Suggested change
if message.isRequestOrNotification() {
return fmt.Errorf("MCP %s expected a response from server %s, got %q request", method, client.server.Name, message.Method)
}
if message.isRequestOrNotification() {
return fmt.Errorf("MCP %s expected a response from server %s, got %q request or notification", method, client.server.Name, message.Method)
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch — a method-bearing frame with no id is a notification, not a request. Reworded to expected a response from server X, got method "roots/list" instead in 3dc67b9 so it stays accurate for both.

@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 6462b8e5-e68f-4ebf-8d81-cbc01c259586

📥 Commits

Reviewing files that changed from the base of the PR and between 086b420 and 3dc67b9.

📒 Files selected for processing (1)
  • internal/mcp/network_client.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/mcp/network_client.go

Included review availability: Your plan provides up to 4 included reviews per hour; 2 remain after this review.


Walkthrough

The streamable-HTTP MCP client now rejects server-initiated requests or notifications when a response is expected. A regression test verifies that a roots/list frame produces an error during CallTool.

Changes

MCP response validation

Layer / File(s) Summary
Response guard and regression test
internal/mcp/network_client.go, internal/mcp/network_client_test.go
The client checks for request or notification frames before response ID validation. The test sends a roots/list frame for tools/call and verifies that CallTool returns an error naming the unexpected method.

Priority: ⬇️ Low

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

Change: Bug fix · Severity of issue fixed: Low

Merge Risk: ⚪ Minimal · up to 3dc67

The client now returns a protocol error instead of accepting a server request as a tool response. No actionable merge risk remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: rejecting server-initiated requests received as streamable-HTTP responses.
Linked Issues check ✅ Passed Issue #1050 requires a method guard before response ID matching, a protocol error that names the unexpected method, and an HTTP regression test. networkClient.request now calls `message.isRequestOrN…
Out of Scope Changes check ✅ Passed The diff adds only the streamable-HTTP response guard and its focused regression test. Both changes directly implement Issue #1050. No unrelated production or test changes are present.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

coderabbitai[bot]
coderabbitai Bot previously approved these changes Sep 13, 2026
A method-bearing frame without an id is a notification, so the error
should not call it a request.

Signed-off-by: Vladimir Babin <vovababin@gmail.com>

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

Approving. This is exactly the fix #1050 asked for, and the test is the one I hoped someone would write.

The guard goes in before the id match, so a method-bearing frame is rejected on its own terms rather than falling through to an id comparison that happens to fail, and the error names the unexpected method, which is what makes it diagnosable. Reusing isRequestOrNotification() means this path and the stdio path now answer the same question the same way, which was the point of filing it separately rather than leaving it inside the stdio issue.

Verified rather than read. The test passes, and removing the three added lines reproduces the exact symptom I described in the issue:

CallTool() error = nil, result = CallToolResult{Content:nil, IsError:false}

An error-free empty tool result, which is the thing that makes this hard to trace back from a user report. So the guard is load-bearing rather than decorative. internal/mcp is green here, and CI came back 9 of 9 after I released the fork gate.

Your commit message is also more accurate than my issue text: describing the frame by its method rather than calling it a request is the better framing, since a notification hits the same path.

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.

MCP streamable-HTTP client matches a response on id with no method guard

2 participants