Skip to content

fix(stdio): treat "id": null as a request, not a notification#169

Open
dpage wants to merge 3 commits into
mainfrom
fix/issue-152-stdio-null-id
Open

fix(stdio): treat "id": null as a request, not a notification#169
dpage wants to merge 3 commits into
mainfrom
fix/issue-152-stdio-null-id

Conversation

@dpage

@dpage dpage commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

Test plan

  • go test ./internal/mcp/ — added Run()-level tests that pipe
    stdin and assert the resulting stdout
  • notifications/initialized produces no response
  • Unknown notification methods (/cancelled,
    /roots/list_changed, some/unknown) produce no response
  • {"jsonrpc":"2.0","id":null,"method":"unknown/method"} receives
    -32601 Method not found
  • {"jsonrpc":"2.0","id":null,"method":"ping"} receives an
    empty-object result
  • make build, make test, make fmt all clean. make lint
    reports 3 pre-existing staticcheck findings in
    internal/embedding/{ollama,provider}.go (untouched by this PR;
    present on origin/main).

Closes #152

Summary by CodeRabbit

  • Bug Fixes

    • Improved JSON-RPC stdio behavior: requests with explicit null IDs now return proper error responses for unknown methods instead of being dropped; initialization and other notifications are consistently filtered and produce no responses.
  • Documentation

    • Changelog updated to document the notification vs. request differentiation for stdio transport.
  • Tests

    • Added end-to-end stdio/RPC tests validating notification filtering and null-ID request handling.

Review Change Stack

Per JSON-RPC 2.0 §4.1, a Notification is a Request object without an
"id" member, while a Request whose id is explicitly null is still a
Request and MUST receive a reply. Go's interface{} unmarshaling
collapses "id absent" and "id": null to the same nil value, so the
previous default-branch guard (`if req.ID != nil`) silently dropped
unknown-method requests with explicit null id and the hardcoded
notifications/initialized case suppressed null-id requests for that
method too.

Mirror the HTTP transport fix from #142/#143: probe the raw bytes
with hasIDField to distinguish absent vs null id, short-circuit
notifications before dispatch, and remove the now-redundant nil-id
guards in handleRequest's default branch and handlePing. The
notifications/initialized case is no longer reachable through
dispatch and is dropped from the switch.

Tests exercise the full Run() read loop with stdin piped from a pipe:
- notifications/initialized produces no output
- unknown notification methods produce no output
- {"id": null, "method": "unknown"} receives -32601
- {"id": null, "method": "ping"} receives an empty-object result

Closes #152

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR fixes a JSON-RPC 2.0 compliance bug in the stdio transport where requests with explicit "id": null targeting unknown methods were silently dropped instead of returning a -32601 Method not found error. The fix reuses the raw-bytes hasIDField probe to distinguish notifications (no id field) from requests (field present, including null values), updates request dispatch to unconditionally send errors, and adds comprehensive integration tests.

Changes

JSON-RPC stdio dispatch compliance

Layer / File(s) Summary
Notification detection and request handler updates
internal/mcp/server.go
STDIO loop now reads raw scanned bytes and uses hasIDField to detect presence of an id member. Notification filtering moved into Run. handleRequest and handlePing no longer skip responses when req.ID is nil; unknown-method errors are sent for requests (including "id": null). Comments updated accordingly.
Integration test coverage for stdio dispatch
internal/mcp/server_test.go
Adds runStdio helper that pipes JSON-RPC lines into Server.Run() and captures stdout. Adds tests asserting notifications/initialized and other notifications produce no stdout, and that requests with explicit "id": null get -32601 for unknown methods or a proper ping response. Removes an earlier unit test that directly invoked handlePing.
Changelog documentation
docs/changelog.md
Unreleased changelog documents the stdio behavior change: absent id = notification suppressed; explicit "id": null = request that must be replied to; notifications/initialized is now filtered uniformly via hasIDField.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • pgEdge/pgedge-postgres-mcp#167: Related changes to stdio JSON-RPC dispatch and handlePing around nil vs null id handling.
  • pgEdge/pgedge-postgres-mcp#143: Introduced the hasIDField raw-bytes approach for the HTTP transport to distinguish absent id from null; this PR applies the same approach to stdio.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing the stdio transport to treat requests with explicit id:null as requests rather than notifications.
Linked Issues check ✅ Passed The PR fully addresses issue #152 requirements: distinguishes notifications from id:null requests via hasIDField, returns -32601 for unknown methods, and adds comprehensive Run()-level tests.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #152: stdio transport bug fix, documentation updates in changelog, and targeted test coverage with no unrelated modifications.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-152-stdio-null-id

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

@codacy-production

codacy-production Bot commented May 27, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 23 complexity

Metric Results
Complexity 23

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Address CodeRabbit's docstring coverage pre-merge warning. Move the
explanatory comments above the new TestRunStdio_* and handlePing
function declarations and prefix them with the function name, per Go
doc convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 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 `@internal/mcp/server.go`:
- Around line 223-228: The JSON-RPC ping handler (handlePing -> sendResponse)
promises to reply even when req.ID is a nil interface (incoming "id": null), but
JSONRPCResponse.ID in internal/mcp/types.go is tagged `json:"id,omitempty"` so
marshaling will drop the id field; remove `omitempty` from the JSON tag on
JSONRPCResponse.ID (or implement custom MarshalJSON for JSONRPCResponse to
explicitly emit "id": null when ID == nil) so responses to null-id pings include
`"id": null`, and update the stdio tests to assert the raw JSON contains `"id":
null`.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 18ceb9d8-0db0-4776-a611-444d0a9a85e8

📥 Commits

Reviewing files that changed from the base of the PR and between 317aa48 and 2406292.

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

Comment thread internal/mcp/server.go
Per JSON-RPC 2.0 §5.1, the response object MUST include the id
member; the value is the originating request's id, or null when the
id cannot be determined (parse error / invalid request) or when the
request itself used "id": null.

JSONRPCResponse.ID was tagged json:"id,omitempty", so Go's encoder
dropped the field whenever the interface{} value was nil — producing
a response without an id field, which is itself a malformed JSON-RPC
body. This contradicted the new handlePing doc comment promise that
null-id pings receive a response, and affected both the HTTP transport
(parse errors and the id:null branch added in #142/#143) and the new
stdio behavior in this PR.

Drop omitempty from ID so a nil interface marshals as JSON null. Add
raw-byte assertions to the stdio null-id tests and to the HTTP
TestHandleNotification_NullIDIsRequest regression guard so the spec
compliance is verified, not just the parsed Error code.

Addresses CodeRabbit review on PR #169 (discussion_r3310822965).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

stdio transport: requests with explicit id: null are silently dropped instead of receiving -32601

1 participant