Skip to content

fix: migrate MCP server to SDK v2 - #636

Merged
EtanHey merged 2 commits into
mainfrom
fix/mcp-sdk-api-drift
Aug 1, 2026
Merged

fix: migrate MCP server to SDK v2#636
EtanHey merged 2 commits into
mainfrom
fix/mcp-sdk-api-drift

Conversation

@EtanHey

@EtanHey EtanHey commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • migrate BrainLayer's low-level MCP server from removed SDK v1 decorators to MCP v2 on_* callbacks
  • preserve v1 input/output validation and result normalization explicitly, including (content, structured) tool results
  • migrate the in-memory mock/client harness and MCP model field access to the v2 API
  • declare mcp>=2.0.0,<3.0.0 and explicit jsonschema validation support

This is option (a) from the task brief: the migration was bounded, so the repository now follows the current SDK instead of retaining the emergency <2 pin.

Reproduction and collection evidence

Before, in an isolated environment resolving mcp==2.0.0:

$ python -m pytest tests/test_3tool_aliases.py --collect-only -q
ERROR collecting tests/test_3tool_aliases.py
AttributeError: 'Server' object has no attribute 'list_tools'
0 tests collected, 1 error

$ python -m pytest tests/ --collect-only -q
3562 tests collected, 12 errors

After:

$ python -m pytest tests/test_3tool_aliases.py --collect-only -q
38 tests collected in 1.11s

$ python -m pytest <the 12 previously failing files> --collect-only -q
230 tests collected in 1.05s

Test API rewrites

Two tests required API-surface rewrites without changing their behavioral expectations:

  • test_mcp_input_schema_limits.py now calls the server through MCP v2's supported in-memory Client instead of the removed request_handlers implementation detail; it also covers combined-content normalization through a real client.
  • test_mcp_labeled_field_output.py checks the proprietary result-size hint in Tool._meta, because MCP v2 intentionally drops unknown ToolAnnotations fields.

All other test edits are canonical v2 model attribute renames (input_schema, is_error, structured_content, and annotation hint names).

Verification

3666 passed, 9 skipped, 77 deselected, 1 xfailed in 503.28s
40 passed (CI-isolated test files)
101 passed, 1 xfailed (MCP-focused post-review regression set)
ruff check: All checks passed!
ruff format --check: 417 files already formatted
git diff --check: clean

The local CodeRabbit CLI remained in reviewing beyond its bounded three-minute gate and was stopped; reviewer assignment is intentionally left to the lead.


Note

Medium Risk
Touches the primary Claude Code integration surface and a major MCP dependency bump; new schema validation can reject inputs that previously slipped through, though behavior is largely preserved via normalization and broad test updates.

Overview
Migrates the BrainLayer MCP server from SDK v1 decorators to the v2 low-level Server API (on_list_tools, on_call_tool, on_completion) and bumps mcp to >=2.0.0,<3.0.0 with explicit jsonschema for tool I/O checks.

The v2 call path adds _handle_call_tool_request: validate arguments against each tool’s input_schema, route through existing call_tool, normalize legacy return shapes via _normalize_call_tool_result (tuples, dicts, iterables → CallToolResult), and validate structured_content when output_schema is set. Errors use is_error=True instead of isError.

Model/API renames are applied repo-wide: input_schema, output_schema, structured_content, read_only_hint, etc. Read tools move anthropic/maxResultSizeChars from ToolAnnotations to Tool.meta.

Tests and harness: tests/mock_mcp/base.py uses in-memory Client(..., mode="legacy") instead of create_connected_server_and_client_session; schema/validation tests call the real server through that client rather than request_handlers.

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

Note

Migrate MCP server to SDK v2 with snake_case field names and low-level handler API

  • Bumps the mcp dependency to >=2.0.0,<3.0.0 in pyproject.toml and adds jsonschema>=4.20.0 for input/output schema validation.
  • Replaces decorator-based tool registration with MCP v2 low-level handlers (on_list_tools, on_call_tool, on_completion) in src/brainlayer/mcp/init.py.
  • Renames all camelCase SDK fields to snake_case throughout: inputSchemainput_schema, isErroris_error, structuredContentstructured_content, readOnlyHintread_only_hint, etc.
  • Adds _normalize_call_tool_result to coerce varied tool return types into a consistent CallToolResult, and validates arguments against declared input_schema using jsonschema before dispatch.
  • Updates all tests and the mock MCP server in tests/mock_mcp/base.py to use the v2 Client instead of ClientSession.
  • Risk: any external code or client calling these tools that relied on camelCase field names will break.

Macroscope summarized 7fbc9fe.

Summary by CodeRabbit

  • New Features

    • Added compatibility with the latest MCP protocol.
    • Tool definitions now provide standardized input and output schemas.
    • Responses support structured content and result-size metadata.
    • Added validation for tool inputs and outputs, with clearer error reporting.
  • Bug Fixes

    • Updated error handling and response processing for reliable tool execution.
    • Preserved text and structured results across MCP operations.
  • Tests

    • Expanded coverage for schema validation, error responses, annotations, and structured outputs.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your trial has ended. Reactivate Greptile to resume code reviews.

@cursor

cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_b8324236-9b49-4591-9cbc-f78d28c47447)

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The MCP dependency moves to v2. The server adopts low-level v2 callbacks, typed requests, schema validation, snake_case fields, structured results, and metadata. The mock transport and MCP tests update to the new client, schema, annotation, and error APIs.

Changes

MCP v2 server migration

Layer / File(s) Summary
MCP v2 handler adapters
pyproject.toml, src/brainlayer/mcp/__init__.py, src/brainlayer/mcp/_shared.py, src/brainlayer/brainbar_hybrid_helper.py, scripts/smoke/*
The project uses MCP 2.x and jsonschema. The server uses typed callbacks, validates tool inputs and outputs, normalizes results, and emits v2 response fields.
Tool schema and metadata migration
src/brainlayer/mcp/__init__.py, src/brainlayer/mcp/palette.py
Tool definitions use input_schema, output_schema, meta, and snake_case annotations. Read tools expose result-size metadata.
Mock client and server transport
tests/mock_mcp/base.py, tests/test_mcp_input_schema_limits.py
The mock server uses MCP v2 callbacks and the Client transport. Tests verify client-side validation and preservation of text and structured results.
MCP v2 test compatibility
tests/test_3tool_aliases.py, tests/test_brainstore.py, tests/test_search_validation.py, tests/test_tool_annotations.py, tests/test_write_queue.py, tests/test_*
MCP tests use input_schema, is_error, structured_content, read_only_hint, and related v2 fields.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Poem

A rabbit checks each schema line,
With snake_case fields in neat design.
Errors now wear v2 ears,
Structured results calm old fears.
The MCP hops onward, bright and clean.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.14% 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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: migrating the MCP server to SDK v2.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mcp-sdk-api-drift

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.

Comment thread src/brainlayer/mcp/__init__.py Outdated
@cursor

cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_07c6e0c7-6483-4730-9069-ca4b00750de0)

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your trial has ended. Reactivate Greptile to resume code reviews.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pyproject.toml`:
- Around line 19-20: Update the jsonschema dependency specification in
pyproject.toml to add an upper bound excluding the next major version, matching
the existing mcp dependency constraint while preserving the current minimum
version.

In `@src/brainlayer/mcp/__init__.py`:
- Around line 153-154: Update the exception handler around _error_result to log
the caught exception with the module’s existing logger before returning the
client-facing error result, preserving the current str(exc) response behavior.
- Around line 115-129: Update _normalize_call_tool_result to reject bare strings
and tuples whose length is not exactly two, rather than routing them through the
iterable fallback. Restrict the iterable branch to supported iterable content
forms, preserving valid two-item tuple, dict, and other accepted collection
handling, and raise TypeError for unsupported values.
- Around line 132-144: Cache the static tool definitions used by
_handle_call_tool_request instead of awaiting list_tools() for every call. Apply
a single-entry cache to _full_tool_definitions() or the corresponding
definition-site function, while preserving ToolPalette.expose()’s dynamic
session filtering and existing schema validation behavior.
🪄 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 Plus

Run ID: aa9edbb1-d682-4c50-a524-228b741a18ab

📥 Commits

Reviewing files that changed from the base of the PR and between d8c1590 and 8c42619.

📒 Files selected for processing (33)
  • pyproject.toml
  • scripts/smoke/repogolem_mcp_transport_smoke.py
  • src/brainlayer/brainbar_hybrid_helper.py
  • src/brainlayer/mcp/__init__.py
  • src/brainlayer/mcp/_shared.py
  • src/brainlayer/mcp/palette.py
  • tests/mock_mcp/base.py
  • tests/test_3tool_aliases.py
  • tests/test_audit_search_quality.py
  • tests/test_brainbar_hybrid_helper.py
  • tests/test_brainstore.py
  • tests/test_chunk_lifecycle.py
  • tests/test_enrichment_controller.py
  • tests/test_entity_type_sync.py
  • tests/test_issue_type.py
  • tests/test_mcp_digest_modes.py
  • tests/test_mcp_input_schema_limits.py
  • tests/test_mcp_labeled_field_output.py
  • tests/test_mcp_palette.py
  • tests/test_mcp_timeout.py
  • tests/test_mcp_warm_route.py
  • tests/test_phase3_digest.py
  • tests/test_phase5.py
  • tests/test_phase6_sentiment.py
  • tests/test_precompact_chunk_origin.py
  • tests/test_search_fanout.py
  • tests/test_search_filter_params.py
  • tests/test_search_validation.py
  • tests/test_smart_search_entity_dedup.py
  • tests/test_store_handler.py
  • tests/test_think_recall_integration.py
  • tests/test_tool_annotations.py
  • tests/test_write_queue.py
📜 Review details
⏰ Context from checks skipped due to timeout. (3)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.11)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Flag risky DB or concurrency changes explicitly and do not hand-wave lock behavior
Enforce one-write-at-a-time concurrency constraint; reads are safe but brain_digest is write-heavy and must not run in parallel with other MCP work
Run pytest before claiming behavior changed safely; current test suite has 929 tests

Files:

  • src/brainlayer/mcp/_shared.py
  • tests/test_think_recall_integration.py
  • scripts/smoke/repogolem_mcp_transport_smoke.py
  • tests/test_phase3_digest.py
  • tests/test_brainbar_hybrid_helper.py
  • tests/test_phase6_sentiment.py
  • tests/test_mcp_warm_route.py
  • tests/test_audit_search_quality.py
  • tests/test_tool_annotations.py
  • tests/test_phase5.py
  • tests/test_store_handler.py
  • tests/test_issue_type.py
  • src/brainlayer/brainbar_hybrid_helper.py
  • tests/test_mcp_labeled_field_output.py
  • tests/test_search_validation.py
  • tests/test_search_fanout.py
  • tests/test_chunk_lifecycle.py
  • tests/test_write_queue.py
  • tests/test_precompact_chunk_origin.py
  • src/brainlayer/mcp/palette.py
  • tests/test_smart_search_entity_dedup.py
  • tests/test_entity_type_sync.py
  • tests/test_search_filter_params.py
  • tests/test_enrichment_controller.py
  • tests/test_mcp_palette.py
  • tests/test_brainstore.py
  • tests/test_mcp_digest_modes.py
  • tests/mock_mcp/base.py
  • tests/test_mcp_input_schema_limits.py
  • tests/test_mcp_timeout.py
  • tests/test_3tool_aliases.py
  • src/brainlayer/mcp/__init__.py
src/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.py: Use paths.py:get_db_path() for all database path resolution; do not hard-code the BrainLayer database path.
Each worker must use its own database connection and retry on SQLITE_BUSY.
For bulk database operations, stop enrichment workers, checkpoint WAL before and after, drop and recreate FTS triggers around large deletes, batch deletes in 5–10K chunks, and checkpoint every three batches.
Use the canonical BrainLayer naming: BrainLayer (זיכרון) means “memory.”

Files:

  • src/brainlayer/mcp/_shared.py
  • src/brainlayer/brainbar_hybrid_helper.py
  • src/brainlayer/mcp/palette.py
  • src/brainlayer/mcp/__init__.py
src/brainlayer/mcp/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/brainlayer/mcp/**/*.py: Use the fixed-size readonly WAL VectorStore pool; enforce BRAINLAYER_READ_POOL_SIZE, BRAINLAYER_READ_BUSY_TIMEOUT_MS, and the startup memory limit.
Add new MCP tools under the mcp/ package and expose them through the brainlayer-mcp entrypoint.

Files:

  • src/brainlayer/mcp/_shared.py
  • src/brainlayer/mcp/palette.py
  • src/brainlayer/mcp/__init__.py
src/brainlayer/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/brainlayer/**/*.py: Preserve ai_code, stack_trace, and user_message verbatim; skip noise, summarize build logs, and retain only structure for directory listings.
Use AST-aware tree-sitter chunking, never split stack traces, and mask large tool output.
Keep Groq as the primary enrichment backend, Gemini as fallback, and Ollama as the offline last resort; honor BRAINLAYER_ENRICH_BACKEND and BRAINLAYER_ENRICH_RATE.
Default searches must exclude lifecycle-managed chunks; include_archived=True may expose history. brain_supersede must apply its personal-data safety gate, and brain_archive must soft-delete with a timestamp.

Files:

  • src/brainlayer/mcp/_shared.py
  • src/brainlayer/brainbar_hybrid_helper.py
  • src/brainlayer/mcp/palette.py
  • src/brainlayer/mcp/__init__.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Tests must not refresh the production backup heartbeat log; use BRAINLAYER_BACKUP_LOG_PATH and set provenance to pytest.

Files:

  • tests/test_think_recall_integration.py
  • tests/test_phase3_digest.py
  • tests/test_brainbar_hybrid_helper.py
  • tests/test_phase6_sentiment.py
  • tests/test_mcp_warm_route.py
  • tests/test_audit_search_quality.py
  • tests/test_tool_annotations.py
  • tests/test_phase5.py
  • tests/test_store_handler.py
  • tests/test_issue_type.py
  • tests/test_mcp_labeled_field_output.py
  • tests/test_search_validation.py
  • tests/test_search_fanout.py
  • tests/test_chunk_lifecycle.py
  • tests/test_write_queue.py
  • tests/test_precompact_chunk_origin.py
  • tests/test_smart_search_entity_dedup.py
  • tests/test_entity_type_sync.py
  • tests/test_search_filter_params.py
  • tests/test_enrichment_controller.py
  • tests/test_mcp_palette.py
  • tests/test_brainstore.py
  • tests/test_mcp_digest_modes.py
  • tests/mock_mcp/base.py
  • tests/test_mcp_input_schema_limits.py
  • tests/test_mcp_timeout.py
  • tests/test_3tool_aliases.py
🪛 ast-grep (0.45.0)
tests/mock_mcp/base.py

[info] 64-64: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"mock": True, "tool": params.name})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 67-67: use jsonify instead of json.dumps for JSON output
Context: json.dumps(result)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

src/brainlayer/mcp/__init__.py

[info] 121-121: use jsonify instead of json.dumps for JSON output
Context: json.dumps(result, indent=2)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🔇 Additional comments (38)
tests/mock_mcp/base.py (4)

17-19: LGTM!

Also applies to: 35-36


52-73: LGTM!


100-100: LGTM!


145-148: LGTM!

tests/test_mcp_input_schema_limits.py (3)

6-7: LGTM!

Also applies to: 52-52


62-75: LGTM!


77-95: LGTM!

tests/test_3tool_aliases.py (1)

151-151: LGTM!

Also applies to: 197-197, 327-327, 336-336, 345-345, 453-453, 468-476, 492-504

tests/test_mcp_palette.py (1)

45-50: LGTM!

Also applies to: 61-61

tests/test_mcp_timeout.py (1)

16-16: LGTM!

tests/test_mcp_warm_route.py (1)

272-272: LGTM!

tests/test_phase3_digest.py (1)

305-305: LGTM!

Also applies to: 349-349

tests/test_phase5.py (1)

305-305: LGTM!

Also applies to: 331-331

tests/test_phase6_sentiment.py (1)

195-195: LGTM!

tests/test_precompact_chunk_origin.py (1)

946-946: LGTM!

Also applies to: 986-986

tests/test_search_fanout.py (1)

271-271: LGTM!

Also applies to: 299-299

tests/test_audit_search_quality.py (1)

116-116: LGTM!

Also applies to: 129-129

tests/test_brainbar_hybrid_helper.py (1)

183-183: LGTM!

tests/test_brainstore.py (1)

587-587: LGTM!

Also applies to: 597-597

tests/test_chunk_lifecycle.py (1)

267-275: LGTM!

Also applies to: 314-314

tests/test_enrichment_controller.py (1)

1590-1590: LGTM!

Also applies to: 1606-1606

tests/test_think_recall_integration.py (1)

278-278: LGTM!

tests/test_tool_annotations.py (1)

91-121: LGTM!

Also applies to: 136-136, 146-146

tests/test_write_queue.py (1)

1481-1481: LGTM!

Also applies to: 2474-2474

tests/test_entity_type_sync.py (1)

78-86: LGTM!

Also applies to: 97-104, 119-119

tests/test_issue_type.py (1)

166-186: LGTM!

tests/test_mcp_digest_modes.py (1)

77-77: LGTM!

Also applies to: 86-86

tests/test_mcp_labeled_field_output.py (1)

272-273: LGTM!

tests/test_search_filter_params.py (1)

532-532: LGTM!

Also applies to: 810-810

tests/test_search_validation.py (1)

51-51: LGTM!

Also applies to: 66-66, 81-81, 134-134, 150-150

tests/test_smart_search_entity_dedup.py (1)

172-175: LGTM!

Also applies to: 231-231, 383-383, 393-393

tests/test_store_handler.py (1)

179-179: LGTM!

src/brainlayer/mcp/__init__.py (2)

176-217: LGTM!

Also applies to: 469-470, 652-653, 678-783, 812-813, 996-1040, 1110-1251, 1273-1273, 1326-1328, 1334-1354, 1394-1403, 1568-1628


145-151: 🎯 Functional Correctness

Output-schema validation already skips error results.

src/brainlayer/mcp/_shared.py (1)

325-327: LGTM!

scripts/smoke/repogolem_mcp_transport_smoke.py (1)

116-118: LGTM!

src/brainlayer/brainbar_hybrid_helper.py (1)

191-196: LGTM!

src/brainlayer/mcp/palette.py (1)

46-52: LGTM!

Comment thread pyproject.toml
Comment on lines +19 to +20
"mcp>=2.0.0,<3.0.0",
"jsonschema>=4.20.0", # MCP v2 low-level handlers require explicit schema validation

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider pinning an upper bound for jsonschema.

The mcp dependency uses <3.0.0 to guard against a future breaking major release. jsonschema>=4.20.0 has no upper bound. Add a similar cap for consistency and to avoid an untested future major version breaking schema validation.

♻️ Proposed pin
-    "jsonschema>=4.20.0",  # MCP v2 low-level handlers require explicit schema validation
+    "jsonschema>=4.20.0,<5.0.0",  # MCP v2 low-level handlers require explicit schema validation
📝 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
"mcp>=2.0.0,<3.0.0",
"jsonschema>=4.20.0", # MCP v2 low-level handlers require explicit schema validation
"mcp>=2.0.0,<3.0.0",
"jsonschema>=4.20.0,<5.0.0", # MCP v2 low-level handlers require explicit schema validation
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` around lines 19 - 20, Update the jsonschema dependency
specification in pyproject.toml to add an upper bound excluding the next major
version, matching the existing mcp dependency constraint while preserving the
current minimum version.

Comment on lines +115 to +129
def _normalize_call_tool_result(result: Any) -> CallToolResult:
"""Preserve the result forms accepted by the MCP v1 call-tool decorator."""
if isinstance(result, CallToolResult):
return result
if isinstance(result, tuple) and len(result) == 2:
content, structured_content = result
elif isinstance(result, dict):
content = [TextContent(type="text", text=json.dumps(result, indent=2))]
structured_content = result
elif hasattr(result, "__iter__"):
content = result
structured_content = None
else:
raise TypeError(f"Unexpected return type from tool: {type(result).__name__}")
return CallToolResult(content=list(content), structured_content=structured_content, is_error=False)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Tighten the fallback branch in _normalize_call_tool_result.

elif hasattr(result, "__iter__") also matches plain strings and tuples of any length other than 2. A bare string return gets converted to list(content), which splits it into one content item per character instead of raising TypeError. A tuple with 1 or 3+ elements is silently treated as a content list rather than being rejected. The docstring states this function should "preserve the result forms accepted by the MCP v1 call-tool decorator," which did not include bare strings or mismatched tuples.

🐛 Proposed fix
     elif hasattr(result, "__iter__"):
+    elif isinstance(result, list):
         content = result
         structured_content = None
🧰 Tools
🪛 ast-grep (0.45.0)

[info] 121-121: use jsonify instead of json.dumps for JSON output
Context: json.dumps(result, indent=2)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/brainlayer/mcp/__init__.py` around lines 115 - 129, Update
_normalize_call_tool_result to reject bare strings and tuples whose length is
not exactly two, rather than routing them through the iterable fallback.
Restrict the iterable branch to supported iterable content forms, preserving
valid two-item tuple, dict, and other accepted collection handling, and raise
TypeError for unsupported values.

Comment on lines +132 to +144
async def _handle_call_tool_request(_ctx: Any, params: CallToolRequestParams) -> CallToolResult:
"""Validate and route a typed MCP v2 tool call while preserving v1 behavior."""
arguments = params.arguments or {}
try:
tool = next((candidate for candidate in await list_tools() if candidate.name == params.name), None)
if tool is not None:
try:
jsonschema.validate(instance=arguments, schema=tool.input_schema)
except jsonschema.ValidationError as exc:
return _error_result(f"Input validation error: {exc.message}")

result = _normalize_call_tool_result(await call_tool(params.name, arguments))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Cache the static tool definitions to avoid rebuilding them on every tool call.

await list_tools() here rebuilds the full 13-tool inventory (_full_tool_definitions()), including the large brain_search/brain_recall schemas, on every call_tool invocation solely to find one tool's schema for validation. Since _full_tool_definitions() has no external state dependency, cache it once instead of reconstructing it per request.

♻️ Proposed fix
+import functools
+
 def _full_tool_definitions() -> list[Tool]:
     """Build the complete canonical Python MCP tool inventory."""
+
+
+_full_tool_definitions = functools.lru_cache(maxsize=1)(_full_tool_definitions)

Or decorate the function directly with @functools.lru_cache(maxsize=1) at its definition site (line 461). ToolPalette.expose() still filters the cached list dynamically per session, so palette expansion state is unaffected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/brainlayer/mcp/__init__.py` around lines 132 - 144, Cache the static tool
definitions used by _handle_call_tool_request instead of awaiting list_tools()
for every call. Apply a single-entry cache to _full_tool_definitions() or the
corresponding definition-site function, while preserving ToolPalette.expose()’s
dynamic session filtering and existing schema validation behavior.

Comment on lines +153 to +154
except Exception as exc:
return _error_result(str(exc))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log the exception before converting it to an error result.

except Exception as exc: return _error_result(str(exc)) silently swallows the traceback. logger is already defined in this module (Line 12) but unused here. Without a log entry, diagnosing an unexpected tool failure in production requires reproducing it from the client-visible error string alone.

♻️ Proposed fix
     except Exception as exc:
+        logger.exception("MCP call_tool failed for %s", params.name)
         return _error_result(str(exc))
📝 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
except Exception as exc:
return _error_result(str(exc))
except Exception as exc:
logger.exception("MCP call_tool failed for %s", params.name)
return _error_result(str(exc))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/brainlayer/mcp/__init__.py` around lines 153 - 154, Update the exception
handler around _error_result to log the caught exception with the module’s
existing logger before returning the client-facing error result, preserving the
current str(exc) response behavior.

@EtanHey
EtanHey merged commit 4038048 into main Aug 1, 2026
6 of 9 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