Skip to content

feat: port MCP output formatters to Swift with ANSI colors#132

Merged
EtanHey merged 3 commits intomainfrom
feat/brainbar-swift-formatting
Mar 29, 2026
Merged

feat: port MCP output formatters to Swift with ANSI colors#132
EtanHey merged 3 commits intomainfrom
feat/brainbar-swift-formatting

Conversation

@EtanHey
Copy link
Copy Markdown
Owner

@EtanHey EtanHey commented Mar 29, 2026

Summary

  • Port all 7 Python _format.py formatters to Swift Formatters.swift (396 LOC)
  • ANSI color codes: orange values, blue keys, green numbers (gated behind useColor param)
  • Wire brain_search and brain_store in MCPRouter to return formatted text instead of raw JSON
  • 21 new FormattersTests covering all 7 formatters + color toggling + box-drawing chars

Files Changed

File What
Formatters.swift (new, 396 LOC) 7 formatters: search results, store, entity card, entity simple, stats, digest, KG search
MCPRouter.swift (+3/-5) Wire formatters into handleBrainSearch and handleBrainStore
FormattersTests.swift (new, 297 LOC) 21 tests: content, ANSI colors, box-drawing chars
MCPRouterTests.swift (+19/-16) Update 3 filter tests to check formatted text instead of raw JSON

Test plan

  • 21/21 FormattersTests pass
  • 13/13 MCPRouterTests pass
  • 21/21 DatabaseTests pass
  • 7/7 MCPFramingTests pass
  • 62/62 total (excluding 3 pre-existing socket timeout failures)

🤖 Generated with Claude Code

Note

Port MCP output formatters to Swift with ANSI colors and box-drawing for brain tool responses

  • Adds a new Formatters enum in Formatters.swift with static methods to render brain tool outputs (brain_search, brain_store, entity cards, stats, digest, KG search) as human-readable terminal strings using Unicode box-drawing and optional ANSI colors.
  • Updates MCPRouter so brain_search returns formatted text instead of a raw JSON array, and brain_store returns a formatted status line instead of a JSON object.
  • Adds unit tests in FormattersTests.swift covering all formatters and ANSI toggle behavior; updates MCPRouterTests.swift to assert on formatted text substrings.
  • Behavioral Change: tools/call responses for brain_search and brain_store are no longer JSON-parseable — callers expecting structured JSON will break.

Macroscope summarized 94c6c55.

Summary by CodeRabbit

  • New Features

    • Endpoints now return human-readable, terminal-formatted text (search, store confirmations, entity views, stats, digests, KG search) with optional ANSI colors and Unicode box-drawing for improved readability.
  • Tests

    • Added comprehensive tests for all output formatters (content, truncation, symbols, color on/off) and updated routing tests to validate formatted-text responses.

EtanHey and others added 2 commits March 29, 2026 19:06
Tags are now intentionally passed through to compact search results
for the formatted display. Update the dropped_keys assertion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Port all 7 Python _format.py formatters to Swift Formatters.swift:
- formatSearchResults: box-drawing table with scores, projects, tags
- formatStoreResult: one-liner with checkmark
- formatEntityCard: rich entity card with profile, relations, memories
- formatEntitySimple: lighter entity card
- formatStats: knowledge base stats
- formatDigestResult: digest/enrich results with action items
- formatKGSearch: entity-aware KG search with facts graph

ANSI color codes for terminal readability:
- Orange (232,121,36) for values (scores, counts)
- Blue (88,166,255) for keys/labels (score:, imp:, tags:)
- Green (63,185,80) for numbers (chunk counts, result counts)
- Gated behind useColor parameter (default true)

Wire formatters into MCPRouter so brain_search and brain_store
return formatted text instead of raw JSON.

21 new FormattersTests + 13 updated MCPRouterTests = 62/62 green.

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 29, 2026

📝 Walkthrough

Walkthrough

Added a new text-formatting utility (Formatters) that renders MCP response payloads as ANSI/Unicode box-drawn strings and updated router handlers and tests to emit and validate formatted text instead of JSON payloads. Tests for all formatters were added and some router/test assertions adjusted.

Changes

Cohort / File(s) Summary
Formatter Implementation
brain-bar/Sources/BrainBar/Formatters.swift
New Formatters enum with public static functions: formatSearchResults, formatStoreResult, formatEntityCard, formatEntitySimple, formatStats, formatDigestResult, formatKGSearch. Implements truncation, padding, numeric formatting, tag extraction, ANSI optional coloring, and multi-line box-drawn output.
Handler Integration
brain-bar/Sources/BrainBar/MCPRouter.swift
Replaced ad-hoc JSON serialization in handleBrainSearch and handleBrainStore with calls to Formatters.formatSearchResults(...) and Formatters.formatStoreResult(...), returning formatted text payloads instead of JSON-encoded objects.
Formatter Tests
brain-bar/Tests/BrainBarTests/FormattersTests.swift
New comprehensive XCTest coverage for all formatter functions, verifying content labels, truncated fields, tags, status markers (checkmark/hourglass), ANSI escape presence/absence, and consistent box-drawing characters.
Handler Test Updates
brain-bar/Tests/BrainBarTests/MCPRouterTests.swift
Adjusted brain_search filter/cursor tests to treat result as formatted text: string-based assertions for box-drawing header and specific chunk identifiers instead of parsing JSON arrays.
Python Test Cleanup
tests/test_search_routing.py
Removed "tags" from the set of fields asserted absent in format="compact" responses (test assertion tweak).

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Router as MCPRouter
    participant Fmt as Formatters
    participant Response as ResponsePayload

    Client->>Router: POST /brain_search (query, filters)
    Router->>Fmt: formatSearchResults(query, results, total, useColor)
    Fmt-->>Router: formattedText (ANSI/box drawings)
    Router->>Response: build MCP response with formattedText
    Response-->>Client: return formatted text payload
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I nibble bytes and stitch a frame so neat,
Boxed lines and colors make outputs sweet.
From search to store, each result now sings,
Pretty text and tags on tiny rabbit wings. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.34% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: porting Python MCP output formatters to Swift with ANSI color support, which is the primary objective of this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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 feat/brainbar-swift-formatting

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.

Comment thread brain-bar/Sources/BrainBar/MCPRouter.swift
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
brain-bar/Sources/BrainBar/MCPRouter.swift (1)

13-23: 🧹 Nitpick | 🔵 Trivial

Remove unused StoreResultPayload struct.

The StoreResultPayload struct is no longer referenced anywhere in the codebase after the refactor to use Formatters.formatStoreResult(). Remove lines 13-23.

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

In `@brain-bar/Sources/BrainBar/MCPRouter.swift` around lines 13 - 23, Remove the
now-unused StoreResultPayload struct declaration: delete the private struct
StoreResultPayload { ... } (the type with chunkID, rowID, status and its
CodingKeys) since the repo now uses Formatters.formatStoreResult() instead;
ensure no remaining references to StoreResultPayload exist and run a build to
confirm no usage remains.
🤖 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/Formatters.swift`:
- Around line 61-66: formatNumber currently allocates a new NumberFormatter on
each call; change it to reuse a cached formatter by adding a static/lazy stored
NumberFormatter (e.g. static let cachedFormatter or static var sharedFormatter)
inside the same type and have formatNumber call that instead of creating a new
instance. Ensure the cached formatter is configured once (numberStyle =
.decimal, groupingSeparator = ",") and referenced by formatNumber; if
formatNumber may be called from background threads, either document main-thread
use or use a thread-local or synchronization strategy when accessing the shared
formatter.

---

Outside diff comments:
In `@brain-bar/Sources/BrainBar/MCPRouter.swift`:
- Around line 13-23: Remove the now-unused StoreResultPayload struct
declaration: delete the private struct StoreResultPayload { ... } (the type with
chunkID, rowID, status and its CodingKeys) since the repo now uses
Formatters.formatStoreResult() instead; ensure no remaining references to
StoreResultPayload exist and run a build to confirm no usage remains.
🪄 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: e96420f0-ecba-4883-b4ab-9f1d6e1cca88

📥 Commits

Reviewing files that changed from the base of the PR and between bdea5f8 and ddeb942.

📒 Files selected for processing (5)
  • brain-bar/Sources/BrainBar/Formatters.swift
  • brain-bar/Sources/BrainBar/MCPRouter.swift
  • brain-bar/Tests/BrainBarTests/FormattersTests.swift
  • brain-bar/Tests/BrainBarTests/MCPRouterTests.swift
  • tests/test_search_routing.py
💤 Files with no reviewable changes (1)
  • tests/test_search_routing.py
📜 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). (4)
  • GitHub Check: Macroscope - Correctness Check
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.13)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-03-18T00:12:36.931Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 87
File: brain-bar/Sources/BrainBar/MCPRouter.swift:0-0
Timestamp: 2026-03-18T00:12:36.931Z
Learning: In `brain-bar/Sources/BrainBar/MCPRouter.swift` (Swift, BrainBar MCP daemon), the notification guard `let isNotification = (rawID == nil || rawID is NSNull)` is the single and only point where a no-response decision is made. Any message that passes this guard has a non-nil, non-NSNull id and MUST return a proper JSON-RPC response. Returning `[:]` (empty dict = no response) anywhere after the notification guard is always a bug — it creates a silent client hang. Flag any `return [:]` that appears after the guard in future reviews.

Applied to files:

  • brain-bar/Sources/BrainBar/MCPRouter.swift
  • brain-bar/Tests/BrainBarTests/MCPRouterTests.swift
📚 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/MCPRouter.swift
  • brain-bar/Sources/BrainBar/Formatters.swift
📚 Learning: 2026-03-17T01:04:22.497Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 0
File: :0-0
Timestamp: 2026-03-17T01:04:22.497Z
Learning: Applies to src/brainlayer/mcp/**/*.py and brain-bar/Sources/BrainBar/MCPRouter.swift: The 8 required MCP tools are `brain_search`, `brain_store`, `brain_recall`, `brain_entity`, `brain_expand`, `brain_update`, `brain_digest`, `brain_tags`. `brain_tags` is the 8th tool, replacing `brain_get_person`, as defined in the Phase B spec merged in PR `#72`. The Python MCP server already implements `brain_tags`. Legacy `brainlayer_*` aliases must be maintained for backward compatibility.

Applied to files:

  • brain-bar/Tests/BrainBarTests/MCPRouterTests.swift
🪛 SwiftLint (0.63.2)
brain-bar/Tests/BrainBarTests/FormattersTests.swift

[Warning] 9-9: Classes should have an explicit deinit method

(required_deinit)

🔇 Additional comments (7)
brain-bar/Sources/BrainBar/Formatters.swift (1)

1-396: LGTM!

The formatters are well-structured and match the Python reference implementation. The code handles edge cases (empty results, nil values, multiple input formats for tags) correctly and uses appropriate truncation limits. The ANSI color gating via useColor parameter is clean.

brain-bar/Tests/BrainBarTests/FormattersTests.swift (1)

1-297: LGTM!

Comprehensive test coverage for all 7 formatters with good edge case testing (empty results, minimal entities, color toggling). The tests verify both content correctness and structural elements (box-drawing characters, ANSI escape sequences).

brain-bar/Sources/BrainBar/MCPRouter.swift (2)

233-233: LGTM!

The change from JSON serialization to Formatters.formatSearchResults() aligns with the Python MCP behavior (see src/brainlayer/mcp/_format.py). The formatter receives the necessary parameters (query, results, total count).


246-246: LGTM!

The change to use Formatters.formatStoreResult() matches the Python implementation's approach. Note that the superseded and queued parameters are not passed here—if those scenarios apply, you may need to wire them through later.

brain-bar/Tests/BrainBarTests/MCPRouterTests.swift (3)

228-236: LGTM!

The updated assertions correctly verify the formatted text output by checking for the box-drawing header character and the presence/absence of expected chunk IDs. This aligns with the router's switch from JSON to formatted text.


260-267: LGTM!

Consistent pattern with the project filter test—verifies formatted output structure and chunk ID filtering by importance.


301-309: LGTM!

Good awareness of the substring issue (line 308 comment). The test correctly verifies that unread filtering works by checking for "unread-1" in the output. The assertion on line 309 checking for "result" provides a lightweight sanity check that formatted output was produced.

Comment thread brain-bar/Sources/BrainBar/Formatters.swift Outdated
@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented Mar 29, 2026

@coderabbitai review

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented Mar 29, 2026

@cursor @BugBot review

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented Mar 29, 2026

@codex review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 29, 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 Mar 29, 2026

Excellent! All three fixes are correctly applied. Let me create a summary of what was done:

Summary

I've successfully addressed all code review issues identified by @coderabbitai and @macroscopeapp in PR #132:

Fixes Applied (commit 94c6c55)

  1. Critical Bug Fix - Search Results Display

    • Changed formatSearchResults to use content field instead of non-existent snippet field
    • Impact: Previously, when summary was empty, results would display blank text because snippet doesn't exist in the database schema
    • Root cause: BrainDatabase.search() returns fields: content, project, content_type, importance, created_at, summary, tags, session_id - but NOT snippet
    • Now correctly falls back to content when summary is empty
  2. Performance Optimization - NumberFormatter Caching

    • Converted formatNumber() to use a cached static NumberFormatter instead of creating a new instance on each call
    • Impact: Eliminates repeated allocations when formatting multiple search results (up to 100 per search)
    • Used lazy static initialization pattern for thread-safe singleton
  3. Code Cleanup - Remove Dead Code

    • Removed unused StoreResultPayload struct (12 lines)
    • Reason: No longer needed after refactoring to use Formatters.formatStoreResult() which returns plain text

All changes have been committed and pushed to the feat/brainbar-swift-formatting branch.

Open in Web Open in Cursor 

- Use 'content' field instead of non-existent 'snippet' field in formatSearchResults
- Cache NumberFormatter to avoid repeated allocations
- Remove unused StoreResultPayload struct

Co-authored-by: Etan Heyman <EtanHey@users.noreply.github.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ddeb942574

ℹ️ 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".

}
let stored = try db.store(content: content, tags: tags, importance: importance, source: "mcp")
return jsonEncode(StoreResultPayload(chunkID: stored.chunkID, rowID: stored.rowID, status: "stored"))
return Formatters.formatStoreResult(chunkId: stored.chunkID)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve machine-readable brain_store payload

Return a structured payload here instead of only formatted text: BrainBarServer.publishStoredChunk still calls extractStoredChunk to JSON-decode result.content[0].text into StoreResultPayload, and that decode now fails for every successful brain_store response ("✔ Stored → ..."). In the tagged-subscription path this causes the guard in publishStoredChunk to exit early, so resource/channel notifications and unread/ack sequencing are skipped whenever clients store memories through MCP.

Useful? React with 👍 / 👎.

lines.append("\u{2502}")

for (i, r) in results.enumerated() {
let score = r["score"] as? Double ?? 0
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 Badge Stop reporting synthetic zero relevance scores

Avoid defaulting to 0 for r["score"] in search output unless the DB actually provides scores: BrainDatabase.search populates result dictionaries without a score field, so this formatter renders score:0.00 for every hit regardless of ranking. That is a retrieval-quality regression in user-visible output because it presents fabricated relevance values as if they were real.

Useful? React with 👍 / 👎.

Comment on lines +46 to +50
private static func pad(_ text: String?, width: Int, align: Alignment = .left) -> String {
let t = text ?? ""
let s = t.count > width ? String(t.prefix(width - 1)) + "\u{2026}" : t
switch align {
case .left: return s.padding(toLength: width, withPad: " ", startingAt: 0)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Low BrainBar/Formatters.swift:46

The pad function uses inconsistent length semantics across alignment modes. For .left, it calls padding(toLength:withPad:startingAt:) which counts UTF-16 code units (inherited from NSString), but the t.count > width check and the .right/.center cases use s.count which counts Swift Character grapheme clusters. For strings containing emoji or combining characters, .left alignment produces a different visual width than .right or .center, breaking table alignment in terminal output. Consider using a consistent length metric — either convert all cases to use NSString length (e.g., (s as NSString).length) or use Swift Character count consistently by reimplementing left-padding without padding(toLength:).

-        case .left:   return s.padding(toLength: width, withPad: " ", startingAt: 0)
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file brain-bar/Sources/BrainBar/Formatters.swift around lines 46-50:

The `pad` function uses inconsistent length semantics across alignment modes. For `.left`, it calls `padding(toLength:withPad:startingAt:)` which counts UTF-16 code units (inherited from `NSString`), but the `t.count > width` check and the `.right`/`.center` cases use `s.count` which counts Swift `Character` grapheme clusters. For strings containing emoji or combining characters, `.left` alignment produces a different visual width than `.right` or `.center`, breaking table alignment in terminal output. Consider using a consistent length metric — either convert all cases to use `NSString` length (e.g., `(s as NSString).length`) or use Swift `Character` count consistently by reimplementing left-padding without `padding(toLength:)`.

Evidence trail:
1. brain-bar/Sources/BrainBar/Formatters.swift lines 46-56 (REVIEWED_COMMIT) - shows the `pad` function with `.left` using `padding(toLength:)` and `.right`/`.center` using `s.count`
2. Apple Developer Documentation for NSString: https://developer.apple.com/documentation/foundation/nsstring - states "All lengths, character indexes, and ranges are expressed in terms of 16-bit platform-endian values, with index values starting at 0" and "var length : Int - The number of UTF-16 code units in the receiver"
3. `padding(toLength:withPad:startingAt:)` is documented as an NSString method: https://developer.apple.com/documentation/foundation/nsstring/padding(tolength:withpad:startingat:)

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.

2 participants