Skip to content
Open
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
9 changes: 7 additions & 2 deletions brain-bar/Sources/BrainBar/MCPRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -395,11 +395,16 @@ final class MCPRouter: @unchecked Sendable {
return toolCallResult(id: id, output: expandPalette(session: session))
}

// Check tool exists
guard isToolExposed(toolName, session: session) else {
guard Self.toolDefinitions.contains(where: { ($0["name"] as? String) == toolName }) else {
return jsonRPCError(id: id, code: -32601, message: "Unknown tool: \(toolName)")
}

guard isToolExposed(toolName, session: session) else {
let message = "Tool '\(toolName)' is gated by the core MCP profile. "
+ "Call expand_palette, or set BRAINLAYER_MCP_PROFILE=full on the MCP server."
return jsonRPCError(id: id, code: -32601, message: message)
}

// Dispatch to handler
do {
try Self.validate(arguments: arguments, for: toolName)
Expand Down
14 changes: 14 additions & 0 deletions brain-bar/Tests/BrainBarTests/MCPRouterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,19 @@ final class MCPRouterTests: XCTestCase {
}
}

func testCoreProfileExplainsGateAndBothRemedies() throws {
let router = MCPRouter(profile: "core")
let response = router.handle(toolCall(id: 19, name: "brain_digest", arguments: [:]))
let error = try XCTUnwrap(response["error"] as? [String: Any])

XCTAssertEqual(error["code"] as? Int, -32601)
XCTAssertEqual(
error["message"] as? String,
"Tool 'brain_digest' is gated by the core MCP profile. "
+ "Call expand_palette, or set BRAINLAYER_MCP_PROFILE=full on the MCP server."
)
}

func testEnvironmentProfileControlsDefaultRouter() throws {
let environmentKey = "BRAINLAYER_MCP_PROFILE"
let originalProfile = ProcessInfo.processInfo.environment[environmentKey]
Expand Down Expand Up @@ -1038,6 +1051,7 @@ No results found.

XCTAssertNotNil(error, "Unknown tool should return JSON-RPC error")
XCTAssertEqual(error?["code"] as? Int, -32601, "Should be method-not-found error")
XCTAssertEqual(error?["message"] as? String, "Unknown tool: nonexistent_tool")
}

func testBrainDigestRejectsOversizedContentAtSchemaLayer() throws {
Expand Down
5 changes: 4 additions & 1 deletion src/brainlayer/mcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1340,7 +1340,10 @@ async def call_tool(name: str, arguments: dict[str, Any]):
)

if name in _FULL_TOOL_NAMES and not _tool_palette.is_exposed(name):
return _error_result(f"Tool not exposed in the core profile: {name}. Call {EXPAND_TOOL_NAME} first.")
return _error_result(
f"Tool '{name}' is gated by the core MCP profile. "
f"Call {EXPAND_TOOL_NAME}, or set BRAINLAYER_MCP_PROFILE=full on the MCP server."
)

# --- New consolidated tools ---

Expand Down
25 changes: 24 additions & 1 deletion tests/test_mcp_palette.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,29 @@ def test_environment_profile_is_resolved_once(monkeypatch):
assert len(palette.expose(_full_tool_definitions())) == 13


def test_python_core_profile_explains_gate_and_both_remedies(monkeypatch):
palette = ToolPalette("core")
monkeypatch.setattr(mcp_module, "_tool_palette", palette)

result = asyncio.run(call_tool("brain_digest", {}))

assert result.isError is True
assert result.content[0].text == (
"Tool 'brain_digest' is gated by the core MCP profile. "
"Call expand_palette, or set BRAINLAYER_MCP_PROFILE=full on the MCP server."
)


def test_python_unknown_tool_remains_unknown(monkeypatch):
palette = ToolPalette("core")
monkeypatch.setattr(mcp_module, "_tool_palette", palette)

result = asyncio.run(call_tool("nonexistent_tool", {}))

assert result.isError is True
assert result.content[0].text == "Unknown tool: nonexistent_tool"


def test_python_palette_expands_once_and_dispatches_deferred_tools(monkeypatch):
palette = ToolPalette("core")
monkeypatch.setattr(mcp_module, "_tool_palette", palette)
Expand All @@ -43,7 +66,7 @@ def test_python_palette_expands_once_and_dispatches_deferred_tools(monkeypatch):

before = asyncio.run(call_tool("brain_tags", {}))
assert before.isError is True
assert "not exposed" in before.content[0].text
assert "gated by the core MCP profile" in before.content[0].text

first = asyncio.run(call_tool("expand_palette", {}))
assert first.isError is False
Expand Down
Loading