Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .mcp.json.example
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
{
"_comment": "Run: ~/Gits/orchestrator/scripts/mcp-setup.sh ~/Gits/brainlayer",
"_comment2": "brainlayer-socat: BrainBar daemon on Unix socket (replaces 9 Python processes)",
"mcpServers": {
"brainlayer": {
"command": "socat",
"args": ["STDIO", "UNIX-CONNECT:/tmp/brainbar.sock"]
},
"brainlayer-legacy": {
"_comment": "Fallback: original Python MCP server (remove after BrainBar is stable)",
"command": "brainlayer-mcp"
},
"voicelayer": {
"command": "voicelayer-mcp"
},
"cmux": {
"command": "node",
"args": [
"/Users/etanheyman/Gits/orchestrator/tools/cmux-mcp/dist/index.js"
]
}
Comment on lines +16 to 21
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 | 🟡 Minor

Remove hardcoded user-specific path from example file.

The cmux entry contains a hardcoded absolute path (/Users/etanheyman/...) that won't work for other developers. Example files should use placeholder paths or environment variables.

💡 Suggested fix
     "cmux": {
       "command": "node",
       "args": [
-        "/Users/etanheyman/Gits/orchestrator/tools/cmux-mcp/dist/index.js"
+        "${ORCHESTRATOR_PATH}/tools/cmux-mcp/dist/index.js"
       ]
     }

Or use a comment placeholder like "/path/to/orchestrator/tools/cmux-mcp/dist/index.js".

📝 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
"cmux": {
"command": "node",
"args": [
"/Users/etanheyman/Gits/orchestrator/tools/cmux-mcp/dist/index.js"
]
}
"cmux": {
"command": "node",
"args": [
"${ORCHESTRATOR_PATH}/tools/cmux-mcp/dist/index.js"
]
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.mcp.json.example around lines 16 - 21, The example file contains a
hardcoded absolute user path in the "cmux" entry (keys "command" and "args")
which should be replaced with a generic placeholder or environment variable;
update the "args" value to something like a reusable placeholder (e.g.
"/path/to/orchestrator/tools/cmux-mcp/dist/index.js" or reference an env var) so
the example is portable and remove any user-specific paths.

}
}
4 changes: 4 additions & 0 deletions brain-bar/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.build/
.swiftpm/
*.xcodeproj/
DerivedData/
23 changes: 23 additions & 0 deletions brain-bar/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// swift-tools-version: 6.0
import PackageDescription

let package = Package(
name: "BrainBar",
platforms: [
.macOS(.v14),
],
targets: [
.executableTarget(
name: "BrainBar",
path: "Sources/BrainBar",
linkerSettings: [
.linkedLibrary("sqlite3"),
]
),
.testTarget(
name: "BrainBarTests",
dependencies: ["BrainBar"],
path: "Tests/BrainBarTests"
),
]
)
58 changes: 58 additions & 0 deletions brain-bar/Sources/BrainBar/BrainBarApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// BrainBarApp.swift — Entry point for BrainBar menu bar daemon.
//
// Menu bar app (no Dock icon) that owns the BrainLayer SQLite database
// and serves MCP tools over /tmp/brainbar.sock.

import AppKit
import SwiftUI

// MARK: - App Delegate

final class AppDelegate: NSObject, NSApplicationDelegate {
private var server: BrainBarServer?

func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.accessory)

let srv = BrainBarServer()
server = srv
srv.start()
}

func applicationWillTerminate(_ notification: Notification) {
server?.stop()
}

func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
false
}
}

// MARK: - SwiftUI App entry point

@main
struct BrainBarApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

var body: some Scene {
MenuBarExtra("BrainBar", systemImage: "brain.head.profile") {
VStack(alignment: .leading, spacing: 6) {
Text("BrainBar")
.font(.system(.caption, weight: .bold))
Text("Memory daemon active")
.font(.system(.caption2))
.foregroundStyle(.secondary)
Divider()
Button("Quit BrainBar") {
NSApplication.shared.terminate(nil)
}
.keyboardShortcut("q")
}
.padding(8)
}

Settings {
EmptyView()
}
}
}
199 changes: 199 additions & 0 deletions brain-bar/Sources/BrainBar/BrainBarServer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// BrainBarServer.swift — Integrated socket server + MCP router + database.
//
// Owns:
// - Unix domain socket on /tmp/brainbar.sock
// - MCP Content-Length framing parser
// - JSON-RPC router
// - SQLite database (single-writer)

import Foundation

final class BrainBarServer: @unchecked Sendable {
private let socketPath: String
private let dbPath: String
private let queue = DispatchQueue(label: "com.brainlayer.brainbar.server", qos: .userInitiated)
private var listenFD: Int32 = -1
private var listenSource: DispatchSourceRead?
private var clients: [Int32: ClientState] = [:]
private var router: MCPRouter!
private var database: BrainDatabase!

struct ClientState {
var source: DispatchSourceRead
var framing: MCPFraming
}

init(socketPath: String = "/tmp/brainbar.sock", dbPath: String? = nil) {
self.socketPath = socketPath
self.dbPath = dbPath ?? Self.defaultDBPath()
}

static func defaultDBPath() -> String {
let home = FileManager.default.homeDirectoryForCurrentUser.path
return "\(home)/.local/share/brainlayer/brainlayer.db"
}

func start() {
queue.async { [weak self] in
self?.startOnQueue()
}
Comment on lines +36 to +39
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

Don't let startup fail silently.

Lines 36-39 detach startup from launch, and the failure paths in Lines 58-60 and 79-92 only log and return. Line 50 also cannot surface a database-open failure back to the caller, so the menu bar app can finish launching even when the daemon never became ready.

Also applies to: 48-52, 58-60, 79-92

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

In `@brain-bar/Sources/BrainBar/BrainBarServer.swift` around lines 36 - 39, The
startup currently runs asynchronously in start() via queue.async and swallows
failures inside startOnQueue() (including the database open and the other
logged-but-return paths), so change the API to surface errors: make start()
accept a completion handler (or make it throwing/synchronous) and have
startOnQueue() propagate thrown errors or call the completion with a failure;
stop swallowing the result of openDatabase (or the databaseOpen function) and
propagate its error upward instead of only logging, and on the main thread call
the supplied completion (or rethrow) so the menu bar app can handle/terminate if
the daemon failed to become ready; update any call sites that relied on the old
fire-and-forget start() to wait for the completion or handle the thrown error.

}

func stop() {
queue.sync {
self.cleanup()
}
}

private func startOnQueue() {
// Initialize database and router
database = BrainDatabase(path: dbPath)
router = MCPRouter()
router.setDatabase(database)

// Clean up stale socket
unlink(socketPath)

let fd = socket(AF_UNIX, SOCK_STREAM, 0)
guard fd >= 0 else {
NSLog("[BrainBar] Failed to create socket: errno %d", errno)
return
}

var addr = sockaddr_un()
addr.sun_family = sa_family_t(AF_UNIX)
let pathBytes = socketPath.utf8CString
guard pathBytes.count <= MemoryLayout.size(ofValue: addr.sun_path) else {
NSLog("[BrainBar] Socket path too long (%d > %d): %@",
pathBytes.count, MemoryLayout.size(ofValue: addr.sun_path), socketPath)
close(fd)
return
}
withUnsafeMutablePointer(to: &addr.sun_path) { ptr in
ptr.withMemoryRebound(to: CChar.self, capacity: pathBytes.count) { dest in
pathBytes.withUnsafeBufferPointer { src in
_ = memcpy(dest, src.baseAddress!, src.count)
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

let bindResult = withUnsafePointer(to: &addr) { addrPtr in
addrPtr.withMemoryRebound(to: sockaddr.self, capacity: 1) { ptr in
bind(fd, ptr, socklen_t(MemoryLayout<sockaddr_un>.size))
}
}
guard bindResult == 0 else {
NSLog("[BrainBar] Failed to bind: errno %d", errno)
close(fd)
return
}

chmod(socketPath, 0o600)

guard listen(fd, 16) == 0 else {
NSLog("[BrainBar] Failed to listen: errno %d", errno)
close(fd)
unlink(socketPath)
return
}

listenFD = fd

let source = DispatchSource.makeReadSource(fileDescriptor: fd, queue: queue)
source.setEventHandler { [weak self] in
self?.acceptClient()
}
source.setCancelHandler { [weak self] in
guard let self else { return }
close(fd)
listenFD = -1
}
source.resume()
listenSource = source

NSLog("[BrainBar] Server listening on %@", socketPath)
}

private func acceptClient() {
let clientFD = accept(listenFD, nil, nil)
guard clientFD >= 0 else { return }

let flags = fcntl(clientFD, F_GETFL)
_ = fcntl(clientFD, F_SETFL, flags | O_NONBLOCK)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

var nosigpipe: Int32 = 1
setsockopt(clientFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, socklen_t(MemoryLayout<Int32>.size))

let readSource = DispatchSource.makeReadSource(fileDescriptor: clientFD, queue: queue)
readSource.setEventHandler { [weak self] in
self?.readFromClient(fd: clientFD)
}
readSource.setCancelHandler {
close(clientFD)
}
readSource.resume()

clients[clientFD] = ClientState(source: readSource, framing: MCPFraming())
NSLog("[BrainBar] Client connected (fd: %d)", clientFD)
}

private func readFromClient(fd: Int32) {
var buf = [UInt8](repeating: 0, count: 65536)
let n = read(fd, &buf, buf.count)

if n <= 0 {
if n == -1, errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR {
return
}
clients[fd]?.source.cancel()
clients.removeValue(forKey: fd)
return
}

guard var state = clients[fd] else { return }
state.framing.append(Data(buf[0..<n]))

let messages = state.framing.extractMessages()
for msg in messages {
let response = router.handle(msg)
if !response.isEmpty {
sendResponse(fd: fd, response: response)
}
}

clients[fd] = state
}

private func sendResponse(fd: Int32, response: [String: Any]) {
guard let framed = try? MCPFraming.encode(response) else { return }
framed.withUnsafeBytes { ptr in
var totalWritten = 0
while totalWritten < framed.count {
let n = write(fd, ptr.baseAddress!.advanced(by: totalWritten), framed.count - totalWritten)
if n < 0 {
if errno == EAGAIN || errno == EWOULDBLOCK {
// Kernel buffer full — brief retry
usleep(1000) // 1ms
continue
}
break // Real error
}
if n == 0 { break } // EOF
totalWritten += n
}
Comment on lines +167 to +183
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 | 🔴 Critical

Don't spin on a blocked client socket on the server queue.

Lines 174-177 retry forever with usleep(1000) on the same serial queue that accepts clients and processes requests. One slow client can pin the queue here indefinitely and stall the entire daemon. Queue the unwritten tail behind a writable source, or close the client after a bounded retry budget.

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

In `@brain-bar/Sources/BrainBar/BrainBarServer.swift` around lines 167 - 183, The
sendResponse(fd: Int32, response: [String: Any]) is currently busy-waiting on
write with repeated usleep calls on the server serial queue; change this to a
non-blocking, bounded strategy: attempt one immediate non-blocking write pass
(using write as currently done), and if not fully written, stop retrying on the
server queue and either (a) schedule the remaining bytes to be written by
attaching a DispatchSource.makeWriteSource for the client fd that resumes when
writable and completes the tail write, or (b) if you prefer a simpler fix,
enforce a small retry budget (e.g., N attempts) and then close(fd) and log an
error; implement this using the existing sendResponse, framed buffer, and fd
variables and avoid any infinite loop on the server queue.

}
}

private func cleanup() {
listenSource?.cancel()
listenSource = nil
for (_, state) in clients {
state.source.cancel()
}
clients.removeAll()
if listenFD >= 0 { listenFD = -1 }
unlink(socketPath)
database?.close()
NSLog("[BrainBar] Server stopped")
}
}
Loading
Loading