Skip to content

fix(brainbar): stabilize json-rpc envelope order#274

Merged
EtanHey merged 1 commit into
mainfrom
fix/brainbar-jsonrpc-envelope-order
May 3, 2026
Merged

fix(brainbar): stabilize json-rpc envelope order#274
EtanHey merged 1 commit into
mainfrom
fix/brainbar-jsonrpc-envelope-order

Conversation

@EtanHey
Copy link
Copy Markdown
Owner

@EtanHey EtanHey commented May 3, 2026

Summary

  • Stabilizes BrainBar MCP JSON-RPC response encoding so top-level envelopes serialize with jsonrpc first, then id, then result/error.
  • Routes the raw newline fallback through the same encoder as the Content-Length MCP path.
  • Adds regression coverage for both direct framing and the real tools/list router response.

Diagnosis

Claude Desktop rejected BrainBar on boot with Invalid JSON-RPC message even though the response was structurally valid JSON-RPC. Live framed socket probing against the installed BrainBar showed the Content-Length path emitted {"id":2,"jsonrpc":"2.0","result":...}. A release-mode TDD test reproduced Foundation dictionary serialization emitting non-MCP-like top-level ordering ({"result":...,"id":2,"jsonrpc":"2.0"}). Python/Node MCP servers emit jsonrpc first; this PR makes BrainBar match that stable envelope shape instead of relying on Swift dictionary serialization.

Tests

  • TDD red first: swift test --package-path brain-bar -c release --filter MCPFramingTests/testEncodesJSONRPCEnvelopeWithJSONRPCFirst failed before the fix with body {"result":{"tools":[{"name":"brain_search"}]},"id":2,"jsonrpc":"2.0"}.
  • swift test --package-path brain-bar -c release --filter MCPFramingTests/testEncodesJSONRPCEnvelopeWithJSONRPCFirst passed after fix.
  • swift test --package-path brain-bar --filter MCPFramingTests passed: 8 tests, 0 failures.
  • swift test --package-path brain-bar --filter MCPRouterTests/testEncodedToolsListEnvelopeStartsWithJSONRPC passed.
  • swift test --package-path brain-bar passed: 339 tests, 0 failures.
  • ./scripts/run_tests.sh passed: pytest unit suite 1823 passed, 9 skipped, 75 deselected, 1 xfailed; MCP registration 3 passed; isolated eval/hook routing 32 passed; bun suite 1 pass; FTS5 regression PASS.
  • Pre-push test gate reran ./scripts/run_tests.sh and passed with the same suites.

Deployment Plan

After merge, rebuild and reinstall BrainBar, restart it with enrichment paused, then smoke-test /tmp/brainbar.sock using tools/list to confirm the deployed response starts with {"jsonrpc":"2.0","id":2,"result":...}.


Note

Medium Risk
Changes JSON-RPC response serialization at the transport boundary (both Content-Length and newline-delimited modes), which could affect compatibility with existing clients if they relied on Foundation’s previous output shape.

Overview
Ensures BrainBar’s MCP JSON-RPC responses serialize with a deterministic envelope key order (jsonrpc, then id, then result/error) to avoid client-side rejection caused by Foundation dictionary ordering.

Routes the newline-delimited JSON-RPC fallback through the same encoder as the Content-Length framing path, and adds regression tests covering both the framing encoder and a real tools/list router response.

Reviewed by Cursor Bugbot for commit 45ac3a7. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix JSON-RPC envelope key order in MCPFraming to be deterministic

  • Replaces JSONSerialization-based encoding with a new MCPFraming.encodeJSONResponse helper that explicitly orders top-level keys as jsonrpc, id, then result/error.
  • Applies to both Content-Length framed responses (MCPFraming.swift) and newline-delimited responses (BrainBarServer.swift).
  • Falls back to Foundation serialization if required keys are missing.
  • Two new tests assert that encoded envelopes start with jsonrpc first in both framing modes.

Macroscope summarized 45ac3a7.

Summary by CodeRabbit

Release Notes

  • Improvements

    • Enhanced JSON-RPC response serialization to ensure deterministic and consistent formatting of protocol responses.
  • Tests

    • Added tests verifying proper JSON-RPC response envelope structure and formatting behavior.

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 3, 2026

@coderabbitai review

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 3, 2026

@codex review

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 3, 2026

@cursor @BugBot review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

📝 Walkthrough

Walkthrough

The PR refactors JSON-RPC response serialization in the MCP framing layer to produce deterministic JSON-RPC envelopes with fixed key ordering (jsonrpc, id, result/error). Both the Content-Length framing and newline-delimited paths now use the new encodeJSONResponse helper. Tests verify that encoded responses start with the jsonrpc key.

Changes

Deterministic JSON-RPC Envelope Encoding

Layer / File(s) Summary
Serialization Helpers
brain-bar/Sources/BrainBar/MCPFraming.swift
Added encodeJSONResponse(_:) to build JSON-RPC envelopes with deterministic key order (jsonrpc, id, result/error). Added encodeJSONValue(_:) helper using JSONSerialization with .fragmentsAllowed.
Core Framing Integration
brain-bar/Sources/BrainBar/MCPFraming.swift, brain-bar/Sources/BrainBar/BrainBarServer.swift
MCPFraming.encode(_:) now constructs Content-Length frames via encodeJSONResponse. sendResponse newline-delimited branch switches from JSONSerialization.data(withJSONObject:) to MCPFraming.encodeJSONResponse(response).
Tests & Verification
brain-bar/Tests/BrainBarTests/MCPFramingTests.swift, brain-bar/Tests/BrainBarTests/MCPRouterTests.swift
Added testEncodesJSONRPCEnvelopeWithJSONRPCFirst to verify framed body starts with {"jsonrpc":"2.0",...}. Added testEncodedToolsListEnvelopeStartsWithJSONRPC to verify router responses maintain key ordering.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Poem

🐰 A rabbit hops through JSON keys,
Ordering them with JSON expertise,
First jsonrpc, then id so neat,
result or error to make it complete!
Deterministic frames, what a treat! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. 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 'fix(brainbar): stabilize json-rpc envelope order' directly and clearly summarizes the primary change—ensuring JSON-RPC response envelopes serialize with a deterministic field order, with jsonrpc first.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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/brainbar-jsonrpc-envelope-order

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
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cursor
Copy link
Copy Markdown

cursor Bot commented May 3, 2026

Swift is not available in this environment, but that's fine. The PR description indicates all tests have passed. Let me now provide a comprehensive review:

Bug Bot Review: PR #274 - fix(brainbar): stabilize json-rpc envelope order

✅ Summary

This PR addresses a critical production bug where Claude Desktop rejected BrainBar on boot due to non-deterministic JSON-RPC envelope key ordering. The fix is correct, minimal, and well-tested.


🎯 Core Changes Review

MCPFraming.swift - The Fix ✅

The new encodeJSONResponse function enforces deterministic JSON-RPC envelope ordering:

  1. Correct ordering: jsonrpcidresult/error matches the MCP spec and what Claude Desktop expects
  2. Scoped fix: Only applies to JSON-RPC envelopes (has jsonrpc, id, and result/error), falls back to standard JSONSerialization for other structures
  3. Clean implementation: Uses manual Data building with .fragmentsAllowed for nested values

Key insight from the code: The guard clause correctly identifies JSON-RPC envelopes and avoids breaking non-envelope JSON objects.

guard let jsonrpc = response["jsonrpc"],
      let id = response["id"],
      response["result"] != nil || response["error"] != nil else {
    return try JSONSerialization.data(withJSONObject: response)
}

BrainBarServer.swift - Unified Path ✅

The change at line 405 routes the newline-delimited fallback through the same encoder:

-guard let jsonData = try? JSONSerialization.data(withJSONObject: response) else { return false }
+guard let jsonData = try? MCPFraming.encodeJSONResponse(response) else { return false }

This is critical — before this change, Content-Length framing used the fixed encoder but newline-delimited didn't, creating two different serialization paths. Now both paths produce identical envelope shapes.


🧪 Test Coverage Review

MCPFramingTests

func testEncodesJSONRPCEnvelopeWithJSONRPCFirst()
  • Creates a response dictionary with keys in wrong order (id first)
  • Verifies the encoded body starts with {"jsonrpc":"2.0","id":2,"result":
  • Good assertion message that explains the business requirement

MCPRouterTests

func testEncodedToolsListEnvelopeStartsWithJSONRPC()
  • Tests the real production path via the router's tools/list handler
  • Verifies the framed response (not just the encoder in isolation)
  • This catches any regressions in the full request → router → encoder → framing stack

Both tests follow TDD pattern described in the PR: they would have failed before the fix, pass after.


🔍 Correctness Analysis

No Breaking Changes

  • The encodeJSONValue helper uses .fragmentsAllowed which is correct for encoding nested objects/arrays as JSON fragments
  • The fallback to standard JSONSerialization for non-envelope objects preserves existing behavior
  • Error propagation is preserved (all throws are correctly propagated)

Edge Cases Handled

  1. Empty objects: Guard clause catches missing keys
  2. Error responses: else if let error handles error envelopes correctly
  3. Notifications without id: Would fail the guard and fall back to standard serialization (correct — notifications aren't JSON-RPC responses)

⚠️ Potential Concerns (None critical)

  1. Manual JSON construction — This approach works but is brittle if the spec changes. However:

    • The JSON-RPC spec is stable
    • The alternative (custom Encoder) would be overengineered for this narrow fix
    • Tests provide regression protection
  2. Unicode handlingData("{\"jsonrpc\":".utf8) is safe because:

    • The literal strings are ASCII
    • The nested values use JSONSerialization which handles Unicode correctly

🧩 Integration Points

BrainBarServer write paths

Both framing modes now use the same encoder:

  • Content-Length path: Line 401 → MCPFraming.encodeencodeJSONResponse
  • Newline-delimited path: Line 405 → encodeJSONResponse

This eliminates the divergence that likely caused the bug in the first place.

MCPRouter compatibility

The router builds response dictionaries using standard Swift Dictionary literals. The encoder correctly handles whatever key order the runtime produces.


🚨 Safety Concerns: None

No concurrency issues, no locking, no DB writes. This is pure data transformation.

Resource safety: The manual Data building is efficient — no intermediate allocations beyond what JSONSerialization already does for nested values.


📋 Code Quality

Strengths ✅

  • Excellent comments explaining why (Claude Desktop's behavior)
  • Clear function naming (encodeJSONResponse vs encode)
  • Private helpers properly scoped
  • Test names are descriptive

Minor nits (non-blocking)

  • Line 147 magic byte 0x7D could be Data("}".utf8)[0] for readability (but current form is fine)

🔬 Test Execution Evidence

From PR description:

  • swift test --package-path brain-bar -c release: 339 tests, 0 failures
  • ./scripts/run_tests.sh: 1823 passed, 9 skipped, 75 deselected, 1 xfailed
  • Pre-push gate: Re-ran and passed ✅

Critical test: testEncodesJSONRPCEnvelopeWithJSONRPCFirst failed before fix, passes after — confirms the fix works.


🎯 Deployment Plan Review

From PR description:

After merge, rebuild and reinstall BrainBar, restart it with enrichment paused, then smoke-test /tmp/brainbar.sock using tools/list to confirm the deployed response starts with {"jsonrpc":"2.0","id":2,"result":...}.

This is good, but I'd add:

  1. Verify Claude Desktop boot: The real test is whether Claude Desktop accepts BrainBar on launch
  2. Monitor debug logs: Check /tmp/brainbar-debug.log for any framing issues
  3. Rollback plan: Keep old binary until smoke test passes

✅ Final Verdict

LGTM — Ship it.

This PR:

  • ✅ Fixes the root cause (non-deterministic key ordering)
  • ✅ Unifies both framing paths (Content-Length + newline-delimited)
  • ✅ Has comprehensive test coverage (unit + integration)
  • ✅ All 339 BrainBar tests pass + full test suite passes
  • ✅ Minimal risk — scoped change with fallback behavior
  • ✅ No breaking changes to existing functionality

The manual JSON construction is the simplest fix for this MCP spec compliance issue. Alternative approaches (custom JSONEncoder, dictionary sorting) would be more complex for marginal gain.


📝 Recommendations

  1. Post-deployment: Monitor Claude Desktop boot success rate for 24h
  2. Future-proofing: If MCP adds more top-level envelope keys, this code will need updates. Consider adding a TODO or unit test that documents expected envelope shape.
  3. Documentation: Consider adding a comment in BrainBarServer.swift at line 405 explaining why both paths must use the same encoder.

No changes required before merge. The PR is production-ready.

Open in Web Open in Cursor 

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@brain-bar/Sources/BrainBar/MCPFraming.swift`:
- Around line 127-131: The encodeJSONResponse helper currently falls back to
JSONSerialization when "result"/"error" are missing, which lets notification
envelopes use non-deterministic Foundation ordering; update encodeJSONResponse
to always use the deterministic JSON path (remove the JSONSerialization
fallback) so notifications as well as responses are encoded via the
deterministic encoder used elsewhere (referencing encodeJSONResponse and the
deterministicEncoder/encoding helper in MCPFraming) and return that Data for all
messages.

In `@brain-bar/Tests/BrainBarTests/MCPRouterTests.swift`:
- Around line 155-157: The assertion currently expects the framed response to
contain "result":" (a string) but tools/list returns an object, so update the
XCTAssertTrue that uses body.hasPrefix(...) in MCPRouterTests.swift to expect
...,"result":{ instead of ...,"result":"; locate the XCTAssertTrue call (the one
checking body.hasPrefix and the accompanying failure message) and change the
prefix string accordingly so the test matches an object-valued result.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ae40e121-7838-42d6-90bb-2aaa3166149a

📥 Commits

Reviewing files that changed from the base of the PR and between dada249 and 45ac3a7.

📒 Files selected for processing (4)
  • brain-bar/Sources/BrainBar/BrainBarServer.swift
  • brain-bar/Sources/BrainBar/MCPFraming.swift
  • brain-bar/Tests/BrainBarTests/MCPFramingTests.swift
  • brain-bar/Tests/BrainBarTests/MCPRouterTests.swift
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Macroscope - Correctness Check
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.11)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-03-18T00:12:08.774Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 87
File: brain-bar/Sources/BrainBar/BrainBarServer.swift:118-129
Timestamp: 2026-03-18T00:12:08.774Z
Learning: In Swift files under brain-bar/Sources/BrainBar, enforce that when a critical dependency like the database is nil due to startup ordering (socket before DB), any tool handler that accesses the database must throw an explicit error (e.g., ToolError.noDatabase) instead of returning a default/empty value. Do not allow silent defaults (e.g., guard let db else { return ... }). Flag patterns that silently return defaults when db is nil, as this masks startup timing issues. This guidance applies broadly to similar Swift files in the BrainBar module, not just this one location.

Applied to files:

  • brain-bar/Sources/BrainBar/BrainBarServer.swift
  • brain-bar/Sources/BrainBar/MCPFraming.swift
📚 Learning: 2026-03-29T18:45:40.988Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 133
File: brain-bar/Sources/BrainBar/BrainDatabase.swift:0-0
Timestamp: 2026-03-29T18:45:40.988Z
Learning: In the BrainBar module’s Swift database layer (notably BrainDatabase.swift), ensure that the `search()` function’s `unreadOnly=true` path orders results by the delivery frontier cursor so the watermark `maxRowID` stays contiguous. Specifically, when `unreadOnly` is enabled, the query must include `ORDER BY c.rowid ASC` (e.g., via `let orderByClause = unreadOnly ? "c.rowid ASC" : "f.rank"`). Do not replace the unread-only ordering with relevance-based sorting (e.g., `f.rank`) unconditionally or for the unread-only path, as it can introduce gaps in the watermark and incorrectly mark unseen rows as delivered. Flag any future change to the `ORDER BY` clause in this function that makes relevance sorting apply to the unread-only case.

Applied to files:

  • brain-bar/Sources/BrainBar/BrainBarServer.swift
  • brain-bar/Sources/BrainBar/MCPFraming.swift
🔇 Additional comments (2)
brain-bar/Tests/BrainBarTests/MCPFramingTests.swift (1)

131-133: Same prefix bug as the router test.

This fixture also encodes result as an object, so the expected prefix needs to end with ...,"result":{ instead of ...,"result":".

brain-bar/Sources/BrainBar/BrainBarServer.swift (1)

403-409: Good: reuse the shared encoder here.

This keeps the newline-delimited branch aligned with the Content-Length path for JSON-RPC response envelopes.

Comment on lines +127 to +131
static func encodeJSONResponse(_ response: [String: Any]) throws -> Data {
guard let jsonrpc = response["jsonrpc"],
let id = response["id"],
response["result"] != nil || response["error"] != nil else {
return try JSONSerialization.data(withJSONObject: response)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Keep notification envelopes on the deterministic path too.

This helper still falls back to JSONSerialization when result/error is absent, so newline-delimited notifications can still come out with Foundation-dependent top-level ordering. If those messages hit the same client/parser, the original ordering bug is not fully closed here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/MCPFraming.swift` around lines 127 - 131, The
encodeJSONResponse helper currently falls back to JSONSerialization when
"result"/"error" are missing, which lets notification envelopes use
non-deterministic Foundation ordering; update encodeJSONResponse to always use
the deterministic JSON path (remove the JSONSerialization fallback) so
notifications as well as responses are encoded via the deterministic encoder
used elsewhere (referencing encodeJSONResponse and the
deterministicEncoder/encoding helper in MCPFraming) and return that Data for all
messages.

Comment on lines +155 to +157
XCTAssertTrue(
body.hasPrefix(#"{"jsonrpc":"2.0","id":2,"result":"#),
"Claude Desktop expects jsonrpc to be the first envelope key for tools/list; got: \(body.prefix(80))"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix the expected prefix.

tools/list returns an object in result, so the framed body starts with ...,"result":{..., not ...,"result":".... As written, this assertion will fail even when the encoder is correct.

🔧 Proposed fix
-        XCTAssertTrue(
-            body.hasPrefix(#"{"jsonrpc":"2.0","id":2,"result":"#),
-            "Claude Desktop expects jsonrpc to be the first envelope key for tools/list; got: \(body.prefix(80))"
-        )
+        XCTAssertTrue(
+            body.hasPrefix(#"{"jsonrpc":"2.0","id":2,"result":{"#),
+            "Claude Desktop expects jsonrpc to be the first envelope key for tools/list; got: \(body.prefix(80))"
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
XCTAssertTrue(
body.hasPrefix(#"{"jsonrpc":"2.0","id":2,"result":"#),
"Claude Desktop expects jsonrpc to be the first envelope key for tools/list; got: \(body.prefix(80))"
XCTAssertTrue(
body.hasPrefix(#"{"jsonrpc":"2.0","id":2,"result":{"#),
"Claude Desktop expects jsonrpc to be the first envelope key for tools/list; got: \(body.prefix(80))"
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Tests/BrainBarTests/MCPRouterTests.swift` around lines 155 - 157,
The assertion currently expects the framed response to contain "result":" (a
string) but tools/list returns an object, so update the XCTAssertTrue that uses
body.hasPrefix(...) in MCPRouterTests.swift to expect ...,"result":{ instead of
...,"result":"; locate the XCTAssertTrue call (the one checking body.hasPrefix
and the accompanying failure message) and change the prefix string accordingly
so the test matches an object-valued result.

@EtanHey EtanHey merged commit edbfb36 into main May 3, 2026
7 checks passed
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.

1 participant