From 1a1301690d2770311d46b455c987c597cd744ae8 Mon Sep 17 00:00:00 2001 From: Test User Date: Sun, 3 May 2026 12:20:53 +0300 Subject: [PATCH 1/3] fix(brainbar): compact tools list for claude extension --- brain-bar/Sources/BrainBar/MCPRouter.swift | 11 ++++- .../Tests/BrainBarTests/MCPRouterTests.swift | 35 +++++++++++----- .../SocketIntegrationTests.swift | 41 +++++-------------- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/brain-bar/Sources/BrainBar/MCPRouter.swift b/brain-bar/Sources/BrainBar/MCPRouter.swift index 665cadea..689090b4 100644 --- a/brain-bar/Sources/BrainBar/MCPRouter.swift +++ b/brain-bar/Sources/BrainBar/MCPRouter.swift @@ -119,7 +119,7 @@ final class MCPRouter: @unchecked Sendable { "jsonrpc": "2.0", "id": id, "result": [ - "tools": Self.toolDefinitions + "tools": Self.compactToolDefinitions ] ] } @@ -821,6 +821,15 @@ final class MCPRouter: @unchecked Sendable { idempotent: true ) + // Claude Desktop's MCPB utility process currently parses extension stdout + // in 8192-byte chunks. Keep the raw newline tools/list response below that + // boundary by omitting optional annotations from the public list response. + nonisolated(unsafe) static let compactToolDefinitions: [[String: Any]] = toolDefinitions.map { tool in + var compact = tool + compact.removeValue(forKey: "annotations") + return compact + } + nonisolated(unsafe) static let toolDefinitions: [[String: Any]] = [ [ "name": "brain_search", diff --git a/brain-bar/Tests/BrainBarTests/MCPRouterTests.swift b/brain-bar/Tests/BrainBarTests/MCPRouterTests.swift index 3272e4ea..6c1f5dec 100644 --- a/brain-bar/Tests/BrainBarTests/MCPRouterTests.swift +++ b/brain-bar/Tests/BrainBarTests/MCPRouterTests.swift @@ -158,6 +158,30 @@ final class MCPRouterTests: XCTestCase { ) } + func testEncodedToolsListFitsClaudeExtensionRawMessageLimit() throws { + let router = MCPRouter() + let response = router.handle([ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/list", + ]) + + let body = try MCPFraming.encodeJSONResponse(response) + let tools = (response["result"] as? [String: Any])?["tools"] as? [[String: Any]] ?? [] + + XCTAssertLessThan( + body.count, + 8192, + "Claude Desktop's MCPB utility process parses raw extension stdout in 8192-byte chunks" + ) + for tool in tools { + XCTAssertNil( + tool["annotations"], + "\(tool["name"] ?? "unknown") should omit optional annotations from tools/list to keep the raw MCPB response under 8 KiB" + ) + } + } + func testEachToolHasInputSchema() throws { let router = MCPRouter() let request: [String: Any] = [ @@ -177,17 +201,8 @@ final class MCPRouterTests: XCTestCase { } func testEachToolHasExpectedAnnotations() throws { - let router = MCPRouter() - let request: [String: Any] = [ - "jsonrpc": "2.0", - "id": 12, - "method": "tools/list", - ] - - let response = router.handle(request) - let tools = (response["result"] as? [String: Any])?["tools"] as? [[String: Any]] ?? [] let toolsByName = Dictionary( - uniqueKeysWithValues: tools.compactMap { tool -> (String, [String: Any])? in + uniqueKeysWithValues: MCPRouter.toolDefinitions.compactMap { tool -> (String, [String: Any])? in guard let name = tool["name"] as? String else { return nil } return (name, tool) } diff --git a/brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift b/brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift index 1ed01566..5fecd104 100644 --- a/brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift +++ b/brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift @@ -93,39 +93,18 @@ final class SocketIntegrationTests: XCTestCase { XCTAssertNotNil(tools) XCTAssertEqual(tools?.count, 16) - let toolsByName = Dictionary( - uniqueKeysWithValues: (tools ?? []).compactMap { tool -> (String, [String: Any])? in - guard let name = tool["name"] as? String else { return nil } - return (name, tool) - } + let encodedResponse = try MCPFraming.encodeJSONResponse(response) + XCTAssertLessThan( + encodedResponse.count, + 8192, + "Socket tools/list must stay below Claude Desktop MCPB's raw 8192-byte parse boundary" ) - let expected: [String: (readOnly: Bool, destructive: Bool, idempotent: Bool, openWorld: Bool)] = [ - "brain_search": (true, false, true, false), - "brain_store": (false, false, false, false), - "brain_get_person": (true, false, true, false), - "brain_recall": (true, false, true, false), - "brain_entity": (true, false, true, false), - "brain_digest": (false, false, false, false), - "brain_update": (false, false, true, false), - "brain_expand": (true, false, true, false), - "brain_tags": (true, false, true, false), - "brain_supersede": (false, true, false, false), - "brain_archive": (false, true, false, false), - "brain_enrich": (false, false, false, false), - "brain_subscribe": (false, false, false, false), - "brain_unsubscribe": (false, false, true, false), - "brain_ack": (false, false, true, false), - "brain_maintenance_rebuild_trigram": (false, false, true, false), - ] - - for (name, taxonomy) in expected { - let annotations = toolsByName[name]?["annotations"] as? [String: Any] - XCTAssertNotNil(annotations, "\(name) must expose MCP tool annotations over socket transport") - XCTAssertEqual(annotations?["readOnlyHint"] as? Bool, taxonomy.readOnly, "\(name) readOnlyHint mismatch") - XCTAssertEqual(annotations?["destructiveHint"] as? Bool, taxonomy.destructive, "\(name) destructiveHint mismatch") - XCTAssertEqual(annotations?["idempotentHint"] as? Bool, taxonomy.idempotent, "\(name) idempotentHint mismatch") - XCTAssertEqual(annotations?["openWorldHint"] as? Bool, taxonomy.openWorld, "\(name) openWorldHint mismatch") + for tool in tools ?? [] { + XCTAssertNil( + tool["annotations"], + "\(tool["name"] ?? "unknown") should omit optional annotations over socket transport" + ) } } From 4b1f020c4eeabc0a68c6d81d2764cc4506a993cb Mon Sep 17 00:00:00 2001 From: Test User Date: Sun, 3 May 2026 12:36:14 +0300 Subject: [PATCH 2/3] fix(brainbar): scope mcpb tools compaction to raw transport --- .../Sources/BrainBar/BrainBarServer.swift | 24 +++++- brain-bar/Sources/BrainBar/MCPRouter.swift | 11 +-- .../Tests/BrainBarTests/MCPRouterTests.swift | 22 +++--- .../SocketIntegrationTests.swift | 78 ++++++++++++++++++- 4 files changed, 110 insertions(+), 25 deletions(-) diff --git a/brain-bar/Sources/BrainBar/BrainBarServer.swift b/brain-bar/Sources/BrainBar/BrainBarServer.swift index be85dc16..5a7496a8 100644 --- a/brain-bar/Sources/BrainBar/BrainBarServer.swift +++ b/brain-bar/Sources/BrainBar/BrainBarServer.swift @@ -402,7 +402,8 @@ final class BrainBarServer: @unchecked Sendable { framed = data } else { // Newline-delimited JSON-RPC (Claude Code v2.1+ / MCP 2025-11-25) - guard let jsonData = try? MCPFraming.encodeJSONResponse(response) else { return false } + let rawResponse = Self.compactRawJSONResponseIfNeeded(response) + guard let jsonData = try? MCPFraming.encodeJSONResponse(rawResponse) else { return false } var data = jsonData data.append(0x0A) // trailing \n framed = data @@ -439,6 +440,27 @@ final class BrainBarServer: @unchecked Sendable { } } + private static func compactRawJSONResponseIfNeeded(_ response: [String: Any]) -> [String: Any] { + guard let result = response["result"] as? [String: Any], + let tools = result["tools"] as? [[String: Any]] else { + return response + } + + // Claude Desktop's MCPB utility process currently parses raw extension + // stdout in 8192-byte chunks. Raw newline transport omits optional tool + // annotations; Content-Length transport keeps the canonical tools/list. + var compactResult = result + compactResult["tools"] = tools.map { tool in + var compact = tool + compact.removeValue(forKey: "annotations") + return compact + } + + var compactResponse = response + compactResponse["result"] = compactResult + return compactResponse + } + private func disconnectClient(fd: Int32) { if let agentID = clients[fd]?.agentID { try? database?.markSubscriberDisconnected(agentID: agentID) diff --git a/brain-bar/Sources/BrainBar/MCPRouter.swift b/brain-bar/Sources/BrainBar/MCPRouter.swift index 689090b4..665cadea 100644 --- a/brain-bar/Sources/BrainBar/MCPRouter.swift +++ b/brain-bar/Sources/BrainBar/MCPRouter.swift @@ -119,7 +119,7 @@ final class MCPRouter: @unchecked Sendable { "jsonrpc": "2.0", "id": id, "result": [ - "tools": Self.compactToolDefinitions + "tools": Self.toolDefinitions ] ] } @@ -821,15 +821,6 @@ final class MCPRouter: @unchecked Sendable { idempotent: true ) - // Claude Desktop's MCPB utility process currently parses extension stdout - // in 8192-byte chunks. Keep the raw newline tools/list response below that - // boundary by omitting optional annotations from the public list response. - nonisolated(unsafe) static let compactToolDefinitions: [[String: Any]] = toolDefinitions.map { tool in - var compact = tool - compact.removeValue(forKey: "annotations") - return compact - } - nonisolated(unsafe) static let toolDefinitions: [[String: Any]] = [ [ "name": "brain_search", diff --git a/brain-bar/Tests/BrainBarTests/MCPRouterTests.swift b/brain-bar/Tests/BrainBarTests/MCPRouterTests.swift index 6c1f5dec..6032764c 100644 --- a/brain-bar/Tests/BrainBarTests/MCPRouterTests.swift +++ b/brain-bar/Tests/BrainBarTests/MCPRouterTests.swift @@ -158,7 +158,7 @@ final class MCPRouterTests: XCTestCase { ) } - func testEncodedToolsListFitsClaudeExtensionRawMessageLimit() throws { + func testToolsListPreservesCanonicalAnnotations() throws { let router = MCPRouter() let response = router.handle([ "jsonrpc": "2.0", @@ -166,18 +166,13 @@ final class MCPRouterTests: XCTestCase { "method": "tools/list", ]) - let body = try MCPFraming.encodeJSONResponse(response) let tools = (response["result"] as? [String: Any])?["tools"] as? [[String: Any]] ?? [] - XCTAssertLessThan( - body.count, - 8192, - "Claude Desktop's MCPB utility process parses raw extension stdout in 8192-byte chunks" - ) + XCTAssertEqual(tools.count, 16) for tool in tools { - XCTAssertNil( + XCTAssertNotNil( tool["annotations"], - "\(tool["name"] ?? "unknown") should omit optional annotations from tools/list to keep the raw MCPB response under 8 KiB" + "\(tool["name"] ?? "unknown") should keep annotations in the canonical tools/list response" ) } } @@ -201,8 +196,15 @@ final class MCPRouterTests: XCTestCase { } func testEachToolHasExpectedAnnotations() throws { + let router = MCPRouter() + let response = router.handle([ + "jsonrpc": "2.0", + "id": 3, + "method": "tools/list", + ]) + let tools = (response["result"] as? [String: Any])?["tools"] as? [[String: Any]] ?? [] let toolsByName = Dictionary( - uniqueKeysWithValues: MCPRouter.toolDefinitions.compactMap { tool -> (String, [String: Any])? in + uniqueKeysWithValues: tools.compactMap { tool -> (String, [String: Any])? in guard let name = tool["name"] as? String else { return nil } return (name, tool) } diff --git a/brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift b/brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift index 5fecd104..2a281752 100644 --- a/brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift +++ b/brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift @@ -94,16 +94,52 @@ final class SocketIntegrationTests: XCTestCase { XCTAssertEqual(tools?.count, 16) let encodedResponse = try MCPFraming.encodeJSONResponse(response) + XCTAssertGreaterThan(encodedResponse.count, 8192) + + for tool in tools ?? [] { + XCTAssertNotNil( + tool["annotations"], + "\(tool["name"] ?? "unknown") should keep annotations over framed socket transport" + ) + } + } + + func testRawLineToolsListCompactsForClaudeExtensionLimit() throws { + let fd = try connectClient() + defer { close(fd) } + + try sendRawLineJSON(on: fd, object: [ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": [ + "protocolVersion": "2024-11-05", + "capabilities": [:] as [String: Any], + "clientInfo": ["name": "claude-extension-test", "version": "1.0"] + ] + ]) + _ = try readRawLineJSONData(fd: fd) + + try sendRawLineJSON(on: fd, object: [ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/list", + ]) + + let line = try readRawLineJSONData(fd: fd) + let response = try JSONSerialization.jsonObject(with: line) as? [String: Any] ?? [:] + let tools = (response["result"] as? [String: Any])?["tools"] as? [[String: Any]] + XCTAssertLessThan( - encodedResponse.count, + line.count, 8192, - "Socket tools/list must stay below Claude Desktop MCPB's raw 8192-byte parse boundary" + "Claude Desktop's MCPB utility process parses raw extension stdout in 8192-byte chunks" ) - + XCTAssertEqual(tools?.count, 16) for tool in tools ?? [] { XCTAssertNil( tool["annotations"], - "\(tool["name"] ?? "unknown") should omit optional annotations over socket transport" + "\(tool["name"] ?? "unknown") should omit optional annotations over raw newline transport" ) } } @@ -761,6 +797,40 @@ final class SocketIntegrationTests: XCTestCase { } } + private func sendRawLineJSON(on fd: Int32, object: [String: Any]) throws { + var data = try JSONSerialization.data(withJSONObject: object) + data.append(0x0A) + let sent = data.withUnsafeBytes { ptr in + write(fd, ptr.baseAddress!, data.count) + } + guard sent == data.count else { + throw NSError(domain: "test", code: 3, userInfo: [NSLocalizedDescriptionKey: "raw write() incomplete"]) + } + } + + private func readRawLineJSONData(fd: Int32, timeout: TimeInterval = 5.0) throws -> Data { + var buffer = Data() + var readBuf = [UInt8](repeating: 0, count: 65536) + let deadline = Date().addingTimeInterval(timeout) + + while Date() < deadline { + let n = read(fd, &readBuf, readBuf.count) + if n > 0 { + buffer.append(contentsOf: readBuf[0.. [String: Any] { return try readMCPMessages(fd: fd, expectedCount: 1, timeout: timeout).first ?? [:] } From 99371eb4458b2a6cc3941ed2b3f945bdf9a6361e Mon Sep 17 00:00:00 2001 From: Test User Date: Sun, 3 May 2026 12:50:12 +0300 Subject: [PATCH 3/3] fix(brainbar): preserve small raw tool annotations --- .../Sources/BrainBar/BrainBarServer.swift | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/brain-bar/Sources/BrainBar/BrainBarServer.swift b/brain-bar/Sources/BrainBar/BrainBarServer.swift index 5a7496a8..a547d19e 100644 --- a/brain-bar/Sources/BrainBar/BrainBarServer.swift +++ b/brain-bar/Sources/BrainBar/BrainBarServer.swift @@ -402,8 +402,7 @@ final class BrainBarServer: @unchecked Sendable { framed = data } else { // Newline-delimited JSON-RPC (Claude Code v2.1+ / MCP 2025-11-25) - let rawResponse = Self.compactRawJSONResponseIfNeeded(response) - guard let jsonData = try? MCPFraming.encodeJSONResponse(rawResponse) else { return false } + guard let jsonData = Self.encodeRawJSONResponse(response) else { return false } var data = jsonData data.append(0x0A) // trailing \n framed = data @@ -440,10 +439,22 @@ final class BrainBarServer: @unchecked Sendable { } } - private static func compactRawJSONResponseIfNeeded(_ response: [String: Any]) -> [String: Any] { + private static let claudeExtensionRawChunkLimit = 8192 + + private static func encodeRawJSONResponse(_ response: [String: Any]) -> Data? { + guard let jsonData = try? MCPFraming.encodeJSONResponse(response) else { return nil } + guard jsonData.count >= claudeExtensionRawChunkLimit else { return jsonData } + guard let compacted = compactRawJSONResponseIfNeeded(response), + let compactData = try? MCPFraming.encodeJSONResponse(compacted) else { + return jsonData + } + return compactData + } + + private static func compactRawJSONResponseIfNeeded(_ response: [String: Any]) -> [String: Any]? { guard let result = response["result"] as? [String: Any], let tools = result["tools"] as? [[String: Any]] else { - return response + return nil } // Claude Desktop's MCPB utility process currently parses raw extension