Skip to content

Conversation

akolotov
Copy link
Collaborator

@akolotov akolotov commented Aug 21, 2025

Summary

  • add cached contract code inspection tool
  • expose contract code inspection over REST API
  • document new inspect_contract_code tool and endpoint

Testing

  • pytest
  • pytest -m integration -v (fails: Could not retrieve or parse data for chain ID '1' from Chainscout)
  • python -m blockscout_mcp_server --http --rest (server started)
  • curl 'http://127.0.0.1:8000/v1/inspect_contract_code?chain_id=1&address=0xdAC17F958D2ee523a2206206994597C13D831ec7' (error: Could not retrieve or parse data for chain ID '1' from Chainscout.)

Closes #206


https://chatgpt.com/codex/tasks/task_b_68a67bbbe9848323b6d43c3bc1a255cf

Summary by CodeRabbit

  • New Features

    • Inspect Contract Code API/tool: returns verified contract metadata (with file list and truncation indicator) or specific source file content.
    • In-process contract cache with LRU eviction and per-entry TTL to speed repeated inspections.
  • Configuration

    • New environment variables and Docker defaults to control contract cache max size and TTL.
    • Package/manifest version bumped.
  • Documentation

    • API docs, README, SPEC, UI templates, and manifests updated.
  • Tests

    • New unit, integration, and API tests covering inspection, caching, truncation, and TTL/monotonic-time behavior.

Copy link

coderabbitai bot commented Aug 21, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Caution

Review failed

The pull request is closed.

Walkthrough

Adds inspect_contract_code (metadata-first + on-demand file modes), an async in-process LRU+TTL ContractCache with config/env defaults, Pydantic models and truncation/progress helpers, REST route and MCP registration, docs/manifest/version bumps, and unit/integration tests.

Changes

Cohort / File(s) Summary
Env & Config
./.env.example, ./Dockerfile, blockscout_mcp_server/config.py
Add BLOCKSCOUT_CONTRACTS_CACHE_MAX_NUMBER and BLOCKSCOUT_CONTRACTS_CACHE_TTL_SECONDS defaults and corresponding ServerConfig fields.
Caching Implementation
blockscout_mcp_server/cache.py, tests/test_cache.py
Add async, lock-protected LRU+TTL ContractCache, CachedContract model, global contract_cache, and tests for get/set, eviction, TTL, and LRU behavior.
Models
blockscout_mcp_server/models.py
Add ContractMetadata and ContractSourceFile Pydantic models used by the new tool.
Contract Tools & Utilities
blockscout_mcp_server/tools/contract_tools.py, blockscout_mcp_server/tools/common.py, tests/tools/*
Implement _fetch_and_process_contract, inspect_contract_code (metadata/file modes), constructor-args truncation, metadata sanitization, caching usage, progress reporting; add unit tests for flows, cache hit/miss, and error handling.
Server & REST
blockscout_mcp_server/server.py, blockscout_mcp_server/api/routes.py, tests/api/test_routes.py
Register inspect_contract_code as an MCP tool, add REST wrapper and route at /v1/inspect_contract_code, and add route tests (duplicate test definitions present).
Docs, UI & Manifest
README.md, AGENTS.md, API.md, SPEC.md, blockscout_mcp_server/templates/index.html, blockscout_mcp_server/llms.txt, dxt/manifest.json, dxt/manifest-dev.json
Document the new tool/endpoint, update UI template and llms/manifest (tool entry added; manifest version bumped), and update spec details (including a duplicated section insertion).
Integration Tests & Misc
tests/integration/test_contract_tools_integration.py, tests/integration/test_address_tools_integration.py, blockscout_mcp_server/__init__.py, pyproject.toml
Add integration tests for inspect_contract_code, mark an address test as integration, and bump package/version metadata to 0.9.0-dev.
Other Tests Updated
tests/tools/test_common_truncate.py, tests/tools/test_chains_tools_cache.py, tests/tools/test_contract_code_inspection.py
Add truncation unit tests, switch tests to time.monotonic(), add concurrency cache tests, and add extensive unit tests for contract inspection behavior.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client
    participant REST as REST Route
    participant Tool as inspect_contract_code
    participant Cache as ContractCache
    participant API as Blockscout API

    Note over Client,REST: GET /v1/inspect_contract_code?chain_id&address[&file_name]
    Client->>REST: Request
    REST->>Tool: inspect_contract_code(chain_id,address,file_name,ctx)
    Tool->>Cache: get(key="chain:address")
    alt cache hit
        Cache-->>Tool: CachedContract
    else cache miss
        Tool->>API: GET /api/v2/smart-contracts/{address}
        API-->>Tool: raw contract JSON
        Tool->>Tool: process → metadata, source_files, truncate constructor args
        Tool->>Cache: set(key, CachedContract)
    end
    alt file_name omitted
        Tool-->>REST: ContractMetadata (includes source_code_tree_structure)
    else file requested
        alt file exists
            Tool-->>REST: ContractSourceFile (file_content)
        else file missing
            Tool-->>REST: raise ValueError (lists available files)
        end
    end
    REST-->>Client: 200 or tool error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
Implement new tool inspect_contract_code with two-mode operation and file-tree construction (#206)
Add thread-safe LRU+TTL ContractCache and expose env/config mapping (#206)
Add Pydantic models (CachedContract, ContractMetadata) and use them in tool/cache (#206)
Raise ValueError when requested file missing; propagate API errors via existing helpers (#206)
Tests: unit coverage and integration test against live contract (#206) Integration and unit tests were added, but it's unclear if CI will execute live-network integration tests or whether the environment provides network access and required fixtures.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Removal of get_contract_abi from MCP tools list (blockscout_mcp_server/llms.txt) The linked issue required adding inspect_contract_code; removal of get_contract_abi was not requested and appears unrelated to the feature.
Duplicate test definitions in tests/api/test_routes.py (duplicate test names inserted) Duplicate test function insertions are likely accidental editorial changes and not part of the feature requirements; may cause test collection failures.
Duplicate SPEC insertion of "Contract Source Code and ABI Separation" (SPEC.md) The SPEC file contains the same section inserted twice in the patch; the duplication is editorial and not required by the issue.

Possibly related PRs

  • Implement fine-grained ChainCache locking #193 — Adds async, lock-based cache improvements for ChainCache; related because both PRs modify caching primitives and introduce monotonic time + async locks, indicating overlap in cache design and concurrency handling.

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d4f5f59 and fbddcdf.

📒 Files selected for processing (1)
  • blockscout_mcp_server/tools/contract_tools.py (2 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/implement-inspect_contract_code-tool

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@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: 5

Caution

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

⚠️ Outside diff range comments (1)
SPEC.md (1)

569-584: Clarify behavior and fix minor grammar; also reconcile earlier “sources not returned” statement.

  • Tighten grammar and bullet formatting; explicitly call out that this tool is the intended exception to the earlier guidance that sources aren’t returned.
  • Add a note that a 400 error is raised when file_name is missing from the contract’s sources.

Apply this diff:

-### inspect_contract_code
-
-This tool lets agents explore verified contract sources without flooding the
-LLM context. When invoked without `file_name`, it returns structured metadata
-and a list of source file paths. Supplying `file_name` returns the raw contents
-of that file.
-
-- **Caching**: Processed contracts are cached in-memory with LRU eviction and
-  TTL expiry to minimize repeat API calls.
-- **File tree logic**: For multi-file contracts, all sources are indexed by
-  path. Single-file Solidity contracts with empty `file_path` use
-  `<name>.sol`, while Vyper contracts default to `<name>.vy`.
-- **Constructor args**: Raw and decoded constructor arguments are truncated
-  when exceeding the input-size limit, setting a `constructor_args_truncated`
-  flag in the metadata.
+### inspect_contract_code
+
+This tool lets agents explore verified contract sources without flooding the LLM context.
+- When invoked without `file_name`, it returns structured metadata plus a list of source file paths.
+- When `file_name` is provided, it returns the raw contents of that file.
+
+Notes:
+- Exception to earlier guidance: Unlike other tools that avoid returning full sources by default, `inspect_contract_code` intentionally returns source code when `file_name` is specified.
+- Caching: Processed contracts are cached in-memory with LRU eviction and TTL expiry to minimize repeat API calls.
+- File tree logic: For multi-file contracts, all sources are indexed by path. Single-file Solidity contracts with empty `file_path` use `<name>.sol`, while Vyper contracts default to `<name>.vy`.
+- Constructor args: Raw and decoded constructor arguments are truncated when exceeding the input-size limit, setting a `constructor_args_truncated` flag in the metadata.
+- Errors: If `file_name` is provided but not found among the contract’s sources, the tool raises a client error (400) with a list of available files.
🧹 Nitpick comments (36)
tests/integration/test_address_tools_integration.py (1)

67-69: Harden against Chainscout outages: also skip on ChainNotFoundError.

get_blockscout_base_url raises ChainNotFoundError on Chainscout lookup failures; currently the test only skips on httpx.RequestError. Add an import and broaden the except to skip on ChainNotFoundError as well to make this test resilient (matching the “be resilient to temporary network issues” guidance).

Apply outside-range changes with this diff:

@@
-import httpx
+import httpx
+from blockscout_mcp_server.tools.common import ChainNotFoundError
@@
-    try:
-        result = await get_address_info(chain_id="1", address=address, ctx=mock_ctx)
-    except httpx.RequestError as e:
-        pytest.skip(f"Skipping test due to network error on primary API call: {e}")
+    try:
+        result = await get_address_info(chain_id="1", address=address, ctx=mock_ctx)
+    except (httpx.RequestError, ChainNotFoundError) as e:
+        pytest.skip(f"Skipping test due to upstream resolution error: {e}")
blockscout_mcp_server/tools/common.py (1)

463-475: Optional: Harmonize truncation representation for strings vs nested structures.

Strings return "...", while nested data returns {"value_sample", "value_truncated"}. Consider returning a structured object for long strings too for consistency, or ensure downstream code/tests explicitly rely on constructor_args_truncated instead of string shape.

Example alternative:

-        if len(args) > INPUT_DATA_TRUNCATION_LIMIT:
-            return f"{args[:INPUT_DATA_TRUNCATION_LIMIT]}...", True
+        if len(args) > INPUT_DATA_TRUNCATION_LIMIT:
+            return {
+                "value_sample": args[:INPUT_DATA_TRUNCATION_LIMIT],
+                "value_truncated": True,
+            }, True
blockscout_mcp_server/llms.txt (1)

46-57: Align llms.txt with API.md ordering and add REST path for inspect_contract_code

I ran a quick comparison between the tool list in blockscout_mcp_server/llms.txt and the entries in API.md and noticed a few drifts that could lead to confusion:

  • The inspect_contract_code entry in llms.txt doesn’t include its REST route, whereas the first tool (__unlock_blockchain_analysis__) does. For consistency, append “(/v1/inspect_contract_code)” to that line.
  • The sequence of functions in llms.txt diverges from the order in API.md. In particular:
    • get_address_by_ens_name and lookup_token_by_symbol appear much earlier in llms.txt than in API.md.
    • Core contract endpoints in API.md (e.g. get_contract_abi, inspect_contract_code, then get_address_by_ens_name) are interleaved with address- and token-related tools in the LLM list.
  • There’s a new read_contract entry in llms.txt that isn’t present in API.md; decide whether to update the reference docs or adjust its placement here.

Please reorder the numbered list in llms.txt to mirror the structure of the “Contract” section in API.md, and update the inspect_contract_code line as shown:

- 6. **`inspect_contract_code`** - Inspects a verified contract's source code
+ 6. **`inspect_contract_code`** (/v1/inspect_contract_code) - Inspects a verified contract's source code

Optional: review whether deprecated endpoints like get_address_logs should be listed (or intentionally omitted) for completeness across both docs.

AGENTS.md (2)

34-35: Directory tree entry updated correctly; add a short usage example for the new tool.

The tree now references inspect_contract_code, which is great. Add a brief example invocation (MCP and curl) near this section to make the new tool discoverable in AGENTS.md without context switching.

Example snippet to add below the tools list (no diff required here):

Example:
- MCP: inspect_contract_code(chain_id="1", address="0x...", file_name=None)  # metadata mode
- REST: curl "http://127.0.0.1:8000/v1/inspect_contract_code?chain_id=1&address=0x..."
- REST (file): curl "http://127.0.0.1:8000/v1/inspect_contract_code?chain_id=1&address=0x...&file_name=contracts/Token.sol"

258-259: Update AGENTS.md to reference the new inspect_contract_code tests

The test files for inspect_contract_code have been added, so we should document them alongside the existing get_contract_abi tests.

• In AGENTS.md, under the contract_tools.py entry, add:

  • Unit tests: tests/tools/test_contract_code_inspection.py
  • Integration tests: tests/integration/test_contract_tools_integration.py
API.md (1)

430-450: Polish the new endpoint docs: fix table spacing, include file_name example, and note 400 error semantics.

  • Grammar/format: remove the stray line break after “source file for a” and ensure proper spacing in the table for file_name.
  • Add a second curl example demonstrating file_name.
  • Briefly document the 400 error when a file is not found, so clients know what to expect.

Apply this diff:

-#### Inspect Contract Code (`inspect_contract_code`)
-
-Returns contract metadata or the content of a specific source file for a
-verified smart contract.
+#### Inspect Contract Code (`inspect_contract_code`)
+
+Returns contract metadata or the content of a specific source file for a verified smart contract.

 `GET /v1/inspect_contract_code`

 **Parameters**

-| Name       | Type     | Required | Description                                                                 |
-| ---------- | -------- | -------- | --------------------------------------------------------------------------- |
-| `chain_id` | `string` | Yes      | The ID of the blockchain.                                                   |
-| `address`  | `string` | Yes      | The smart contract address.                                                 |
-| `file_name`| `string` | No       | The name of the source file to fetch. Omit to retrieve metadata and file list. |
+| Name        | Type     | Required | Description                                                                 |
+| ----------- | -------- | -------- | --------------------------------------------------------------------------- |
+| `chain_id`  | `string` | Yes      | The ID of the blockchain.                                                   |
+| `address`   | `string` | Yes      | The smart contract address.                                                 |
+| `file_name` | `string` | No       | Exact path/name of the source file to fetch. Omit to retrieve metadata and file list. |

 **Example Request**

 ```bash
 curl "http://127.0.0.1:8000/v1/inspect_contract_code?chain_id=1&address=0x..."

+Example Request (file contents)
+
+bash +curl "http://127.0.0.1:8000/v1/inspect_contract_code?chain_id=1&address=0x...&file_name=contracts/Token.sol" +
+
+Errors
+
+- 400 Bad Request: when file_name is provided but not found for the contract. The response body:
+
+json +{ "error": "File not found. Available files: [\"contracts/Token.sol\", \"contracts/Lib.sol\", ...]" } +


</blockquote></details>
<details>
<summary>tests/test_cache.py (3)</summary><blockquote>

`134-141`: **Avoid mutating private attributes; pass cache limits via constructor instead**

Directly changing `_max_size` in tests couples them to internals. Prefer constructor parameters to set max size.

Suggested change in this test:

```diff
-    cache = ContractCache()
-    cache._max_size = 2
+    cache = ContractCache(max_size=2)

Complementary change outside this file (ContractCache constructor):

 class ContractCache:
-    """In-process, thread-safe, LRU, TTL cache for processed contract data."""
+    """In-process, async-safe, LRU, TTL cache for processed contract data."""

-    def __init__(self) -> None:
-        self._cache: OrderedDict[str, tuple[CachedContract, float]] = OrderedDict()
-        self._lock = anyio.Lock()
-        self._max_size = config.contracts_cache_max_number
-        self._ttl = config.contracts_cache_ttl_seconds
+    def __init__(self, *, max_size: int | None = None, ttl_seconds: float | None = None) -> None:
+        self._cache: OrderedDict[str, tuple[CachedContract, float]] = OrderedDict()
+        self._lock = anyio.Lock()
+        self._max_size = max_size or config.contracts_cache_max_number
+        self._ttl = ttl_seconds or config.contracts_cache_ttl_seconds

146-153: Prefer monotonic time for TTL to avoid expiry drift on system clock changes

ContractCache uses time.time() for TTL; ChainCache uses time.monotonic(). For robustness and consistency, use monotonic in both.

No changes needed in this test, but propose updating the implementation:

-            contract_data, expiry_timestamp = self._cache[key]
-            if time.time() >= expiry_timestamp:
+            contract_data, expiry_timestamp = self._cache[key]
+            if time.monotonic() >= expiry_timestamp:
                 self._cache.pop(key)
                 return None
@@
-            expiry_timestamp = time.time() + self._ttl
+            expiry_timestamp = time.monotonic() + self._ttl

161-171: Refresh-on-access behavior validated, but avoid touching private fields and mixed async marks

  • Same private attribute concern as above; prefer constructor argument:
-    cache = ContractCache()
-    cache._max_size = 2
+    cache = ContractCache(max_size=2)
  • You globally set pytestmark = pytest.mark.anyio and also use @pytest.mark.asyncio on these tests. Mixing runners is unnecessary and can cause confusion. Since anyio is already configured, remove the per-test @pytest.mark.asyncio decorators here for consistency.

Optionally, add a lightweight concurrency test to assert lock correctness under load (reads move-to-end vs writes):

# New test suggestion (place near other contract cache tests)
@pytest.mark.anyio
async def test_contract_cache_concurrent_access_updates_lru():
    cache = ContractCache(max_size=2)
    await cache.set("A", CachedContract(metadata={}, source_files={}))
    await cache.set("B", CachedContract(metadata={}, source_files={}))
    async with anyio.create_task_group() as tg:
        for _ in range(50):
            tg.start_soon(cache.get, "A")
    await cache.set("C", CachedContract(metadata={}, source_files={}))
    assert await cache.get("B") is None  # B should be evicted, A recently accessed
blockscout_mcp_server/server.py (1)

96-96: Tool registration added; consider centralizing registration to avoid duplication

Registration is correct. To reduce maintenance, consider a single registry function that both MCP and REST call (as previously preferred by the maintainer) to prevent divergent tool lists.

blockscout_mcp_server/config.py (1)

28-31: Environment variable mapping is correct; use validation_alias and add bounds

Pydantic v2’s BaseSettings uses validation_alias (or alias) to bind environment variables, and any env_prefix is ignored when an alias is set. By specifying

contracts_cache_max_number: int = Field(default=10, validation_alias="blockscout_contracts_cache_max_number")

you will indeed read from BLOCKSCOUT_CONTRACTS_CACHE_MAX_NUMBER, not a double-prefixed name . No need to switch to env= here.

Optional refactor: to guard against zero or negative values, add a ge=1 constraint:

- contracts_cache_max_number: int = Field(default=10, validation_alias="blockscout_contracts_cache_max_number")
- contracts_cache_ttl_seconds: int = Field(default=3600, validation_alias="blockscout_contracts_cache_ttl_seconds")
+ contracts_cache_max_number: int = Field(
+     default=10,
+     validation_alias="blockscout_contracts_cache_max_number",
+     ge=1,
+ )
+ contracts_cache_ttl_seconds: int = Field(
+     default=3600,
+     validation_alias="blockscout_contracts_cache_ttl_seconds",
+     ge=1,
+ )

This enforces that both values are at least 1 without altering the environment-variable names.

README.md (1)

132-143: Clarify two-mode behavior and caching/truncation in the new tool’s description

To set correct expectations for users, expand the entry to describe metadata vs file content modes and note truncation and caching.

-6. `inspect_contract_code(chain_id, address, file_name=None)` - Returns contract metadata or the contents of a specified source file for verified contracts.
+6. `inspect_contract_code(chain_id, address, file_name=None)` - Inspects verified contract source.
+   - When `file_name` is omitted: returns `ContractMetadata` with contract name, language, compiler version, verification details, and `source_code_tree_structure` (list of file paths). Large fields (e.g., ABI/bytecode) are excluded and oversized items may be truncated with clear markers.
+   - When `file_name` is provided: returns the raw contents of that source file. Use the values from `source_code_tree_structure` exactly as shown.
+   - Supports single-file Solidity/Vyper, multi-file projects, and flattened Solidity. Results are cached in-process with an LRU+TTL policy for faster subsequent calls.
tests/integration/test_contract_tools_integration.py (6)

333-342: Make language assertion tolerant and resilient

Scanner labels can vary in case or include suffixes. Use a substring match to reduce brittleness.

-    assert result.data.language.lower() == "vyper"
+    assert "vyper" in result.data.language.lower()

Optionally, document why this address was chosen (long-lived verified Vyper contract) to satisfy stability guidance.


345-353: Avoid exact filename equality for flattened Solidity; assert structure characteristics instead

Exact filenames can change (verification metadata changes or re-verification). Prefer structural checks:

-    assert result.data.source_code_tree_structure == ["EternalStorageProxy.sol"]
+    assert len(result.data.source_code_tree_structure) == 1
+    assert result.data.source_code_tree_structure[0].endswith(".sol")

355-364: Strengthen assertions for multipart contract

Add a minimal language or extension check while keeping the primary structural assertion:

     assert len(result.data.source_code_tree_structure) > 1
+    # Optional: at least one Solidity/Vyper file in the bundle
+    assert any(name.endswith((".sol", ".vy")) for name in result.data.source_code_tree_structure)

366-377: Add quick sanity checks on file listing before fetching content

Ensure the file list is non-empty and the first entry looks like a Solidity source:

-    file_name = meta.data.source_code_tree_structure[0]
+    assert meta.data.source_code_tree_structure
+    file_name = meta.data.source_code_tree_structure[0]
+    assert file_name.endswith(".sol")

390-396: LGTM: unverified contract correctly surfaces HTTP status error

Consider asserting part of the error message to ensure the error originates from Blockscout verification status rather than unrelated transport errors.


333-396: Reduce flakiness: add a retry helper for transient network failures

Multiple tests duplicate try/except blocks for network errors. A tiny retry wrapper improves resilience and readability.

Add near the top of this file:

import asyncio

async def _with_retries(coro, attempts=3, delay=0.6):
    last_exc = None
    for _ in range(attempts):
        try:
            return await coro
        except (aiohttp.ClientError, httpx.HTTPError, OSError) as e:  # transient
            last_exc = e
            await asyncio.sleep(delay)
    pytest.skip(f"Network connectivity issue after {attempts} attempts: {last_exc}")

Then use it, e.g.:

-    result = await inspect_contract_code(chain_id="1", address=address, ctx=mock_ctx)
+    result = await _with_retries(inspect_contract_code(chain_id="1", address=address, ctx=mock_ctx))

This follows the integration testing guideline for resiliency.

blockscout_mcp_server/api/routes.py (1)

299-299: Route registered under /v1 with helper — good

  • Endpoint path and registration via _add_v1_tool_route align with guidelines.
  • Consider adding this endpoint to any developer-facing route docs/examples if not already done.
tests/api/test_routes.py (1)

154-160: Add failure test for missing chain_id

You covered missing address; add a twin test for missing chain_id to fully satisfy “required params” coverage.

Suggested addition:

+@pytest.mark.asyncio
+async def test_inspect_contract_code_route_missing_chain_id(client: AsyncClient):
+    """Missing required chain_id parameter returns 400."""
+    response = await client.get("/v1/inspect_contract_code?address=0xabc")
+    assert response.status_code == 400
+    assert response.json() == {"error": "Missing required query parameter: 'chain_id'"}
blockscout_mcp_server/models.py (1)

74-100: ContractMetadata shape looks solid; consider two small refinements

  • Keeping extra="allow" makes sense if we intentionally forward extra fields from Blockscout. If this model is considered internal/owned schema, we should drop extra="allow" per guidelines; please confirm intent.
  • verified_at as datetime instead of str would give us built-in validation while still serializing to ISO 8601 in JSON.

If you choose to tighten types and still allow pass-through fields, one compromise is to funnel unknown keys into an extras: dict[str, Any] field.

Example tweaks:

-class ContractMetadata(BaseModel):
+class ContractMetadata(BaseModel):
     """Detailed metadata for a verified smart contract."""
-    model_config = ConfigDict(extra="allow")
+    model_config = ConfigDict(extra="allow")  # keep if we intend to pass through upstream fields

-    verified_at: str | None = Field(description="The timestamp when the contract was verified.")
+    # Optional: strengthen type; Pydantic will parse ISO strings automatically
+    verified_at: datetime | None = Field(description="When the contract was verified (parsed from ISO string).")
blockscout_mcp_server/cache.py (4)

99-104: Type tighten CachedContract fields for clarity

Use precise generics to document the payload structure and aid tooling.

-class CachedContract(BaseModel):
+class CachedContract(BaseModel):
     """Represents the pre-processed and cached data for a smart contract."""
-
-    metadata: dict = Field(description="The processed metadata of the contract, with large fields removed.")
-    source_files: dict[str, str] = Field(description="A map of file paths to their source code content.")
+    metadata: dict[str, Any] = Field(
+        description="The processed metadata of the contract, with large fields removed."
+    )
+    source_files: dict[str, str] = Field(
+        description="A map of file paths to their source code content."
+    )

106-114: Docstring says “thread-safe” but implementation is async-safe

anyio.Lock provides coroutine-level mutual exclusion but isn’t cross-thread. If true thread-safety is required (rare in our async server), we’d need a threading.Lock around access or enforce single-threaded access. Recommend clarifying the docstring.

-class ContractCache:
-    """In-process, thread-safe, LRU, TTL cache for processed contract data."""
+class ContractCache:
+    """In-process, async-safe (event-loop), LRU, TTL cache for processed contract data."""

127-135: Evict expired entries before LRU to preserve hot, fresh items

Before evicting by LRU when over capacity, purge any expired items; otherwise we may evict a fresh entry while stale ones remain.

     async def set(self, key: str, value: CachedContract) -> None:
         """Add an entry to the cache, enforcing size and TTL."""
         async with self._lock:
-            expiry_timestamp = time.time() + self._ttl
+            expiry_timestamp = time.monotonic() + self._ttl
             self._cache[key] = (value, expiry_timestamp)
             self._cache.move_to_end(key)
-            if len(self._cache) > self._max_size:
+            # Drop expired first
+            now = time.monotonic()
+            expired_keys = [k for k, (_, exp) in self._cache.items() if now >= exp]
+            for k in expired_keys:
+                self._cache.pop(k, None)
+            if len(self._cache) > self._max_size:
                 self._cache.popitem(last=False)

137-138: Global singleton is fine; ensure key normalization upstream

Since keys are strings like f"{chain_id}:{address}", ensure callers always canonicalize addresses (e.g., lowercased) to avoid duplicate cache entries for checksummed vs. lowercased inputs. The current _fetch_and_process_contract uses the raw address; consider normalizing there.

tests/tools/test_contract_code_inspection.py (6)

14-44: Metadata-mode unit test is focused and uses the fixture correctly

  • Uses the mock_ctx fixture per guidelines.
  • Patches the internal fetch function; asserts types and key content.
    Optional: also assert that mock_ctx.report_progress.await_count == 0 to lock in “no-progress” behavior.
     result = await inspect_contract_code(chain_id="1", address="0xabc", file_name=None, ctx=mock_ctx)
     mock_fetch.assert_awaited_once_with("1", "0xabc", mock_ctx)
     assert isinstance(result, ToolResponse)
     assert isinstance(result.data, ContractMetadata)
     assert result.data.source_code_tree_structure == ["A.sol"]
+    assert mock_ctx.report_progress.await_count == 0

46-56: File-content mode covered; add assertion on returned type and no progress

Tiny tightening:

 result = await inspect_contract_code(chain_id="1", address="0xabc", file_name="A.sol", ctx=mock_ctx)
 assert result.data == "pragma"
+assert isinstance(result, ToolResponse)
+assert mock_ctx.report_progress.await_count == 0

58-68: Missing-file error path covered; assert error message includes available files

Strengthen the expectation to validate helpful diagnostics:

-    with pytest.raises(ValueError):
-        await inspect_contract_code(chain_id="1", address="0xabc", file_name="B.sol", ctx=mock_ctx)
+    with pytest.raises(ValueError) as ei:
+        await inspect_contract_code(chain_id="1", address="0xabc", file_name="B.sol", ctx=mock_ctx)
+    assert "B.sol" in str(ei.value) and "A.sol" in str(ei.value)

70-104: Cache-miss path: assert exact request args and base URL resolution

You already assert calls occurred. Also assert that:

  • get_blockscout_base_url was called with the chain_id.
  • make_blockscout_request was called with derived base_url and api_path.
-        patch(
+        patch(
             "blockscout_mcp_server.tools.contract_tools.get_blockscout_base_url",
             new_callable=AsyncMock,
             return_value="https://base",
-        ),
+        ) as mock_base_url,
     ):
         await _fetch_and_process_contract("1", "0xabc", mock_ctx)
     mock_get.assert_awaited_once_with("1:0xabc")
-    mock_request.assert_awaited_once()
+    mock_base_url.assert_awaited_once_with("1")
+    mock_request.assert_awaited_once_with(base_url="https://base", api_path="/api/v2/smart-contracts/0xabc")
     mock_set.assert_awaited_once()

126-159: Single-file Solidity derivation test is great; lock in cache key normalization if adopted

If you normalize addresses or cache keys later, consider asserting the exact key used in contract_cache.set. For now, current assertion is sufficient.


70-104: Missing test: constructor args truncation behavior

Given _truncate_constructor_args is part of processing, add a narrow unit test that feeds oversized constructor_args and asserts constructor_args_truncated=True in metadata, with processed content truncated.

Example addition:

+@pytest.mark.asyncio
+async def test_constructor_args_truncation_sets_flag(mock_ctx):
+    long_args = "0x" + ("ab" * 5000)  # large input
+    api_response = {
+        "name": "C",
+        "language": "Solidity",
+        "source_code": "code",
+        "file_path": "C.sol",
+        "constructor_args": long_args,
+    }
+    with (
+        patch("blockscout_mcp_server.tools.contract_tools.contract_cache.get", new_callable=AsyncMock, return_value=None),
+        patch("blockscout_mcp_server.tools.contract_tools.make_blockscout_request", new_callable=AsyncMock, return_value=api_response),
+        patch("blockscout_mcp_server.tools.contract_tools.contract_cache.set", new_callable=AsyncMock) as mock_set,
+        patch("blockscout_mcp_server.tools.contract_tools.get_blockscout_base_url", new_callable=AsyncMock, return_value="https://base"),
+    ):
+        cached = await _fetch_and_process_contract("1", "0xabc", mock_ctx)
+    assert cached.metadata.get("constructor_args_truncated") is True
+    # Ensure we still store a constructed metadata payload
+    assert "source_code_tree_structure" in cached.metadata

Also applies to: 126-159

blockscout_mcp_server/tools/contract_tools.py (5)

28-30: Unused ctx parameter in private helper.

_fetch_and_process_contract(..., ctx: Context) doesn’t use ctx, which will trigger Ruff’s unused-argument warning. Either use it for progress logging or prefix with an underscore to silence the linter.

Apply this minimal change to avoid the lint error:

-async def _fetch_and_process_contract(chain_id: str, address: str, ctx: Context) -> CachedContract:
+async def _fetch_and_process_contract(chain_id: str, address: str, _ctx: Context) -> CachedContract:

45-52: Single-file path derivation is good; add a guard when source_code is absent.

If the contract isn’t actually verified, source_code can be empty/None. Early error makes the failure explicit instead of caching an empty file.

-    else:
-        file_path = raw_data.get("file_path")
-        if not file_path or file_path == ".sol":
-            if raw_data.get("language", "").lower() == "solidity":
-                file_path = f"{raw_data.get('name', 'Contract')}.sol"
-            else:
-                file_path = f"{raw_data.get('name', 'Contract')}.vy"
-        source_files[file_path] = raw_data.get("source_code")
+    else:
+        src = raw_data.get("source_code")
+        if src is None:
+            raise ValueError("Contract does not have source code (likely not verified).")
+        source_files[main_file_path] = src

63-64: Deterministic ordering for source_code_tree_structure.

For reproducible outputs and easier testing, prefer a sorted list.

-    metadata_copy["source_code_tree_structure"] = list(source_files.keys())
+    metadata_copy["source_code_tree_structure"] = sorted(source_files.keys())

80-96: Docstring could better advertise the two-mode behavior.

Optional: Expand the docstring to explicitly describe metadata mode vs. file-content mode and that file mode returns raw source as a string.

Example:

-    """Inspects a verified contract's source code or metadata."""
+    """Inspects a verified contract's source code or metadata.
+
+    - When file_name is omitted: returns ContractMetadata (sanitized, with source_code_tree_structure).
+    - When file_name is provided: returns the raw source code (str) for that file.
+    """

98-107: Add progress reporting and improve 'not found' error determinism.

  • Tools should use report_and_log_progress for user feedback.
  • Sort the available filenames to make the error reproducible and easier to read.
-    processed = await _fetch_and_process_contract(chain_id, address, ctx)
+    await report_and_log_progress(
+        ctx,
+        progress=0.0,
+        total=2.0,
+        message=f"Fetching verified contract data for {address} on chain {chain_id}...",
+    )
+    processed = await _fetch_and_process_contract(chain_id, address, ctx)
     if file_name is None:
-        metadata = ContractMetadata.model_validate(processed.metadata)
-        return build_tool_response(data=metadata)
+        await report_and_log_progress(
+            ctx,
+            progress=1.0,
+            total=2.0,
+            message="Fetched contract metadata. Preparing response...",
+        )
+        metadata = ContractMetadata.model_validate(processed.metadata)
+        resp = build_tool_response(data=metadata)
+        await report_and_log_progress(ctx, progress=2.0, total=2.0, message="Done.")
+        return resp
     if file_name not in processed.source_files:
-        available = ", ".join(processed.source_files.keys())
+        available = ", ".join(sorted(processed.source_files.keys()))
         raise ValueError(
             f"File '{file_name}' not found in the source code for this contract. Available files: {available}"
         )
-    return build_tool_response(data=processed.source_files[file_name])
+    await report_and_log_progress(
+        ctx,
+        progress=1.0,
+        total=2.0,
+        message=f"Found '{file_name}'. Returning source file content...",
+    )
+    resp = build_tool_response(data=processed.source_files[file_name])
+    await report_and_log_progress(ctx, progress=2.0, total=2.0, message="Done.")
+    return resp

Copy link

@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: 2

♻️ Duplicate comments (1)
blockscout_mcp_server/tools/contract_tools.py (1)

53-77: Handle empty/missing file paths in additional_sources to avoid silently dropping files.

If an item in additional_sources has an empty or missing file_path, it’s skipped and the file is lost. This violates the “correct file-tree construction” acceptance criterion for multi-file contracts.

Apply:

-    source_files: dict[str, str] = {}
-    if raw_data.get("source_code"):
-        if raw_data.get("additional_sources"):
-            main_file_path = raw_data.get("file_path")
-            if not main_file_path or main_file_path == ".sol":
-                language = raw_data.get("language", "").lower()
-                if language == "solidity":
-                    main_file_path = f"{raw_data.get('name', 'Contract')}.sol"
-                else:
-                    main_file_path = f"{raw_data.get('name', 'Contract')}.vy"
-            source_files[main_file_path] = raw_data.get("source_code")
-            for item in raw_data.get("additional_sources", []):
-                item_path = item.get("file_path")
-                if item_path:
-                    source_files[item_path] = item.get("source_code")
+    source_files: dict[str, str] = {}
+    if raw_data.get("source_code"):
+        lang = (raw_data.get("language") or "").lower()
+        ext = "sol" if lang == "solidity" else "vy"
+        if raw_data.get("additional_sources"):
+            main_file_path = raw_data.get("file_path")
+            if not main_file_path or main_file_path == ".sol":
+                main_file_path = f"{raw_data.get('name', 'Contract')}.{ext}"
+            # Only add if non-None
+            if raw_data.get("source_code") is not None:
+                source_files[main_file_path] = raw_data.get("source_code")
+            for idx, item in enumerate(raw_data.get("additional_sources", [])):
+                fp = (item.get("file_path") or "").strip()
+                src = item.get("source_code")
+                if not fp:
+                    # Fallback to a stable unique filename to avoid losing sources
+                    fp = f"{raw_data.get('name', 'Contract')}_extra_{idx}.{ext}"
+                if src is not None:
+                    source_files[fp] = src
         else:
             file_path = raw_data.get("file_path")
             if not file_path or file_path == ".sol":
-                language = raw_data.get("language", "").lower()
-                if language == "solidity":
-                    file_path = f"{raw_data.get('name', 'Contract')}.sol"
-                else:
-                    file_path = f"{raw_data.get('name', 'Contract')}.vy"
+                file_path = f"{raw_data.get('name', 'Contract')}.{ext}"
             source_files[file_path] = raw_data.get("source_code")

This mirrors the safeguard suggested in earlier review cycles and prevents file loss.

🧹 Nitpick comments (6)
tests/tools/test_common_truncate.py (3)

1-4: Directly testing a private helper makes tests brittle; consider small public seam or add rationale.

Relying on _truncate_constructor_args (a private helper) couples tests to internal structure. Either expose a tiny public wrapper used by tools, or add a brief comment explaining why direct testing is intentional here.


7-12: Strengthen coverage and boundary checks for list inputs.

Good happy-path check. Please add boundary tests (exactly at the limit) and a None passthrough to guard regressions in the truncation limit semantics.

Add these tests (outside the current range):

def test_truncate_constructor_args_list_at_limit():
    data = ["a" * INPUT_DATA_TRUNCATION_LIMIT]
    processed, truncated = _truncate_constructor_args(data)
    assert truncated is False
    # Expect original structure retained when at limit
    assert processed == data

def test_truncate_constructor_args_none_passthrough():
    processed, truncated = _truncate_constructor_args(None)
    assert processed is None
    assert truncated is False

14-18: Add tests for string inputs and exact-limit boundary.

The string path in _truncate_constructor_args is distinct logic; cover it explicitly and verify the ellipsis size and boundary behavior.

Add these tests (outside the current range):

def test_truncate_constructor_args_string_over_limit():
    s = "x" * (INPUT_DATA_TRUNCATION_LIMIT + 5)
    processed, truncated = _truncate_constructor_args(s)
    assert truncated is True
    assert isinstance(processed, str)
    assert processed.endswith("...")
    assert len(processed) == INPUT_DATA_TRUNCATION_LIMIT + 3  # includes "..."

def test_truncate_constructor_args_string_at_limit():
    s = "y" * INPUT_DATA_TRUNCATION_LIMIT
    processed, truncated = _truncate_constructor_args(s)
    assert truncated is False
    assert processed == s
blockscout_mcp_server/tools/contract_tools.py (3)

36-39: Add progress updates around network I/O for better UX.

Expose milestones for resolving base URL and fetching the contract record.

Apply:

     base_url = await get_blockscout_base_url(chain_id)
     api_path = f"/api/v2/smart-contracts/{normalized_address}"
+    await report_and_log_progress(
+        ctx,
+        progress=1.0,
+        total=3.0,
+        message="Resolved Blockscout base URL; fetching contract record...",
+    )
     raw_data = await make_blockscout_request(base_url=base_url, api_path=api_path)
+    await report_and_log_progress(
+        ctx,
+        progress=2.0,
+        total=3.0,
+        message="Fetched contract record; processing source files...",
+    )

78-86: Truncation handling is correct; also surface truncation to callers via ToolResponse notes.

You correctly set constructor_args_truncated. Add a small note when returning metadata so API clients and agents don’t miss the truncation signal.

See suggested change in the metadata-return branch below to add a note when constructor_args_truncated is True.


87-99: Stabilize the source tree ordering.

list(source_files.keys()) depends on insertion order and can vary across paths. Sort for deterministic responses and easier testing.

Apply:

-    metadata_copy["source_code_tree_structure"] = list(source_files.keys())
+    metadata_copy["source_code_tree_structure"] = sorted(source_files.keys())
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between dea63f3 and 512a837.

📒 Files selected for processing (7)
  • blockscout_mcp_server/__init__.py (1 hunks)
  • blockscout_mcp_server/tools/common.py (1 hunks)
  • blockscout_mcp_server/tools/contract_tools.py (2 hunks)
  • pyproject.toml (1 hunks)
  • tests/integration/test_contract_tools_integration.py (2 hunks)
  • tests/tools/test_common_truncate.py (1 hunks)
  • tests/tools/test_contract_code_inspection.py (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • blockscout_mcp_server/init.py
  • pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/tools/test_contract_code_inspection.py
  • tests/integration/test_contract_tools_integration.py
  • blockscout_mcp_server/tools/common.py
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py

📄 CodeRabbit Inference Engine (.cursor/rules/000-role-and-task.mdc)

**/*.py: The MCP server must be implemented in Python, as you are a senior Python developer and the expertise is in Python.
The MCP server must wrap Blockscout APIs and expose blockchain data (balances, tokens, NFTs, contract metadata) via the Model Context Protocol (MCP).
The MCP server must communicate with AI agents/chat applications through stdin.

**/*.py: Regular Python modules should generally not exceed 500 lines of code (LOC). If a module approaches this limit, consider splitting it into multiple focused modules (e.g., address_tools.py and address_tools_advanced.py) to maintain readability and logical organization.
ALL import statements must be placed at the top of the Python module, immediately after the module docstring (if present) and before any other code. Never insert imports inline near where the functionality is used. Follow PEP 8 import order.
ALL linting and formatting issues must be resolved before committing or pushing code. Use the Ruff rules defined in 300-ruff-lint-and-format.mdc to identify and fix issues.

**/*.py: Always run ruff check . --fix and ruff format . on generated code before suggesting commits or opening a PR
Avoid using # noqa: E501 for ordinary code lines; split long lines instead. Only use # noqa: E501 for docstrings or string literals that must exceed 120 characters.
Use Ruff to enforce a 120-character line length, compatible with Black formatting

Files:

  • tests/tools/test_common_truncate.py
  • blockscout_mcp_server/tools/contract_tools.py
tests/tools/test_*.py

📄 CodeRabbit Inference Engine (.cursor/rules/200-development-testing-workflow.mdc)

Create or update the appropriate unit test file when adding new functionality or modifying existing code: Tool functions in tests/tools/test_{tool_module}.py

Files:

  • tests/tools/test_common_truncate.py
tests/tools/*

📄 CodeRabbit Inference Engine (.cursor/rules/210-unit-testing-guidelines.mdc)

tests/tools/*: Each unit test in tests/tools/* must be narrow and specific; a single test should verify one specific behavior or scenario. If a test covers multiple scenarios or input parameter groups, split it into separate tests.
Use the mock_ctx pytest fixture from tests/conftest.py for mocking the MCP Context object in tests; do not create manual MagicMock instances for the context within test functions.
When testing tools that return a ToolResponse object, do not parse JSON from string results in your test. Instead, mock the serialization function (json.dumps) if used internally, and make assertions on the structured ToolResponse object and its attributes.
When testing tools that transform a list of items, programmatically generate the expected_result from the mock_api_response to keep tests maintainable, while still documenting the transformation logic.
Always verify the number of calls to mock_ctx.report_progress in tests to ensure progress tracking is tested.
Assert that mocked API helper functions (such as make_blockscout_request) are called exactly once with the correct api_path and params in tests.
For tools using make_request_with_periodic_progress, mock the wrapper itself and assert that it was called with the correct arguments (request_function, request_args, etc.).
Unit test files in tests/tools/* must not exceed 500 lines of code (LOC). If a file approaches this limit, split tests into multiple files to maintain readability and focus.
Write tests covering success scenarios, error cases, and edge cases in unit test files.
Ensure all external API calls in tests are properly mocked using unittest.mock.patch and AsyncMock.
Group related tests using descriptive class names or clear function naming patterns.

Files:

  • tests/tools/test_common_truncate.py
blockscout_mcp_server/tools/*.py

📄 CodeRabbit Inference Engine (.cursor/rules/110-new-mcp-tool.mdc)

blockscout_mcp_server/tools/*.py: Create or modify a tool module file in blockscout_mcp_server/tools/ for each new tool, using @log_tool_invocation to decorate each tool function.
All tools MUST return a strongly-typed ToolResponse[YourDataModel] instead of generic ToolResponse[dict].
For tools that query Blockscout API, use get_blockscout_base_url for dynamic chain resolution and make_blockscout_request for API calls.
For tools that use fixed API endpoints (like BENS), use the appropriate request helper (e.g., make_bens_request) from tools/common.py.
All tools MUST return a standardized ToolResponse[YourDataModel] object using the build_tool_response helper.
For paginated tools, accept an optional cursor argument and use apply_cursor_to_params to handle incoming cursors.
For paginated tools, use create_items_pagination from tools/common.py to handle slicing and pagination in responses.
For paginated tools, include the exact notice 'SUPPORTS PAGINATION: If response includes 'pagination' field, use the provided next_call to get additional pages.' in the tool docstring.
When returning addresses from Blockscout API responses, simplify address objects to a single address string in the tool output.
Truncate large data fields (such as raw 'data' or deeply nested values) in tool responses to save LLM context, and add notes about truncation.
Recursively truncate long strings in nested data structures in tool responses, replacing them with a structured object to signal truncation and adding notes.
Always raise exceptions for error conditions (e.g., ValueError, RuntimeError, TimeoutError) instead of returning ToolResponse objects with error messages in notes.
Use the report_and_log_progress helper from tools/common.py for all progress reporting in tool functions, instead of calling ctx.report_progress directly.
When making multiple independent API calls in a tool, use asyncio.gather with return_exceptions=True for concurrent execution and proper error handling.

Files:

  • blockscout_mcp_server/tools/contract_tools.py
🧠 Learnings (3)
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : Truncate large data fields (such as raw 'data' or deeply nested values) in tool responses to save LLM context, and add notes about truncation.

Applied to files:

  • tests/tools/test_common_truncate.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : Recursively truncate long strings in nested data structures in tool responses, replacing them with a structured object to signal truncation and adding notes.

Applied to files:

  • tests/tools/test_common_truncate.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : For tools that query Blockscout API, use get_blockscout_base_url for dynamic chain resolution and make_blockscout_request for API calls.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
🧬 Code Graph Analysis (2)
tests/tools/test_common_truncate.py (1)
blockscout_mcp_server/tools/common.py (1)
  • _truncate_constructor_args (463-474)
blockscout_mcp_server/tools/contract_tools.py (3)
blockscout_mcp_server/cache.py (3)
  • CachedContract (99-103)
  • get (35-40)
  • get (115-125)
blockscout_mcp_server/models.py (4)
  • ContractAbiData (102-107)
  • ContractMetadata (75-98)
  • ContractReadData (110-113)
  • ToolResponse (325-350)
blockscout_mcp_server/tools/common.py (4)
  • _truncate_constructor_args (463-474)
  • get_blockscout_base_url (62-119)
  • make_blockscout_request (122-175)
  • build_tool_response (489-530)
⏰ 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). (2)
  • GitHub Check: Run Integration Tests
  • GitHub Check: Docker build and docker push
🔇 Additional comments (1)
blockscout_mcp_server/tools/contract_tools.py (1)

10-16: Good: strongly-typed models are imported and used consistently.

Imports for CachedContract, ContractMetadata, and ToolResponse align with the project’s typed ToolResponse pattern.

Copy link

@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: 0

🧹 Nitpick comments (5)
tests/tools/test_chains_tools_cache.py (5)

68-70: Consistent monotonic patching—consider DRYing via a fixture.

You repeat the same fake clock + monkeypatch pattern across tests. A small pytest fixture (e.g., fake_clock) that exposes .now and .advance() and auto-patches blockscout_mcp_server.cache.time.monotonic would remove duplication and improve readability.


110-112: Refresh-error path covered; consider asserting cache retention.

Nice check that an error on refresh is surfaced and bulk_set isn’t called again. Optionally also assert the cached snapshot remains intact after the failed refresh to guard against unintended cache invalidation on errors.


147-148: Concurrency dedupe test: avoid real sleeps for speed/determinism.

Using asyncio.sleep(0.1) introduces real-time delay. You can coordinate with an asyncio.Event to force overlap without sleeping, keeping the test fast and robust.

Sketch:

started = asyncio.Event()
released = asyncio.Event()

async def slow_request(*, api_path: str):
    started.set()
    await released.wait()
    return mock_api_response

# In test:
task1 = asyncio.create_task(get_chains_list(ctx=mock_ctx))
await started.wait()
task2 = asyncio.create_task(get_chains_list(ctx=mock_ctx))
released.set()
results = await asyncio.gather(task1, task2)

189-189: LGTM on switching cached expiry to time.monotonic().

This aligns the cached path with the new monotonic-based TTL. Optional: for consistency, you could also use the same fake clock approach here to avoid relying on a 60s safety window.


32-34: All TTL logic now consistently uses time.monotonic; no time.time calls remain.

Minor optional nitpick—your fake_time() helpers currently return int, whereas time.monotonic() returns float. To mirror the real API and avoid any subtle type-checking wrinkles, you can adjust each test’s fake_time() signature and return value:

• File: tests/tools/test_chains_tools_cache.py at lines 29, 65, 107, and 144

- def fake_time() -> int:
-     return fake_now
+ def fake_time() -> float:
+     return float(fake_now)

This change is purely cosmetic and not required for correctness.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 512a837 and 4b7e470.

📒 Files selected for processing (2)
  • blockscout_mcp_server/cache.py (3 hunks)
  • tests/tools/test_chains_tools_cache.py (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • blockscout_mcp_server/cache.py
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit Inference Engine (.cursor/rules/000-role-and-task.mdc)

**/*.py: The MCP server must be implemented in Python, as you are a senior Python developer and the expertise is in Python.
The MCP server must wrap Blockscout APIs and expose blockchain data (balances, tokens, NFTs, contract metadata) via the Model Context Protocol (MCP).
The MCP server must communicate with AI agents/chat applications through stdin.

**/*.py: Regular Python modules should generally not exceed 500 lines of code (LOC). If a module approaches this limit, consider splitting it into multiple focused modules (e.g., address_tools.py and address_tools_advanced.py) to maintain readability and logical organization.
ALL import statements must be placed at the top of the Python module, immediately after the module docstring (if present) and before any other code. Never insert imports inline near where the functionality is used. Follow PEP 8 import order.
ALL linting and formatting issues must be resolved before committing or pushing code. Use the Ruff rules defined in 300-ruff-lint-and-format.mdc to identify and fix issues.

**/*.py: Always run ruff check . --fix and ruff format . on generated code before suggesting commits or opening a PR
Avoid using # noqa: E501 for ordinary code lines; split long lines instead. Only use # noqa: E501 for docstrings or string literals that must exceed 120 characters.
Use Ruff to enforce a 120-character line length, compatible with Black formatting

Files:

  • tests/tools/test_chains_tools_cache.py
tests/tools/test_*.py

📄 CodeRabbit Inference Engine (.cursor/rules/200-development-testing-workflow.mdc)

Create or update the appropriate unit test file when adding new functionality or modifying existing code: Tool functions in tests/tools/test_{tool_module}.py

Files:

  • tests/tools/test_chains_tools_cache.py
tests/tools/*

📄 CodeRabbit Inference Engine (.cursor/rules/210-unit-testing-guidelines.mdc)

tests/tools/*: Each unit test in tests/tools/* must be narrow and specific; a single test should verify one specific behavior or scenario. If a test covers multiple scenarios or input parameter groups, split it into separate tests.
Use the mock_ctx pytest fixture from tests/conftest.py for mocking the MCP Context object in tests; do not create manual MagicMock instances for the context within test functions.
When testing tools that return a ToolResponse object, do not parse JSON from string results in your test. Instead, mock the serialization function (json.dumps) if used internally, and make assertions on the structured ToolResponse object and its attributes.
When testing tools that transform a list of items, programmatically generate the expected_result from the mock_api_response to keep tests maintainable, while still documenting the transformation logic.
Always verify the number of calls to mock_ctx.report_progress in tests to ensure progress tracking is tested.
Assert that mocked API helper functions (such as make_blockscout_request) are called exactly once with the correct api_path and params in tests.
For tools using make_request_with_periodic_progress, mock the wrapper itself and assert that it was called with the correct arguments (request_function, request_args, etc.).
Unit test files in tests/tools/* must not exceed 500 lines of code (LOC). If a file approaches this limit, split tests into multiple files to maintain readability and focus.
Write tests covering success scenarios, error cases, and edge cases in unit test files.
Ensure all external API calls in tests are properly mocked using unittest.mock.patch and AsyncMock.
Group related tests using descriptive class names or clear function naming patterns.

Files:

  • tests/tools/test_chains_tools_cache.py
🧠 Learnings (1)
📚 Learning: 2025-07-22T00:13:40.792Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/210-unit-testing-guidelines.mdc:0-0
Timestamp: 2025-07-22T00:13:40.792Z
Learning: Applies to tests/tools/* : Ensure all external API calls in tests are properly mocked using unittest.mock.patch and AsyncMock.

Applied to files:

  • tests/tools/test_chains_tools_cache.py
⏰ 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). (3)
  • GitHub Check: Run Integration Tests
  • GitHub Check: Run Unit Tests (3.12)
  • GitHub Check: Run Unit Tests (3.12)

Copy link

@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: 0

Caution

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

⚠️ Outside diff range comments (1)
blockscout_mcp_server/tools/contract_tools.py (1)

335-338: Avoid brittle pre-encoding check; rely on Web3 encoder and tailor exceptions

The import path and signature of check_if_arguments_can_be_encoded differ by Web3 version; pre-check can false-negative. Let Web3 perform encoding and convert encoding errors into a clear ValueError. This also removes a dependency on internal utils.

Apply:

-    check_args = [_for_check(a) for a in py_args]
-    if not check_if_arguments_can_be_encoded(abi, *check_args):
-        raise ValueError(f"Arguments {py_args} cannot be encoded for function '{function_name}'")
+    # Let Web3 perform ABI argument validation during encoding.

And refine exception handling:

-    try:
-        result = await fn(*py_args).call(block_identifier=block)
-    except ContractLogicError as e:
-        raise RuntimeError(f"Contract call failed: {e}") from e
-    except Exception as e:  # noqa: BLE001
-        # Surface unexpected errors with context to the caller
-        raise RuntimeError(f"Contract call errored: {type(e).__name__}: {e}") from e
+    try:
+        result = await fn(*py_args).call(block_identifier=block)
+    except (TypeError, ValueError) as e:
+        # Typically raised when arguments cannot be encoded to ABI
+        raise ValueError(f"Arguments {py_args} cannot be encoded for function '{function_name}': {e}") from e
+    except ContractLogicError as e:
+        raise RuntimeError(f"Contract call failed: {e}") from e
+    except Exception as e:  # noqa: BLE001
+        raise RuntimeError(f"Contract call errored: {type(e).__name__}: {e}") from e

Alternative (if you want an explicit check without calling the chain): attempt local ABI encoding via fn(*py_args)._encode_transaction_data() and catch encoding errors before the RPC call.

Also applies to: 350-356

♻️ Duplicate comments (1)
blockscout_mcp_server/tools/contract_tools.py (1)

65-89: Guard against missing file_path in additional sources to avoid silently dropping files

Current logic skips additional sources with empty/None file_path; consider assigning deterministic fallbacks so content isn’t dropped (rare but observed on some explorers). Keeps tree complete and predictable.

Apply:

-            for item in raw_data.get("additional_sources", []):
-                item_path = item.get("file_path")
-                if item_path:
-                    source_files[item_path] = item.get("source_code")
+            for idx, item in enumerate(raw_data.get("additional_sources", [])):
+                item_path = (item.get("file_path") or "").strip()
+                src = item.get("source_code")
+                if not item_path:
+                    lang = (raw_data.get("language") or "").lower()
+                    ext = "sol" if lang == "solidity" else "vy"
+                    item_path = f"{raw_data.get('name', 'Contract')}_extra_{idx}.{ext}"
+                if src is not None:
+                    source_files[item_path] = src
🧹 Nitpick comments (7)
blockscout_mcp_server/tools/contract_tools.py (7)

31-35: Add a quick progress hook around the cache lookup for better UX

Optional, but helpful: report progress before checking the cache and on cache hit. This also gives immediate feedback on hot-path returns.

Apply:

 normalized_address = address.lower()
 cache_key = f"{chain_id}:{normalized_address}"
-if cached := await contract_cache.get(cache_key):
-    return cached
+await report_and_log_progress(
+    ctx,
+    progress=0.0,
+    total=2.0,
+    message=f"Inspecting {address} on chain {chain_id}: checking cache...",
+)
+if cached := await contract_cache.get(cache_key):
+    await report_and_log_progress(
+        ctx,
+        progress=2.0,
+        total=2.0,
+        message="Cache hit: using processed metadata.",
+    )
+    return cached

99-111: Make source file list order stable for tests and diffs

Sort the tree structure for deterministic output across runs.

Apply:

-metadata_copy["source_code_tree_structure"] = list(source_files.keys())
+metadata_copy["source_code_tree_structure"] = sorted(source_files.keys())

132-134: Return type mixes typed and raw str; accepting as a documented exception

Per your stated preference (learning noted), returning a raw string for file retrieval is intentional and simplifies clients. While this deviates from the “strongly-typed data model” guideline, I’m OK with it here provided we keep it documented.

Follow-up: Add a brief comment in the docstring noting that the file-content branch intentionally returns a plain string for simplicity (exception to the general rule). No code change required.


146-148: Surface truncation via ToolResponse notes and guide the next step

When constructor_args are truncated, add a note; also provide a gentle instruction for retrieving files.

Apply:

-    if file_name is None:
-        metadata = ContractMetadata.model_validate(processed.metadata)
-        return build_tool_response(data=metadata)
+    if file_name is None:
+        metadata = ContractMetadata.model_validate(processed.metadata)
+        notes = None
+        if getattr(metadata, "constructor_args_truncated", False):
+            notes = [
+                "Constructor arguments were truncated to limit context size. "
+                "Long strings may be replaced with structured placeholders."
+            ]
+        return build_tool_response(
+            data=metadata,
+            notes=notes,
+            instructions=[
+                "To fetch a file's contents, call this tool again with 'file_name' "
+                "set to one of the paths from 'source_code_tree_structure'."
+            ],
+        )

149-154: Improve error message with closest match to reduce retries

On file miss, suggest the closest available file name and keep the list sorted for readability.

Apply:

-    if file_name not in processed.source_files:
-        available = ", ".join(processed.source_files.keys())
-        raise ValueError(
-            f"File '{file_name}' not found in the source code for this contract. Available files: {available}"
-        )
+    if file_name not in processed.source_files:
+        available_files = sorted(processed.source_files.keys())
+        try:
+            from difflib import get_close_matches  # noqa: PLC0415 (local import by design)
+            suggestion = get_close_matches(file_name, available_files, n=1)
+            hint = f" Did you mean: {suggestion[0]}?" if suggestion else ""
+        except Exception:
+            hint = ""
+        available = ", ".join(available_files)
+        raise ValueError(
+            f"File '{file_name}' not found in this contract. Available files: {available}.{hint}"
+        )

If you prefer top-level imports only, add:

# at the top of the file with other imports
from difflib import get_close_matches

203-227: Input normalization helper is pragmatic and safe

Deep conversion with checksum attempts and conservative hex handling is fine. Minor nit: numeric strings with leading “+/-” or hex ints aren’t converted—acceptable unless you plan to support them.


328-337: Validate function existence earlier; consider surfacing missing input names for payable vs non-payable mismatches

You already raise on missing function names; that’s good. If you later support named args, you may want to validate input names/lengths before call—out of scope for now.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4b7e470 and 69942bf.

📒 Files selected for processing (2)
  • blockscout_mcp_server/tools/contract_tools.py (2 hunks)
  • tests/tools/test_contract_code_inspection.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/tools/test_contract_code_inspection.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit Inference Engine (.cursor/rules/000-role-and-task.mdc)

**/*.py: The MCP server must be implemented in Python, as you are a senior Python developer and the expertise is in Python.
The MCP server must wrap Blockscout APIs and expose blockchain data (balances, tokens, NFTs, contract metadata) via the Model Context Protocol (MCP).
The MCP server must communicate with AI agents/chat applications through stdin.

**/*.py: Regular Python modules should generally not exceed 500 lines of code (LOC). If a module approaches this limit, consider splitting it into multiple focused modules (e.g., address_tools.py and address_tools_advanced.py) to maintain readability and logical organization.
ALL import statements must be placed at the top of the Python module, immediately after the module docstring (if present) and before any other code. Never insert imports inline near where the functionality is used. Follow PEP 8 import order.
ALL linting and formatting issues must be resolved before committing or pushing code. Use the Ruff rules defined in 300-ruff-lint-and-format.mdc to identify and fix issues.

**/*.py: Always run ruff check . --fix and ruff format . on generated code before suggesting commits or opening a PR
Avoid using # noqa: E501 for ordinary code lines; split long lines instead. Only use # noqa: E501 for docstrings or string literals that must exceed 120 characters.
Use Ruff to enforce a 120-character line length, compatible with Black formatting

Files:

  • blockscout_mcp_server/tools/contract_tools.py
blockscout_mcp_server/tools/*.py

📄 CodeRabbit Inference Engine (.cursor/rules/110-new-mcp-tool.mdc)

blockscout_mcp_server/tools/*.py: Create or modify a tool module file in blockscout_mcp_server/tools/ for each new tool, using @log_tool_invocation to decorate each tool function.
All tools MUST return a strongly-typed ToolResponse[YourDataModel] instead of generic ToolResponse[dict].
For tools that query Blockscout API, use get_blockscout_base_url for dynamic chain resolution and make_blockscout_request for API calls.
For tools that use fixed API endpoints (like BENS), use the appropriate request helper (e.g., make_bens_request) from tools/common.py.
All tools MUST return a standardized ToolResponse[YourDataModel] object using the build_tool_response helper.
For paginated tools, accept an optional cursor argument and use apply_cursor_to_params to handle incoming cursors.
For paginated tools, use create_items_pagination from tools/common.py to handle slicing and pagination in responses.
For paginated tools, include the exact notice 'SUPPORTS PAGINATION: If response includes 'pagination' field, use the provided next_call to get additional pages.' in the tool docstring.
When returning addresses from Blockscout API responses, simplify address objects to a single address string in the tool output.
Truncate large data fields (such as raw 'data' or deeply nested values) in tool responses to save LLM context, and add notes about truncation.
Recursively truncate long strings in nested data structures in tool responses, replacing them with a structured object to signal truncation and adding notes.
Always raise exceptions for error conditions (e.g., ValueError, RuntimeError, TimeoutError) instead of returning ToolResponse objects with error messages in notes.
Use the report_and_log_progress helper from tools/common.py for all progress reporting in tool functions, instead of calling ctx.report_progress directly.
When making multiple independent API calls in a tool, use asyncio.gather with return_exceptions=True for concurrent execution and proper error handling.

Files:

  • blockscout_mcp_server/tools/contract_tools.py
🧠 Learnings (6)
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : Use the report_and_log_progress helper from tools/common.py for all progress reporting in tool functions, instead of calling ctx.report_progress directly.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-08-21T04:53:52.741Z
Learnt from: akolotov
PR: blockscout/mcp-server#207
File: blockscout_mcp_server/tools/contract_tools.py:105-133
Timestamp: 2025-08-21T04:53:52.741Z
Learning: In blockscout_mcp_server/tools/contract_tools.py, the user prefers returning raw string content for file retrieval in inspect_contract_code rather than a structured model, citing that the file name parameter makes returning it again redundant.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : All tools MUST return a strongly-typed ToolResponse[YourDataModel] instead of generic ToolResponse[dict].

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : All tools MUST return a standardized ToolResponse[YourDataModel] object using the build_tool_response helper.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-07-10T19:21:50.271Z
Learnt from: akolotov
PR: blockscout/mcp-server#142
File: blockscout_mcp_server/api/routes.py:242-258
Timestamp: 2025-07-10T19:21:50.271Z
Learning: The user akolotov prefers a unified tool registration approach where a single function registers tools in both MCP and REST API instead of having separate registration points in blockscout_mcp_server/server.py and blockscout_mcp_server/api/routes.py. This would eliminate duplication and reduce maintenance burden.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : For tools that query Blockscout API, use get_blockscout_base_url for dynamic chain resolution and make_blockscout_request for API calls.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
🧬 Code Graph Analysis (1)
blockscout_mcp_server/tools/contract_tools.py (4)
blockscout_mcp_server/cache.py (3)
  • CachedContract (99-103)
  • get (35-40)
  • get (115-125)
blockscout_mcp_server/models.py (4)
  • ContractAbiData (102-107)
  • ContractMetadata (75-98)
  • ContractReadData (110-113)
  • ToolResponse (325-350)
blockscout_mcp_server/tools/common.py (5)
  • _truncate_constructor_args (463-474)
  • get_blockscout_base_url (62-119)
  • report_and_log_progress (477-486)
  • make_blockscout_request (122-175)
  • build_tool_response (489-530)
blockscout_mcp_server/tools/decorators.py (1)
  • log_tool_invocation (13-49)
🔇 Additional comments (6)
blockscout_mcp_server/tools/contract_tools.py (6)

36-50: Good: clear two-step progress and robust base URL resolution

This is aligned with our helpers usage and provides user-visible milestones. No issues.


51-64: Sane defaults for missing metadata fields

Defaulting commonly missing fields to None avoids KeyErrors downstream and stabilizes the model. Looks good.


112-114: LRU+TTL cache population is correct

Caching the pre-processed bundle (metadata + files) matches the design and avoids re-work. Good.


157-201: ABI fetch flow is clean and idiomatic

Good use of base URL resolution, progress reporting, and minimal post-processing into a typed model.


10-23: Imports are correctly grouped and use project helpers

PEP 8 grouping looks correct, and helpers are used consistently. No action.


335-338: Import path and signature verified
For Web3 v7.13.0, check_if_arguments_can_be_encoded is correctly exported from web3.utils.abi and its signature (abi_element, *args, abi_codec=None, **kwargs) matches our usage (web3py.readthedocs.io). No changes needed.

Copy link

@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: 0

🧹 Nitpick comments (7)
tests/tools/test_chains_tools_cache.py (7)

142-142: Docstring does not match what the test exercises (sequential, not concurrent)

This test currently validates deduplication via the cache on two sequential calls. Either update the docstring to reflect “sequential calls are deduped via cache” or make the test truly concurrent (you already have a true concurrency test below).

Apply this minimal docstring fix:

-    """Test that concurrent calls are properly handled by the cache locking mechanism."""
+    """Sequential calls are deduplicated by the cache (concurrency covered separately)."""

178-191: Add progress logging assertions to meet testing guidelines

Per guidelines, tests in tests/tools/* should verify progress reporting. This case makes two sequential calls, so report_progress and info should be called twice per call (4 total).

         # Both results should be the same
         assert result1.data == result2.data
         assert len(result1.data) == 1
         assert result1.data[0].name == "Ethereum"
+
+        # Progress reporting: 2 calls per invocation (start + end) × 2 invocations
+        assert mock_ctx.report_progress.call_count == 4
+        assert mock_ctx.info.call_count == 4
+
+        # Also verify the API mock was awaited exactly once with the expected params
+        mock_api_request.assert_awaited_once_with(api_path="/api/chains")

257-268: Strengthen the true-concurrency assertions and verify progress reporting

Great control-flow with events. Add progress assertions (2 per call = 4 total) and verify awaited params to align with the test guidelines.

         # Both results should be identical
         assert len(results) == 2
         assert results[0].data == results[1].data
         assert len(results[0].data) == 1
         assert results[0].data[0].name == "Ethereum"
+
+        # Progress reporting across two concurrent invocations
+        assert mock_ctx.report_progress.call_count == 4
+        assert mock_ctx.info.call_count == 4
+
+        # API called once with the expected arguments
+        mock_api_request.assert_awaited_once_with(api_path="/api/chains")

98-100: Verify progress reporting for refresh-after-TTL path

A refresh implies two full invocations; assert progress logging happened twice per invocation.

         assert mock_request.call_count == 2
         assert mock_chain_cache.bulk_set.await_count == 2
         assert result1.data != result2.data
+        assert mock_ctx.report_progress.call_count == 4
+        assert mock_ctx.info.call_count == 4

136-138: Verify progress reporting for error path

On the second (failing) call, the “start” progress is reported but the “end” progress is not. Total should be 3 progress calls across the two invocations.

         assert mock_request.call_count == 2
         assert mock_chain_cache.bulk_set.await_count == 1
+        # First call: 2 progress updates; second (error) call: only the start update
+        assert mock_ctx.report_progress.call_count == 3
+        assert mock_ctx.info.call_count == 3

166-177: Prefer argument assertions on mocks for stronger guarantees

You already assert call counts. Add argument assertions to ensure URL map correctness passed to chain_cache.bulk_set (e.g., {"1": "https://eth"}) and api_path="/api/chains" for the request. If you’d like, I can propose the exact assert_has_awaits pattern using unittest.mock.call and update imports accordingly.


32-32: Optional: standardize AsyncMock assertions across tests

Some places use assert_awaited_once() while others use await_count. For consistency and readability, prefer the assertion helpers (assert_awaited_once(), assert_awaited_once_with(...)) throughout.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 69942bf and 8be9438.

📒 Files selected for processing (1)
  • tests/tools/test_chains_tools_cache.py (6 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/000-role-and-task.mdc)

**/*.py: The MCP server must be implemented in Python, as you are a senior Python developer and the expertise is in Python.
The MCP server must wrap Blockscout APIs and expose blockchain data (balances, tokens, NFTs, contract metadata) via the Model Context Protocol (MCP).
The MCP server must communicate with AI agents/chat applications through stdin.

**/*.py: Regular Python modules should generally not exceed 500 lines of code (LOC). If a module approaches this limit, consider splitting it into multiple focused modules (e.g., address_tools.py and address_tools_advanced.py) to maintain readability and logical organization.
ALL import statements must be placed at the top of the Python module, immediately after the module docstring (if present) and before any other code. Never insert imports inline near where the functionality is used. Follow PEP 8 import order.
ALL linting and formatting issues must be resolved before committing or pushing code. Use the Ruff rules defined in 300-ruff-lint-and-format.mdc to identify and fix issues.

**/*.py: Always run ruff check . --fix and ruff format . on generated code before suggesting commits or opening a PR
Avoid using # noqa: E501 for ordinary code lines; split long lines instead. Only use # noqa: E501 for docstrings or string literals that must exceed 120 characters.
Use Ruff to enforce a 120-character line length, compatible with Black formatting

Files:

  • tests/tools/test_chains_tools_cache.py
tests/tools/test_*.py

📄 CodeRabbit inference engine (.cursor/rules/200-development-testing-workflow.mdc)

Create or update the appropriate unit test file when adding new functionality or modifying existing code: Tool functions in tests/tools/test_{tool_module}.py

Files:

  • tests/tools/test_chains_tools_cache.py
tests/tools/*

📄 CodeRabbit inference engine (.cursor/rules/210-unit-testing-guidelines.mdc)

tests/tools/*: Each unit test in tests/tools/* must be narrow and specific; a single test should verify one specific behavior or scenario. If a test covers multiple scenarios or input parameter groups, split it into separate tests.
Use the mock_ctx pytest fixture from tests/conftest.py for mocking the MCP Context object in tests; do not create manual MagicMock instances for the context within test functions.
When testing tools that return a ToolResponse object, do not parse JSON from string results in your test. Instead, mock the serialization function (json.dumps) if used internally, and make assertions on the structured ToolResponse object and its attributes.
When testing tools that transform a list of items, programmatically generate the expected_result from the mock_api_response to keep tests maintainable, while still documenting the transformation logic.
Always verify the number of calls to mock_ctx.report_progress in tests to ensure progress tracking is tested.
Assert that mocked API helper functions (such as make_blockscout_request) are called exactly once with the correct api_path and params in tests.
For tools using make_request_with_periodic_progress, mock the wrapper itself and assert that it was called with the correct arguments (request_function, request_args, etc.).
Unit test files in tests/tools/* must not exceed 500 lines of code (LOC). If a file approaches this limit, split tests into multiple files to maintain readability and focus.
Write tests covering success scenarios, error cases, and edge cases in unit test files.
Ensure all external API calls in tests are properly mocked using unittest.mock.patch and AsyncMock.
Group related tests using descriptive class names or clear function naming patterns.

Files:

  • tests/tools/test_chains_tools_cache.py
🧬 Code graph analysis (1)
tests/tools/test_chains_tools_cache.py (2)
blockscout_mcp_server/tools/chains_tools.py (1)
  • get_chains_list (16-77)
tests/conftest.py (1)
  • mock_ctx (8-13)
⏰ 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). (2)
  • GitHub Check: Run Integration Tests
  • GitHub Check: Docker build and docker push
🔇 Additional comments (2)
tests/tools/test_chains_tools_cache.py (2)

32-32: Monotonic time usage in tests: good alignment with cache implementation

Patching time.monotonic inside blockscout_mcp_server.cache and setting cached expiry_timestamp via time.monotonic() keeps tests deterministic and consistent with the production cache logic.

Also applies to: 68-68, 110-110, 148-148, 201-201, 281-281


271-293: Cached-progress test looks solid

Pre-seeding the snapshot and future expiry_timestamp with time.monotonic() is correct and ensures no network call. Assertions on no request and double progress logging are appropriate.

Copy link

@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: 6

♻️ Duplicate comments (1)
blockscout_mcp_server/tools/contract_tools.py (1)

29-36: Cache key normalization implemented

  • Lowercasing address and composing cache key as f"{chain_id}:{normalized_address}" avoids duplicate entries due to casing. Good fix.
🧹 Nitpick comments (13)
blockscout_mcp_server/config.py (1)

1-1: Import placement is fine; ensure Ruff import rules are satisfied.

The new Field import at the top is acceptable. Just make sure ruff check . --fix doesn’t flag import ordering (third-party grouping) given both are third-party packages.

API.md (1)

448-452: Add a second example showing file content mode.

A concrete file_name example helps consumers quickly discover the two-mode behavior.

Apply this addition:

 ```bash
 curl "http://127.0.0.1:8000/v1/inspect_contract_code?chain_id=1&address=0x..."

+bash +# File-content mode +curl "http://127.0.0.1:8000/v1/inspect_contract_code?chain_id=1&address=0x...&file_name=contracts/Token.sol" +


</blockquote></details>
<details>
<summary>blockscout_mcp_server/models.py (1)</summary><blockquote>

`95-101`: **Types are fine; consider tightening decoded args shape later.**

The broad union for `decoded_constructor_args` covers real-world variance. If we standardize decoding output later, we can narrow this type to `dict[str, Any] | list[Any]`.

</blockquote></details>
<details>
<summary>SPEC.md (1)</summary><blockquote>

`577-584`: **Minor language tightening (optional).**

Consider replacing “TTL expiry” with “TTL expiration,” and hyphenating “multi-file.” Not critical.

</blockquote></details>
<details>
<summary>tests/tools/test_contract_code_inspection.py (4)</summary><blockquote>

`78-91`: **Strengthen the negative-path assertion by checking the error message**

Currently only ValueError type is asserted. Verifying the message guards against accidental changes to error text and improves test diagnostics.


```diff
-    with pytest.raises(ValueError):
-        await inspect_contract_code(chain_id="1", address="0xabc", file_name="B.sol", ctx=mock_ctx)
+    with pytest.raises(ValueError, match=r"File 'B\.sol' not found.*Available files: A\.sol"):
+        await inspect_contract_code(chain_id="1", address="0xabc", file_name="B.sol", ctx=mock_ctx)

94-131: Verify Blockscout request arguments to lock API contract

You already assert it’s called once. Let’s also assert the exact kwargs to prevent regressions (e.g., wrong path or missing normalization). Optionally, assert the base URL resolver is called once.

-    with (
+    with (
         patch(
             "blockscout_mcp_server.tools.contract_tools.contract_cache.get",
             new_callable=AsyncMock,
             return_value=None,
         ) as mock_get,
         patch(
             "blockscout_mcp_server.tools.contract_tools.make_blockscout_request",
             new_callable=AsyncMock,
             return_value=api_response,
         ) as mock_request,
         patch(
             "blockscout_mcp_server.tools.contract_tools.contract_cache.set",
             new_callable=AsyncMock,
         ) as mock_set,
         patch(
             "blockscout_mcp_server.tools.contract_tools.get_blockscout_base_url",
             new_callable=AsyncMock,
             return_value="https://base",
-        ),
+        ) as mock_base_url,
     ):
         await _fetch_and_process_contract("1", "0xAbC", mock_ctx)
     mock_get.assert_awaited_once_with("1:0xabc")
-    mock_request.assert_awaited_once()
+    mock_request.assert_awaited_once()
+    # Verify request parameters
+    assert mock_request.await_args.kwargs["base_url"] == "https://base"
+    assert mock_request.await_args.kwargs["api_path"] == "/api/v2/smart-contracts/0xabc"
+    mock_base_url.assert_awaited_once_with("1")
     mock_set.assert_awaited_once()

134-152: Cache-hit test: optionally assert no base-URL lookup

Minor enhancement: explicitly ensure we don’t resolve the Blockscout base URL on a cache hit.

-    with (
+    with (
         patch(
             "blockscout_mcp_server.tools.contract_tools.contract_cache.get",
             new_callable=AsyncMock,
             return_value=cached,
         ) as mock_get,
         patch(
             "blockscout_mcp_server.tools.contract_tools.make_blockscout_request",
             new_callable=AsyncMock,
         ) as mock_request,
+        patch(
+            "blockscout_mcp_server.tools.contract_tools.get_blockscout_base_url",
+            new_callable=AsyncMock,
+        ) as mock_base_url,
     ):
         result = await _fetch_and_process_contract("1", "0xAbC", mock_ctx)
     assert result is cached
     mock_get.assert_awaited_once_with("1:0xabc")
     mock_request.assert_not_called()
+    mock_base_url.assert_not_called()
     assert mock_ctx.report_progress.await_count == 0

191-224: Multi-file missing main path: good coverage

  • Validates derived Main.sol and presence of B.sol. Optionally, assert order if you require main file first; otherwise this is fine.
blockscout_mcp_server/tools/contract_tools.py (5)

37-51: Progress reporting and API call flow look good; consider minor clarity improvement

  • Clear two-step progress: base URL resolved, then data fetched. Suggest including the address in the second message for easier log correlation when multiple requests run concurrently. Optional.
-    await report_and_log_progress(
+    await report_and_log_progress(
         ctx,
         progress=2.0,
         total=2.0,
-        message="Successfully fetched contract data.",
+        message=f"Successfully fetched contract data for {normalized_address}.",
     )

67-91: Source file map construction: handles edge cases; add one guard for duplicate paths?

  • Correctly derives main path when file_path is missing/“.sol” and switches extensions based on language.
  • Skips additional sources with missing file_path.
  • Optional: if additional_sources contain an entry duplicating the main file path, the latter will overwrite the former (or vice versa). If that’s acceptable, ignore; otherwise, add a guard to skip duplicates or log a note.
-            source_files[main_file_path] = raw_data.get("source_code")
+            # Avoid overwriting if an additional source unexpectedly duplicates the main path
+            source_files.setdefault(main_file_path, raw_data.get("source_code"))
@@
-                if item_path:
-                    source_files[item_path] = item.get("source_code")
+                if item_path:
+                    # Skip duplicates to preserve first-seen (main) content
+                    source_files.setdefault(item_path, item.get("source_code"))

101-111: Metadata slimming: consider removing one more bulky field if present

  • Current list removes all the big offenders. If the upstream adds “metadata” blobs under other keys (rare), those would pass through, but that’s speculative. Optional: also pop “implementations” if the array grows large in some chains; otherwise this is fine.
     for field in [
         "abi",
         "deployed_bytecode",
         "creation_bytecode",
         "source_code",
         "additional_sources",
         "file_path",
+        "implementations",
     ]:
         metadata_copy.pop(field, None)

118-166: Tool behavior is clear; typed returns, helpful notes/instructions; consider a “did you mean” hint

  • Great UX: different start messages per mode, notes when constructor args are truncated, and actionable instructions when files exist.
  • Minor UX enhancement: suggest close matches when file_name is not found.
-    if file_name not in processed.source_files:
+    if file_name not in processed.source_files:
         available = ", ".join(processed.source_files.keys())
-        raise ValueError(
-            f"File '{file_name}' not found in the source code for this contract. Available files: {available}"
-        )
+        try:
+            from difflib import get_close_matches
+            hint = ""
+            if processed.source_files:
+                match = get_close_matches(file_name, list(processed.source_files.keys()), n=1)
+                if match:
+                    hint = f" Did you mean: {match[0]}?"
+        except Exception:
+            hint = ""
+        raise ValueError(
+            f"File '{file_name}' not found in the source code for this contract. Available files: {available}.{hint}"
+        )

118-134: Docstring could mention dual-mode return types for clarity

  • Optional: expand the docstring to explicitly state the two modes and their concrete return models.
-async def inspect_contract_code(
+async def inspect_contract_code(
@@
-) -> ToolResponse[ContractMetadata | ContractSourceFile]:
-    """Inspects a verified contract's source code or metadata."""
+) -> ToolResponse[ContractMetadata | ContractSourceFile]:
+    """
+    Inspects a verified contract's source code or metadata.
+
+    - When file_name is omitted: returns ContractMetadata with source_code_tree_structure listing all files.
+    - When file_name is provided: returns ContractSourceFile containing the raw source code in file_content.
+    """
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8be9438 and eee7cb4.

📒 Files selected for processing (9)
  • API.md (1 hunks)
  • README.md (1 hunks)
  • SPEC.md (1 hunks)
  • blockscout_mcp_server/config.py (2 hunks)
  • blockscout_mcp_server/models.py (1 hunks)
  • blockscout_mcp_server/tools/contract_tools.py (2 hunks)
  • tests/api/test_routes.py (1 hunks)
  • tests/integration/test_contract_tools_integration.py (2 hunks)
  • tests/tools/test_contract_code_inspection.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/api/test_routes.py
  • tests/integration/test_contract_tools_integration.py
🧰 Additional context used
📓 Path-based instructions (7)
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/000-role-and-task.mdc)

**/*.py: The MCP server must be implemented in Python, as you are a senior Python developer and the expertise is in Python.
The MCP server must wrap Blockscout APIs and expose blockchain data (balances, tokens, NFTs, contract metadata) via the Model Context Protocol (MCP).
The MCP server must communicate with AI agents/chat applications through stdin.

**/*.py: Regular Python modules should generally not exceed 500 lines of code (LOC). If a module approaches this limit, consider splitting it into multiple focused modules (e.g., address_tools.py and address_tools_advanced.py) to maintain readability and logical organization.
ALL import statements must be placed at the top of the Python module, immediately after the module docstring (if present) and before any other code. Never insert imports inline near where the functionality is used. Follow PEP 8 import order.
ALL linting and formatting issues must be resolved before committing or pushing code. Use the Ruff rules defined in 300-ruff-lint-and-format.mdc to identify and fix issues.

**/*.py: Always run ruff check . --fix and ruff format . on generated code before suggesting commits or opening a PR
Avoid using # noqa: E501 for ordinary code lines; split long lines instead. Only use # noqa: E501 for docstrings or string literals that must exceed 120 characters.
Use Ruff to enforce a 120-character line length, compatible with Black formatting

Files:

  • tests/tools/test_contract_code_inspection.py
  • blockscout_mcp_server/config.py
  • blockscout_mcp_server/models.py
  • blockscout_mcp_server/tools/contract_tools.py
tests/tools/test_*.py

📄 CodeRabbit inference engine (.cursor/rules/200-development-testing-workflow.mdc)

Create or update the appropriate unit test file when adding new functionality or modifying existing code: Tool functions in tests/tools/test_{tool_module}.py

Files:

  • tests/tools/test_contract_code_inspection.py
tests/tools/*

📄 CodeRabbit inference engine (.cursor/rules/210-unit-testing-guidelines.mdc)

tests/tools/*: Each unit test in tests/tools/* must be narrow and specific; a single test should verify one specific behavior or scenario. If a test covers multiple scenarios or input parameter groups, split it into separate tests.
Use the mock_ctx pytest fixture from tests/conftest.py for mocking the MCP Context object in tests; do not create manual MagicMock instances for the context within test functions.
When testing tools that return a ToolResponse object, do not parse JSON from string results in your test. Instead, mock the serialization function (json.dumps) if used internally, and make assertions on the structured ToolResponse object and its attributes.
When testing tools that transform a list of items, programmatically generate the expected_result from the mock_api_response to keep tests maintainable, while still documenting the transformation logic.
Always verify the number of calls to mock_ctx.report_progress in tests to ensure progress tracking is tested.
Assert that mocked API helper functions (such as make_blockscout_request) are called exactly once with the correct api_path and params in tests.
For tools using make_request_with_periodic_progress, mock the wrapper itself and assert that it was called with the correct arguments (request_function, request_args, etc.).
Unit test files in tests/tools/* must not exceed 500 lines of code (LOC). If a file approaches this limit, split tests into multiple files to maintain readability and focus.
Write tests covering success scenarios, error cases, and edge cases in unit test files.
Ensure all external API calls in tests are properly mocked using unittest.mock.patch and AsyncMock.
Group related tests using descriptive class names or clear function naming patterns.

Files:

  • tests/tools/test_contract_code_inspection.py
blockscout_mcp_server/config.py

📄 CodeRabbit inference engine (.cursor/rules/110-new-mcp-tool.mdc)

blockscout_mcp_server/config.py: Add new API endpoint configuration variables (URL, timeout, API key) to the ServerConfig class in blockscout_mcp_server/config.py when introducing a new external API.
For each new paginated tool, add a dedicated page size configuration variable to blockscout_mcp_server/config.py.

Files:

  • blockscout_mcp_server/config.py
blockscout_mcp_server/models.py

📄 CodeRabbit inference engine (.cursor/rules/110-new-mcp-tool.mdc)

blockscout_mcp_server/models.py: Define a specific Pydantic data model for each new tool's data payload in blockscout_mcp_server/models.py, using Field(description=...) for every attribute.
When defining Pydantic models for external API responses, use ConfigDict(extra="allow") to permit additional fields.
Do NOT use extra="allow" for Pydantic models representing internal structured data.

Files:

  • blockscout_mcp_server/models.py
API.md

📄 CodeRabbit inference engine (.cursor/rules/800-api-documentation-guidelines.mdc)

API.md: Whenever a new MCP tool is added or an existing one is modified, its corresponding REST API endpoint in API.md MUST be added or updated.
Each endpoint documentation MUST follow the exact Markdown structure specified for consistency, including the heading format, parameter table, and example request.
The heading should be the human-readable tool name, with the function name in backticks.
The parameter table must clearly distinguish between required and optional parameters.
The curl example should demonstrate a realistic use case, including optional parameters where applicable.

Update REST API documentation in API.md for each new or updated endpoint, following the API documentation guidelines.

Files:

  • API.md
blockscout_mcp_server/tools/*.py

📄 CodeRabbit inference engine (.cursor/rules/110-new-mcp-tool.mdc)

blockscout_mcp_server/tools/*.py: Create or modify a tool module file in blockscout_mcp_server/tools/ for each new tool, using @log_tool_invocation to decorate each tool function.
All tools MUST return a strongly-typed ToolResponse[YourDataModel] instead of generic ToolResponse[dict].
For tools that query Blockscout API, use get_blockscout_base_url for dynamic chain resolution and make_blockscout_request for API calls.
For tools that use fixed API endpoints (like BENS), use the appropriate request helper (e.g., make_bens_request) from tools/common.py.
All tools MUST return a standardized ToolResponse[YourDataModel] object using the build_tool_response helper.
For paginated tools, accept an optional cursor argument and use apply_cursor_to_params to handle incoming cursors.
For paginated tools, use create_items_pagination from tools/common.py to handle slicing and pagination in responses.
For paginated tools, include the exact notice 'SUPPORTS PAGINATION: If response includes 'pagination' field, use the provided next_call to get additional pages.' in the tool docstring.
When returning addresses from Blockscout API responses, simplify address objects to a single address string in the tool output.
Truncate large data fields (such as raw 'data' or deeply nested values) in tool responses to save LLM context, and add notes about truncation.
Recursively truncate long strings in nested data structures in tool responses, replacing them with a structured object to signal truncation and adding notes.
Always raise exceptions for error conditions (e.g., ValueError, RuntimeError, TimeoutError) instead of returning ToolResponse objects with error messages in notes.
Use the report_and_log_progress helper from tools/common.py for all progress reporting in tool functions, instead of calling ctx.report_progress directly.
When making multiple independent API calls in a tool, use asyncio.gather with return_exceptions=True for concurrent execution and proper error handling.

Files:

  • blockscout_mcp_server/tools/contract_tools.py
🧠 Learnings (10)
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/config.py : Add new API endpoint configuration variables (URL, timeout, API key) to the ServerConfig class in blockscout_mcp_server/config.py when introducing a new external API.

Applied to files:

  • blockscout_mcp_server/config.py
📚 Learning: 2025-07-22T00:12:19.870Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/130-version-management.mdc:0-0
Timestamp: 2025-07-22T00:12:19.870Z
Learning: Update both `pyproject.toml` and `blockscout_mcp_server/__init__.py` simultaneously to maintain consistency across the codebase.

Applied to files:

  • blockscout_mcp_server/config.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/config.py : For each new paginated tool, add a dedicated page size configuration variable to blockscout_mcp_server/config.py.

Applied to files:

  • blockscout_mcp_server/config.py
📚 Learning: 2025-07-22T00:12:19.870Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/130-version-management.mdc:0-0
Timestamp: 2025-07-22T00:12:19.870Z
Learning: Applies to blockscout_mcp_server/__init__.py : When updating the version of the MCP server, update the `__version__` variable in `blockscout_mcp_server/__init__.py`.

Applied to files:

  • blockscout_mcp_server/config.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : Use the report_and_log_progress helper from tools/common.py for all progress reporting in tool functions, instead of calling ctx.report_progress directly.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-08-21T04:53:52.741Z
Learnt from: akolotov
PR: blockscout/mcp-server#207
File: blockscout_mcp_server/tools/contract_tools.py:105-133
Timestamp: 2025-08-21T04:53:52.741Z
Learning: In blockscout_mcp_server/tools/contract_tools.py, the user prefers returning raw string content for file retrieval in inspect_contract_code rather than a structured model, citing that the file name parameter makes returning it again redundant.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
  • SPEC.md
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : All tools MUST return a strongly-typed ToolResponse[YourDataModel] instead of generic ToolResponse[dict].

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : All tools MUST return a standardized ToolResponse[YourDataModel] object using the build_tool_response helper.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-07-10T19:21:50.271Z
Learnt from: akolotov
PR: blockscout/mcp-server#142
File: blockscout_mcp_server/api/routes.py:242-258
Timestamp: 2025-07-10T19:21:50.271Z
Learning: The user akolotov prefers a unified tool registration approach where a single function registers tools in both MCP and REST API instead of having separate registration points in blockscout_mcp_server/server.py and blockscout_mcp_server/api/routes.py. This would eliminate duplication and reduce maintenance burden.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : For tools that query Blockscout API, use get_blockscout_base_url for dynamic chain resolution and make_blockscout_request for API calls.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
🧬 Code graph analysis (2)
tests/tools/test_contract_code_inspection.py (4)
blockscout_mcp_server/cache.py (1)
  • CachedContract (99-103)
blockscout_mcp_server/models.py (3)
  • ContractMetadata (75-101)
  • ContractSourceFile (105-108)
  • ToolResponse (335-360)
blockscout_mcp_server/tools/contract_tools.py (2)
  • _fetch_and_process_contract (29-115)
  • inspect_contract_code (119-166)
tests/conftest.py (1)
  • mock_ctx (8-13)
blockscout_mcp_server/tools/contract_tools.py (4)
blockscout_mcp_server/cache.py (3)
  • CachedContract (99-103)
  • get (35-40)
  • get (115-125)
blockscout_mcp_server/models.py (5)
  • ContractAbiData (112-117)
  • ContractMetadata (75-101)
  • ContractReadData (120-123)
  • ContractSourceFile (105-108)
  • ToolResponse (335-360)
blockscout_mcp_server/tools/common.py (5)
  • _truncate_constructor_args (463-474)
  • get_blockscout_base_url (62-119)
  • report_and_log_progress (477-486)
  • make_blockscout_request (122-175)
  • build_tool_response (489-530)
blockscout_mcp_server/tools/decorators.py (1)
  • log_tool_invocation (13-49)
🪛 LanguageTool
API.md

[grammar] ~434-~434: There might be a mistake here.
Context: ...ual files. When file_name is provided, the source code is returned in a `Contra...

(QB_NEW_EN)


[grammar] ~435-~435: There might be a mistake here.
Context: ...a ContractSourceFile object containing file_content. `GET /v1/inspect_contra...

(QB_NEW_EN)


[grammar] ~446-~446: There might be a mistake here.
Context: ... | | file_name| string | No | The name of the s...

(QB_NEW_EN)

SPEC.md

[grammar] ~574-~574: There might be a mistake here.
Context: ...ereturns aContractSourceFileobject whosefile_content` contains the raw so...

(QB_NEW_EN)


[grammar] ~578-~578: There might be a mistake here.
Context: ...TTL expiry to minimize repeat API calls. - File tree logic: For multi-file contra...

(QB_NEW_EN)


[grammar] ~581-~581: There might be a mistake here.
Context: ... Vyper contracts default to <name>.vy. - Constructor args: Raw and decoded cons...

(QB_NEW_EN)


[grammar] ~584-~584: There might be a mistake here.
Context: ..._args_truncated` flag in the metadata.

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
API.md

440-440: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


448-448: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

🔇 Additional comments (12)
API.md (1)

430-452: Dismiss duplicate endpoint warning – only one block exists

After verifying the file, there is exactly one occurrence of the
#### Inspect Contract Code (inspect_contract_code) block in API.md (lines 429–431). You can safely ignore the duplicate-endpoint concern.

Likely an incorrect or invalid review comment.

README.md (1)

131-133: No action needed for get_contract_abi exposure
After verifying all references, get_contract_abi is consistently exposed across the codebase and documentation:

  • Registered in blockscout_mcp_server/server.py (line 95) via mcp.tool
  • Declared in dxt/manifest.json with name and description
  • Listed in blockscout_mcp_server/llms.txt (line 45)
  • Documented in API.md under “Contract & Name Service Tools” (lines 411–417)
  • Included in README.md (line 131)

The earlier assumption that it had been removed from llms.txt is incorrect. No removals or updates are required.

Likely an incorrect or invalid review comment.

tests/tools/test_contract_code_inspection.py (7)

15-56: Solid metadata-mode happy-path test; good assertions on progress and instructions

  • Validates ToolResponse typing, metadata validation, instructions presence, and the exact progress message. Looks good.

60-75: File-content mode test is precise and aligned with typed return

  • Confirms ContractSourceFile usage and verifies the initial progress message. All good.

155-188: Single-file Solidity derivation test is clear and correct

  • Confirms fallback to ContractName.sol when file_path is ".sol". Good coverage and progress assertions.

227-274: Multi-file Solidity and single-file Vyper cases well covered

  • Ensures correct extensions and tree construction. Progress assertions match implementation. Looks good.

277-311: Unverified contract handling test is appropriate

  • Verifies empty source files and sensible default name normalization. Nicely done.

314-327: API error propagation test is precise

  • Confirms HTTP errors bubble up and that start progress is still emitted. Good.

331-357: Truncation note test is focused and correct

  • Asserts notes only when truncation flag is set. Matches tool behavior.
blockscout_mcp_server/tools/contract_tools.py (3)

10-17: Typed models imported correctly; aligns with project guidelines

  • Using ContractSourceFile keeps ToolResponse strongly typed and consistent across tools. Good adherence to guidelines.

52-66: Defaulting missing metadata fields is robust

  • Ensures ContractMetadata.model_validate never fails due to absent keys. Good defensive coding.

92-100: Constructor args truncation: correct application to raw and decoded fields

  • Properly sets the truncated flag if either raw or decoded args were truncated. This matches the model semantics.

Copy link

@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: 2

Caution

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

⚠️ Outside diff range comments (3)
tests/integration/test_contract_tools_integration.py (3)

129-133: TypeError risk: isinstance does not accept union types

isinstance(obj, list | tuple) raises TypeError in Python. Use a tuple of types instead.

-    assert isinstance(result.data.result, list | tuple)
+    assert isinstance(result.data.result, (list, tuple))

169-174: Same union misuse for bytes/bytearray checks

Use a tuple of types in isinstance.

-    if isinstance(res, bytes | bytearray):
+    if isinstance(res, (bytes, bytearray)):

Also applies to: 307-312


189-191: Replace all remaining isinstance(..., list | tuple) usages

Apply the same fix across the file for consistency.

-    assert isinstance(res, list | tuple)
+    assert isinstance(res, (list, tuple))
@@
-    assert isinstance(res, list | tuple) and len(res) == 3
+    assert isinstance(res, (list, tuple)) and len(res) == 3
@@
-    assert isinstance(res[1], list | tuple) and len(res[1]) == 3
+    assert isinstance(res[1], (list, tuple)) and len(res[1]) == 3
@@
-    assert isinstance(res[2], list | tuple) and len(res[2]) == 2
+    assert isinstance(res[2], (list, tuple)) and len(res[2]) == 2
@@
-    assert isinstance(res, list | tuple)
+    assert isinstance(res, (list, tuple))

Also applies to: 220-223, 237-244, 271-276, 296-299

♻️ Duplicate comments (1)
blockscout_mcp_server/tools/contract_tools.py (1)

32-33: Cache key uses lowercased address — improves hit rate.

This addresses the prior concern about case-sensitive duplicate entries in the contract cache.

🧹 Nitpick comments (19)
SPEC.md (1)

577-584: Tighten wording and add usage clarity for file_name

Minor grammar and clarity improvements; also make it explicit that file_name must match an entry from source_code_tree_structure.

- - **Caching**: Processed contracts are cached in-memory with LRU eviction and
-  TTL expiry to minimize repeat API calls.
+ - **Caching**: Processed contracts are cached in memory with LRU eviction and
+   TTL expiry to minimize repeat API calls.

- - **File tree logic**: For multi-file contracts, all sources are indexed by
-  path. Single-file Solidity contracts with empty `file_path` use
-  `<name>.sol`, while Vyper contracts default to `<name>.vy`.
+ - **File tree logic**: For multi-file contracts, all sources are indexed by
+   path. Single-file Solidity contracts with empty `file_path` use
+   `<name>.sol`, while Vyper contracts default to `<name>.vy`.
+   When fetching a specific file, `file_name` must exactly equal one of the
+   paths returned in `source_code_tree_structure`.

- - **Constructor args**: Raw and decoded constructor arguments are truncated
-  when exceeding the input-size limit, setting a `constructor_args_truncated`
-  flag in the metadata.
+ - **Constructor args**: Raw and decoded constructor arguments are truncated
+   when exceeding the input-size limit, setting a `constructor_args_truncated`
+   flag in the metadata.
blockscout_mcp_server/models.py (2)

95-101: Optional: default None for nullable fields to avoid validation surprises

While the annotations permit None, being explicit helps. Also consider constraining decoded_constructor_args to Any if upstream shapes can vary.

-    constructor_args: str | None = Field(description="The raw constructor arguments, possibly truncated.")
-    decoded_constructor_args: str | dict | list | None = Field(
-        default=None, description="Decoded constructor arguments, if available."
-    )
+    constructor_args: str | None = Field(default=None, description="The raw constructor arguments, possibly truncated.")
+    decoded_constructor_args: str | dict | list | None = Field(
+        default=None,
+        description="Decoded constructor arguments, if available."
+    )

105-109: Reconsider ContractSourceFile: acceptance criteria request a plain string in file mode

If we adhere to the “plain string” requirement, ContractSourceFile becomes unnecessary. Two options:

  • Strict: remove ContractSourceFile and return ToolResponse[str] in file mode.
  • Compatible: temporarily support both by documenting that data is a string in file mode and keep the class only for REST/backward-compat tests until they’re updated.

Let me know which direction you want; I can prep the follow-up diffs across tools and tests.

API.md (3)

442-447: Clarify file_name semantics and improve parameter description

Be explicit that file_name must match an entry in source_code_tree_structure.

-| `file_name`| `string` | No       | The name of the source file to fetch. Omit to retrieve metadata and file list. |
+| `file_name`| `string` | No       | The path of the source file to fetch, exactly as listed in `source_code_tree_structure`. Omit to retrieve metadata and file list. |

448-452: Add a second curl example showing file-content mode

Docs require an example including optional parameters when applicable.

 ```bash
 curl "http://127.0.0.1:8000/v1/inspect_contract_code?chain_id=1&address=0x..."

+bash +# Example: fetch specific source file +curl "http://127.0.0.1:8000/v1/inspect_contract_code?chain_id=1&address=0x...&file_name=EternalStorageProxy.sol" +


---

`440-440`: **Fix markdownlint: avoid using emphasis as a heading**

Replace emphasized labels with proper plain text or a subheading level consistent with the rest of the file.


```diff
-**Parameters**
+Parameters
@@
-**Example Request**
+Example Request

Also applies to: 448-448

tests/integration/test_contract_tools_integration.py (2)

333-341: Make integration tests more resilient: also skip on ValueError from Chainscout resolution

Your skip covers aiohttp/httpx/OSError, but the server may raise ValueError for chain resolution failures (as seen in your manual curl). Add ValueError to the excepts to prevent red failures when Chainscout metadata cannot be retrieved.

-    except (aiohttp.ClientError, httpx.HTTPError, OSError) as e:
+    except (aiohttp.ClientError, httpx.HTTPError, OSError, ValueError) as e:
         pytest.skip(f"Network connectivity issue: {e}")

Also applies to: 345-353, 356-364, 366-377, 379-387, 390-399


371-377: If file-content mode adopts plain string, adjust assertion accordingly

Only applicable if we align with objectives and return ToolResponse[str] in file mode.

-    assert "pragma solidity" in content.data.file_content
+    assert isinstance(content.data, str)
+    assert "pragma solidity" in content.data
tests/tools/test_contract_code_inspection.py (7)

6-11: Import hygiene: remove unused MagicMock if not used after changes

If you no longer need MagicMock due to not constructing requests directly, remove it to appease Ruff.

-from unittest.mock import AsyncMock, MagicMock, patch
+from unittest.mock import AsyncMock, patch

73-75: Adjust unit test if file-content mode returns a plain string

If you switch to ToolResponse[str] in file mode, assert against the string directly.

-    assert isinstance(result.data, ContractSourceFile)
-    assert result.data.file_content == "pragma"
+    assert isinstance(result.data, str)
+    assert result.data == "pragma"

85-91: Better error messaging in ValueError branch

Consider asserting on the ValueError message to ensure it’s the “file not found” path, not another ValueError.

-    with pytest.raises(ValueError):
+    with pytest.raises(ValueError, match="not found|unknown file"):
         await inspect_contract_code(chain_id="1", address="0xabc", file_name="B.sol", ctx=mock_ctx)

103-131: Strengthen cache miss test: assert request arguments

Guidelines recommend asserting make_blockscout_request call parameters. If the helper path is stable, assert it; otherwise, at least assert it was called with the resolved base URL.

-    mock_request.assert_awaited_once()
+    # Example: assert the API path and base URL were used correctly
+    mock_request.assert_awaited_once()
+    # mock_request.assert_awaited_once_with(
+    #     base_url="https://base",
+    #     api_path=f"/api/v2/smart-contracts/{'0xabc'}",
+    #     params=None,
+    # )

155-188: Edge case: file_path=".sol" test is good; also assert file content mapping

Nice coverage for deriving a default filename. Add a quick check that source_files contains the derived key to catch regressions in the map population.

     result = await _fetch_and_process_contract("1", "0xabc", mock_ctx)
     assert result.metadata["source_code_tree_structure"] == ["MyContract.sol"]
+    assert "MyContract.sol" in result.source_files

227-274: Good multi-file and Vyper coverage; consider adding flattened-Solidity case to unit tests

Integration covers flattened contracts; a minimal unit test here (mocked) would prevent regressions in the single-file flattened path.


313-358: Notes propagation test is solid; optionally assert instructions presence in metadata mode

The tool should include follow-up instructions when returning metadata; consider asserting instructions were set to guide clients to fetch file contents.

 result = await inspect_contract_code(chain_id="1", address="0xabc", file_name=None, ctx=mock_ctx)
 assert result.notes == ["Constructor arguments were truncated to limit context size."]
+assert result.instructions and "file_name" in result.instructions[0]
blockscout_mcp_server/tools/contract_tools.py (4)

32-35: Report a “cache hit” progress event before returning.

Small UX improvement: when the cache serves the request, emit a final progress update so clients get a completion signal even on fast paths.

Apply:

-    if cached := await contract_cache.get(cache_key):
-        return cached
+    if cached := await contract_cache.get(cache_key):
+        await report_and_log_progress(
+            ctx,
+            progress=2.0,
+            total=2.0,
+            message="Cache hit: using processed contract data.",
+        )
+        return cached

67-91: Harden additional_sources ingestion (strip paths and ignore None source bodies).

Blockscout edge cases occasionally include empty/whitespace file paths or null source bodies. Guarding here avoids odd map keys and surprises later.

Apply:

-            for item in raw_data.get("additional_sources", []):
-                item_path = item.get("file_path")
-                if item_path:
-                    source_files[item_path] = item.get("source_code")
+            for item in raw_data.get("additional_sources", []):
+                item_path = (item.get("file_path") or "").strip()
+                src = item.get("source_code")
+                if item_path and src is not None:
+                    source_files[item_path] = src

101-112: Stabilize source_code_tree_structure ordering for deterministic outputs.

Sorted paths help tests and client diffs.

Apply:

-    metadata_copy["source_code_tree_structure"] = list(source_files.keys())
+    metadata_copy["source_code_tree_structure"] = sorted(source_files.keys())

161-166: Make filename matching more forgiving and offer a helpful suggestion.

Trimming whitespace avoids easy-to-miss errors. Optionally, provide a “did you mean” hint.

Apply trim:

-    if file_name not in processed.source_files:
+    query_name = file_name.strip()
+    if query_name not in processed.source_files:
         available = ", ".join(processed.source_files.keys())
         raise ValueError(
             f"File '{file_name}' not found in the source code for this contract. Available files: {available}"
         )
-    return build_tool_response(data=ContractSourceFile(file_content=processed.source_files[file_name]))
+    return build_tool_response(data=ContractSourceFile(file_content=processed.source_files[query_name]))

Optionally, suggest a close match (requires a top-level import from difflib import get_close_matches):

candidates = list(processed.source_files.keys())
suggest = get_close_matches(query_name, candidates, n=1)
hint = f" Did you mean: {suggest[0]}?" if suggest else ""
raise ValueError(
    f"File '{file_name}' not found in the source code for this contract. Available files: {available}.{hint}"
)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8be9438 and eee7cb4.

📒 Files selected for processing (9)
  • API.md (1 hunks)
  • README.md (1 hunks)
  • SPEC.md (1 hunks)
  • blockscout_mcp_server/config.py (2 hunks)
  • blockscout_mcp_server/models.py (1 hunks)
  • blockscout_mcp_server/tools/contract_tools.py (2 hunks)
  • tests/api/test_routes.py (1 hunks)
  • tests/integration/test_contract_tools_integration.py (2 hunks)
  • tests/tools/test_contract_code_inspection.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • blockscout_mcp_server/config.py
  • README.md
  • tests/api/test_routes.py
🧰 Additional context used
📓 Path-based instructions (9)
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/000-role-and-task.mdc)

**/*.py: The MCP server must be implemented in Python, as you are a senior Python developer and the expertise is in Python.
The MCP server must wrap Blockscout APIs and expose blockchain data (balances, tokens, NFTs, contract metadata) via the Model Context Protocol (MCP).
The MCP server must communicate with AI agents/chat applications through stdin.

**/*.py: Regular Python modules should generally not exceed 500 lines of code (LOC). If a module approaches this limit, consider splitting it into multiple focused modules (e.g., address_tools.py and address_tools_advanced.py) to maintain readability and logical organization.
ALL import statements must be placed at the top of the Python module, immediately after the module docstring (if present) and before any other code. Never insert imports inline near where the functionality is used. Follow PEP 8 import order.
ALL linting and formatting issues must be resolved before committing or pushing code. Use the Ruff rules defined in 300-ruff-lint-and-format.mdc to identify and fix issues.

**/*.py: Always run ruff check . --fix and ruff format . on generated code before suggesting commits or opening a PR
Avoid using # noqa: E501 for ordinary code lines; split long lines instead. Only use # noqa: E501 for docstrings or string literals that must exceed 120 characters.
Use Ruff to enforce a 120-character line length, compatible with Black formatting

Files:

  • blockscout_mcp_server/models.py
  • tests/integration/test_contract_tools_integration.py
  • tests/tools/test_contract_code_inspection.py
  • blockscout_mcp_server/tools/contract_tools.py
blockscout_mcp_server/models.py

📄 CodeRabbit inference engine (.cursor/rules/110-new-mcp-tool.mdc)

blockscout_mcp_server/models.py: Define a specific Pydantic data model for each new tool's data payload in blockscout_mcp_server/models.py, using Field(description=...) for every attribute.
When defining Pydantic models for external API responses, use ConfigDict(extra="allow") to permit additional fields.
Do NOT use extra="allow" for Pydantic models representing internal structured data.

Files:

  • blockscout_mcp_server/models.py
tests/integration/test_*_integration.py

📄 CodeRabbit inference engine (.cursor/rules/200-development-testing-workflow.mdc)

Add integration tests for tool functions in tests/integration/test_{tool_module}_integration.py when adding new tools that interact with live APIs

tests/integration/test_*_integration.py: Tool-level integration tests targeting high-level MCP tool functions (e.g., get_latest_block, get_tokens_by_address) must be located in files matching tests/integration/test_*_integration.py and validate data extraction and schema against live API responses.
Integration tests for tools that return paginated data must use a loop with a max_pages_to_check limit to search across pages, and skip the test with a clear message if the target data is not found after the maximum number of pages.
Integration tests for tools supporting cursor-based pagination must perform a two-step test: first call without a cursor, extract the cursor, then call again with the cursor and assert the second page's data is different from the first.

Files:

  • tests/integration/test_contract_tools_integration.py
tests/integration/test_*.py

📄 CodeRabbit inference engine (.cursor/rules/200-development-testing-workflow.mdc)

Follow the guidelines in 220-integration-testing-guidelines.mdc when writing integration tests

Files:

  • tests/integration/test_contract_tools_integration.py
tests/integration/*.py

📄 CodeRabbit inference engine (.cursor/rules/220-integration-testing-guidelines.mdc)

tests/integration/*.py: All integration test functions must be decorated with @pytest.mark.integration.
Integration tests must use stable, non-volatile data points such as historical blocks, famous addresses, or established ENS names to ensure reliability across runs.
Integration tests should be resilient to temporary network issues by implementing retry logic and skipping the test with a clear message if connectivity cannot be established after several attempts.
Integration tests must not hardcode environment-specific URLs in assertions; instead, dynamically resolve URLs using the same helper functions as the code under test.
Integration test files must not exceed 500 lines of code (LOC); if a file approaches this limit, split tests into multiple focused files to maintain readability and logical organization.
Group related integration tests using descriptive class names or clear function naming patterns, and use descriptive test names indicating what contract or schema is being validated.
Separate helper-level integration tests from tool-level integration tests into different files when appropriate.
Include clear comments in integration tests explaining why specific test data was chosen, especially for stable targets.

Files:

  • tests/integration/test_contract_tools_integration.py
tests/tools/test_*.py

📄 CodeRabbit inference engine (.cursor/rules/200-development-testing-workflow.mdc)

Create or update the appropriate unit test file when adding new functionality or modifying existing code: Tool functions in tests/tools/test_{tool_module}.py

Files:

  • tests/tools/test_contract_code_inspection.py
tests/tools/*

📄 CodeRabbit inference engine (.cursor/rules/210-unit-testing-guidelines.mdc)

tests/tools/*: Each unit test in tests/tools/* must be narrow and specific; a single test should verify one specific behavior or scenario. If a test covers multiple scenarios or input parameter groups, split it into separate tests.
Use the mock_ctx pytest fixture from tests/conftest.py for mocking the MCP Context object in tests; do not create manual MagicMock instances for the context within test functions.
When testing tools that return a ToolResponse object, do not parse JSON from string results in your test. Instead, mock the serialization function (json.dumps) if used internally, and make assertions on the structured ToolResponse object and its attributes.
When testing tools that transform a list of items, programmatically generate the expected_result from the mock_api_response to keep tests maintainable, while still documenting the transformation logic.
Always verify the number of calls to mock_ctx.report_progress in tests to ensure progress tracking is tested.
Assert that mocked API helper functions (such as make_blockscout_request) are called exactly once with the correct api_path and params in tests.
For tools using make_request_with_periodic_progress, mock the wrapper itself and assert that it was called with the correct arguments (request_function, request_args, etc.).
Unit test files in tests/tools/* must not exceed 500 lines of code (LOC). If a file approaches this limit, split tests into multiple files to maintain readability and focus.
Write tests covering success scenarios, error cases, and edge cases in unit test files.
Ensure all external API calls in tests are properly mocked using unittest.mock.patch and AsyncMock.
Group related tests using descriptive class names or clear function naming patterns.

Files:

  • tests/tools/test_contract_code_inspection.py
blockscout_mcp_server/tools/*.py

📄 CodeRabbit inference engine (.cursor/rules/110-new-mcp-tool.mdc)

blockscout_mcp_server/tools/*.py: Create or modify a tool module file in blockscout_mcp_server/tools/ for each new tool, using @log_tool_invocation to decorate each tool function.
All tools MUST return a strongly-typed ToolResponse[YourDataModel] instead of generic ToolResponse[dict].
For tools that query Blockscout API, use get_blockscout_base_url for dynamic chain resolution and make_blockscout_request for API calls.
For tools that use fixed API endpoints (like BENS), use the appropriate request helper (e.g., make_bens_request) from tools/common.py.
All tools MUST return a standardized ToolResponse[YourDataModel] object using the build_tool_response helper.
For paginated tools, accept an optional cursor argument and use apply_cursor_to_params to handle incoming cursors.
For paginated tools, use create_items_pagination from tools/common.py to handle slicing and pagination in responses.
For paginated tools, include the exact notice 'SUPPORTS PAGINATION: If response includes 'pagination' field, use the provided next_call to get additional pages.' in the tool docstring.
When returning addresses from Blockscout API responses, simplify address objects to a single address string in the tool output.
Truncate large data fields (such as raw 'data' or deeply nested values) in tool responses to save LLM context, and add notes about truncation.
Recursively truncate long strings in nested data structures in tool responses, replacing them with a structured object to signal truncation and adding notes.
Always raise exceptions for error conditions (e.g., ValueError, RuntimeError, TimeoutError) instead of returning ToolResponse objects with error messages in notes.
Use the report_and_log_progress helper from tools/common.py for all progress reporting in tool functions, instead of calling ctx.report_progress directly.
When making multiple independent API calls in a tool, use asyncio.gather with return_exceptions=True for concurrent execution and proper error handling.

Files:

  • blockscout_mcp_server/tools/contract_tools.py
API.md

📄 CodeRabbit inference engine (.cursor/rules/800-api-documentation-guidelines.mdc)

API.md: Whenever a new MCP tool is added or an existing one is modified, its corresponding REST API endpoint in API.md MUST be added or updated.
Each endpoint documentation MUST follow the exact Markdown structure specified for consistency, including the heading format, parameter table, and example request.
The heading should be the human-readable tool name, with the function name in backticks.
The parameter table must clearly distinguish between required and optional parameters.
The curl example should demonstrate a realistic use case, including optional parameters where applicable.

Update REST API documentation in API.md for each new or updated endpoint, following the API documentation guidelines.

Files:

  • API.md
🧠 Learnings (9)
📚 Learning: 2025-07-22T00:13:24.829Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/200-development-testing-workflow.mdc:0-0
Timestamp: 2025-07-22T00:13:24.829Z
Learning: Ensure your changes are well-tested by checking test coverage using pytest --cov=blockscout_mcp_server --cov-report=term-missing

Applied to files:

  • tests/integration/test_contract_tools_integration.py
📚 Learning: 2025-07-22T00:14:04.976Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/220-integration-testing-guidelines.mdc:0-0
Timestamp: 2025-07-22T00:14:04.976Z
Learning: Applies to tests/integration/test_*_integration.py : Tool-level integration tests targeting high-level MCP tool functions (e.g., get_latest_block, get_tokens_by_address) must be located in files matching tests/integration/test_*_integration.py and validate data extraction and schema against live API responses.

Applied to files:

  • tests/integration/test_contract_tools_integration.py
📚 Learning: 2025-07-22T00:13:24.829Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/200-development-testing-workflow.mdc:0-0
Timestamp: 2025-07-22T00:13:24.829Z
Learning: Run integration tests (pytest -m integration) when you have added or modified any existing MCP tool function, modified helper functions in tools/common.py, added or changed any integration test, or changed data extraction or transformation logic

Applied to files:

  • tests/integration/test_contract_tools_integration.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : Use the report_and_log_progress helper from tools/common.py for all progress reporting in tool functions, instead of calling ctx.report_progress directly.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-08-21T04:53:52.741Z
Learnt from: akolotov
PR: blockscout/mcp-server#207
File: blockscout_mcp_server/tools/contract_tools.py:105-133
Timestamp: 2025-08-21T04:53:52.741Z
Learning: In blockscout_mcp_server/tools/contract_tools.py, the user prefers returning raw string content for file retrieval in inspect_contract_code rather than a structured model, citing that the file name parameter makes returning it again redundant.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
  • SPEC.md
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : All tools MUST return a strongly-typed ToolResponse[YourDataModel] instead of generic ToolResponse[dict].

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : All tools MUST return a standardized ToolResponse[YourDataModel] object using the build_tool_response helper.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-07-10T19:21:50.271Z
Learnt from: akolotov
PR: blockscout/mcp-server#142
File: blockscout_mcp_server/api/routes.py:242-258
Timestamp: 2025-07-10T19:21:50.271Z
Learning: The user akolotov prefers a unified tool registration approach where a single function registers tools in both MCP and REST API instead of having separate registration points in blockscout_mcp_server/server.py and blockscout_mcp_server/api/routes.py. This would eliminate duplication and reduce maintenance burden.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : For tools that query Blockscout API, use get_blockscout_base_url for dynamic chain resolution and make_blockscout_request for API calls.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
🧬 Code graph analysis (2)
tests/integration/test_contract_tools_integration.py (1)
tests/conftest.py (1)
  • mock_ctx (8-13)
tests/tools/test_contract_code_inspection.py (2)
blockscout_mcp_server/cache.py (1)
  • CachedContract (99-103)
tests/conftest.py (1)
  • mock_ctx (8-13)
🪛 LanguageTool
API.md

[grammar] ~434-~434: There might be a mistake here.
Context: ...ual files. When file_name is provided, the source code is returned in a `Contra...

(QB_NEW_EN)


[grammar] ~435-~435: There might be a mistake here.
Context: ...a ContractSourceFile object containing file_content. `GET /v1/inspect_contra...

(QB_NEW_EN)


[grammar] ~446-~446: There might be a mistake here.
Context: ... | | file_name| string | No | The name of the s...

(QB_NEW_EN)

SPEC.md

[grammar] ~574-~574: There might be a mistake here.
Context: ...ereturns aContractSourceFileobject whosefile_content` contains the raw so...

(QB_NEW_EN)


[grammar] ~578-~578: There might be a mistake here.
Context: ...TTL expiry to minimize repeat API calls. - File tree logic: For multi-file contra...

(QB_NEW_EN)


[grammar] ~581-~581: There might be a mistake here.
Context: ... Vyper contracts default to <name>.vy. - Constructor args: Raw and decoded cons...

(QB_NEW_EN)


[grammar] ~584-~584: There might be a mistake here.
Context: ..._args_truncated` flag in the metadata.

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
API.md

440-440: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


448-448: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

🔇 Additional comments (7)
SPEC.md (1)

569-576: Inconsistent return shape vs. PR objectives: file mode should return a plain string, not a ContractSourceFile object

Per PR objectives, inspect_contract_code in file-content mode must “return the raw source code … as a plain string.” Here the spec claims it returns a ContractSourceFile object with file_content. Please align the spec with the intended contract, or confirm a deliberate deviation and update objectives/tests accordingly. If keeping standardized ToolResponse, the data field can still be a string (ToolResponse[str]) to satisfy both.

Apply this doc tweak if you choose the plain-string path:

- Supplying `file_name` returns a `ContractSourceFile` object
- whose `file_content` contains the raw source of that file.
+ Supplying `file_name` returns the raw source code of that file as a plain string
+ in the `data` field of the ToolResponse.
blockscout_mcp_server/tools/contract_tools.py (6)

10-17: Typed models wired into ToolResponse — aligns with tool guidelines.

Importing and using ContractSourceFile alongside ContractMetadata keeps responses strongly typed across both modes. Good consistency with build_tool_response.


37-51: Good use of get_blockscout_base_url + progress milestones.

The flow cleanly separates URL resolution and data fetch with clear progress messages. Nice.


92-100: Constructor args truncation logic looks correct.

You consistently set the truncation flag and propagate it if decoded args are also truncated.


118-133: Tool signature, annotation, and logging — LGTM.

The two-mode interface is clear and consistent with other tools; ctx is properly threaded for progress.


147-160: Helpful notes/instructions in metadata mode.

Good touch to surface truncation and guide clients to request specific files by name.


166-166: ContractSourceFile usage is correct as implemented.

I’ve verified that ContractSourceFile in blockscout_mcp_server/models.py defines only the file_content field and does not include content or file_name (lines 105–108). The call in contract_tools.py:166 aligns with this model, so no changes are needed.

Copy link

@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

♻️ Duplicate comments (1)
blockscout_mcp_server/config.py (1)

27-28: Add validation and descriptions to cache config fields

Harden these settings to prevent silent misconfiguration (0/negative values) and to self-document intent.

-    contracts_cache_max_number: int = 10  # Default 10 contracts
-    contracts_cache_ttl_seconds: int = 3600  # Default 1 hour
+    contracts_cache_max_number: int = Field(
+        default=10,
+        ge=1,
+        description="Maximum number of pre-processed contracts in the in-memory LRU cache.",
+    )
+    contracts_cache_ttl_seconds: int = Field(
+        default=3600,
+        ge=1,
+        description="Time-to-live (seconds) for cached contract entries before expiry.",
+    )
🧹 Nitpick comments (10)
README.md (2)

132-133: Clarify inspect_contract_code’s two modes and return types

Spell out metadata vs. file modes and the structured return to match the implementation and reduce ambiguity for tool users.

-6. `inspect_contract_code(chain_id, address, file_name=None)` - Allows getting the source files of verified contracts.
+6. `inspect_contract_code(chain_id, address, file_name=None)` - Inspects verified contract code.
+   - When `file_name` is omitted: returns `ContractMetadata` (excludes ABI/bytecode) and a `source_code_tree_structure` list.
+   - When `file_name` is provided: returns a `ContractSourceFile` whose `file_content` is the raw source of that file.

45-45: Minor: avoid blockquote for “Note” to satisfy markdownlint MD036

Use plain text “Note:” instead of a blockquote to prevent the “emphasis used instead of a heading” warning.

-> _Note: Docker is required for this setup_
+Note: Docker is required for this setup.
blockscout_mcp_server/tools/contract_tools.py (5)

29-38: Harden file path derivation for empty/whitespace and unknown languages

Guard against whitespace-only file_path and make extension selection explicit. This prevents odd keys like " .sol" and clarifies fallback behavior.

 def _determine_file_path(raw_data: dict[str, Any]) -> str:
     """Determine the appropriate file path for a contract source file based on language."""
-    file_path = raw_data.get("file_path")
-    if not file_path or file_path == ".sol":
-        language = raw_data.get("language", "").lower()
-        if language == "solidity":
-            file_path = f"{raw_data.get('name', 'Contract')}.sol"
-        else:
-            file_path = f"{raw_data.get('name', 'Contract')}.vy"
+    file_path = (raw_data.get("file_path") or "").strip()
+    if not file_path or file_path == ".sol":
+        language = (raw_data.get("language") or "").lower()
+        ext = "sol" if language == "solidity" else "vy"
+        file_path = f"{raw_data.get('name', 'Contract')}.{ext}"
     return file_path

44-47: Report cache check/hit to improve UX and observability

On a cache hit, the function returns without any progress/log output. Add minimal progress logs to surface cache behavior and keep step accounting consistent.

     normalized_address = address.lower()
     cache_key = f"{chain_id}:{normalized_address}"
-    if cached := await contract_cache.get(cache_key):
-        return cached
+    # Step 0: cache check
+    await report_and_log_progress(
+        ctx,
+        progress=0.0,
+        total=3.0,
+        message=f"Inspecting {address} on chain {chain_id}: checking cache...",
+    )
+    if cached := await contract_cache.get(cache_key):
+        await report_and_log_progress(
+            ctx,
+            progress=3.0,
+            total=3.0,
+            message="Cache hit: using processed metadata and source map.",
+        )
+        return cached

49-63: Align progress totals with a 3-step flow and add completion log

You currently mark steps against a total of 2.0, but with the added cache step and processing, a 3-step total improves clarity (resolve URL → fetch data → process/cache).

-    await report_and_log_progress(
-        ctx,
-        progress=1.0,
-        total=2.0,
-        message="Resolved Blockscout instance URL.",
-    )
+    await report_and_log_progress(ctx, progress=1.0, total=3.0, message="Resolved Blockscout instance URL.")
@@
-    await report_and_log_progress(
-        ctx,
-        progress=2.0,
-        total=2.0,
-        message="Successfully fetched contract data.",
-    )
+    await report_and_log_progress(ctx, progress=2.0, total=3.0, message="Successfully fetched contract data.")

And after caching (right before returning):

-    await contract_cache.set(cache_key, cached_contract)
-    return cached_contract
+    await contract_cache.set(cache_key, cached_contract)
+    await report_and_log_progress(ctx, progress=3.0, total=3.0, message="Processed and cached contract data.")
+    return cached_contract

101-112: Deterministic file listing in metadata

Sort the file paths to make source_code_tree_structure stable across runs and environments.

-    metadata_copy["source_code_tree_structure"] = list(source_files.keys())
+    metadata_copy["source_code_tree_structure"] = sorted(source_files.keys())

161-165: Improve error message determinism and usability

Sort file names and consider showing a short list when very large. This keeps messages stable and readable.

-        available = ", ".join(processed.source_files.keys())
+        available = ", ".join(sorted(processed.source_files.keys()))
         raise ValueError(
             f"File '{file_name}' not found in the source code for this contract. Available files: {available}"
         )
API.md (2)

432-433: Document the two-mode return types explicitly (ContractMetadata vs ContractSourceFile)

Make it explicit that metadata mode returns ContractMetadata and file mode returns ContractSourceFile with file_content. This avoids ambiguity for REST consumers.

-Returns contract metadata or the content of a specific source file for a verified smart contract.
+Returns contract metadata or the content of a specific source file for a verified smart contract.
+
+- When `file_name` is omitted: the `data` field contains a `ContractMetadata` object (ABI/bytecode are excluded). It also includes a `source_code_tree_structure` listing available file paths.
+- When `file_name` is provided: the `data` field contains a `ContractSourceFile` object whose `file_content` is the raw source of that file.

444-448: Add a second example request showing file mode

Demonstrate retrieving a specific file to clarify the API shape.

 ```bash
 curl "http://127.0.0.1:8000/v1/inspect_contract_code?chain_id=1&address=0x..."

+bash +# Example (file content mode) +curl "http://127.0.0.1:8000/v1/inspect_contract_code?chain_id=1&address=0x...&file_name=contracts/Token.sol" +


</blockquote></details>
<details>
<summary>SPEC.md (1)</summary><blockquote>

`366-380`: **Amend earlier statement that “contract source code is not returned”**

This new section introduces a tool that returns source code on demand. Update the earlier optimization note to carve out an explicit exception for `inspect_contract_code` to avoid contradicting guidance.


```diff
-   - Contract source code is not returned by tools to conserve context; when contract metadata is needed, only the ABI may be returned (sources are omitted).
+   - Contract source code is generally omitted to conserve context. The dedicated `inspect_contract_code` tool provides a two-phase, on-demand path to retrieve metadata and specific source files when necessary.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between eee7cb4 and bd318a8.

📒 Files selected for processing (6)
  • API.md (1 hunks)
  • README.md (2 hunks)
  • SPEC.md (1 hunks)
  • blockscout_mcp_server/config.py (1 hunks)
  • blockscout_mcp_server/models.py (1 hunks)
  • blockscout_mcp_server/tools/contract_tools.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • blockscout_mcp_server/models.py
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/000-role-and-task.mdc)

**/*.py: The MCP server must be implemented in Python, as you are a senior Python developer and the expertise is in Python.
The MCP server must wrap Blockscout APIs and expose blockchain data (balances, tokens, NFTs, contract metadata) via the Model Context Protocol (MCP).
The MCP server must communicate with AI agents/chat applications through stdin.

**/*.py: Regular Python modules should generally not exceed 500 lines of code (LOC). If a module approaches this limit, consider splitting it into multiple focused modules (e.g., address_tools.py and address_tools_advanced.py) to maintain readability and logical organization.
ALL import statements must be placed at the top of the Python module, immediately after the module docstring (if present) and before any other code. Never insert imports inline near where the functionality is used. Follow PEP 8 import order.
ALL linting and formatting issues must be resolved before committing or pushing code. Use the Ruff rules defined in 300-ruff-lint-and-format.mdc to identify and fix issues.

**/*.py: Always run ruff check . --fix and ruff format . on generated code before suggesting commits or opening a PR
Avoid using # noqa: E501 for ordinary code lines; split long lines instead. Only use # noqa: E501 for docstrings or string literals that must exceed 120 characters.
Use Ruff to enforce a 120-character line length, compatible with Black formatting

Files:

  • blockscout_mcp_server/config.py
  • blockscout_mcp_server/tools/contract_tools.py
blockscout_mcp_server/config.py

📄 CodeRabbit inference engine (.cursor/rules/110-new-mcp-tool.mdc)

blockscout_mcp_server/config.py: Add new API endpoint configuration variables (URL, timeout, API key) to the ServerConfig class in blockscout_mcp_server/config.py when introducing a new external API.
For each new paginated tool, add a dedicated page size configuration variable to blockscout_mcp_server/config.py.

Files:

  • blockscout_mcp_server/config.py
blockscout_mcp_server/tools/*.py

📄 CodeRabbit inference engine (.cursor/rules/110-new-mcp-tool.mdc)

blockscout_mcp_server/tools/*.py: Create or modify a tool module file in blockscout_mcp_server/tools/ for each new tool, using @log_tool_invocation to decorate each tool function.
All tools MUST return a strongly-typed ToolResponse[YourDataModel] instead of generic ToolResponse[dict].
For tools that query Blockscout API, use get_blockscout_base_url for dynamic chain resolution and make_blockscout_request for API calls.
For tools that use fixed API endpoints (like BENS), use the appropriate request helper (e.g., make_bens_request) from tools/common.py.
All tools MUST return a standardized ToolResponse[YourDataModel] object using the build_tool_response helper.
For paginated tools, accept an optional cursor argument and use apply_cursor_to_params to handle incoming cursors.
For paginated tools, use create_items_pagination from tools/common.py to handle slicing and pagination in responses.
For paginated tools, include the exact notice 'SUPPORTS PAGINATION: If response includes 'pagination' field, use the provided next_call to get additional pages.' in the tool docstring.
When returning addresses from Blockscout API responses, simplify address objects to a single address string in the tool output.
Truncate large data fields (such as raw 'data' or deeply nested values) in tool responses to save LLM context, and add notes about truncation.
Recursively truncate long strings in nested data structures in tool responses, replacing them with a structured object to signal truncation and adding notes.
Always raise exceptions for error conditions (e.g., ValueError, RuntimeError, TimeoutError) instead of returning ToolResponse objects with error messages in notes.
Use the report_and_log_progress helper from tools/common.py for all progress reporting in tool functions, instead of calling ctx.report_progress directly.
When making multiple independent API calls in a tool, use asyncio.gather with return_exceptions=True for concurrent execution and proper error handling.

Files:

  • blockscout_mcp_server/tools/contract_tools.py
API.md

📄 CodeRabbit inference engine (.cursor/rules/800-api-documentation-guidelines.mdc)

API.md: Whenever a new MCP tool is added or an existing one is modified, its corresponding REST API endpoint in API.md MUST be added or updated.
Each endpoint documentation MUST follow the exact Markdown structure specified for consistency, including the heading format, parameter table, and example request.
The heading should be the human-readable tool name, with the function name in backticks.
The parameter table must clearly distinguish between required and optional parameters.
The curl example should demonstrate a realistic use case, including optional parameters where applicable.

Update REST API documentation in API.md for each new or updated endpoint, following the API documentation guidelines.

Files:

  • API.md
🧠 Learnings (12)
📓 Common learnings
Learnt from: akolotov
PR: blockscout/mcp-server#207
File: blockscout_mcp_server/models.py:104-109
Timestamp: 2025-08-21T23:17:44.832Z
Learning: In blockscout_mcp_server/models.py, the ContractSourceFile model for inspect_contract_code file mode responses was deliberately chosen over returning plain strings. The user akolotov prefers the structured type approach (ContractSourceFile with file_content field) for file mode responses in the inspect_contract_code functionality.
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/config.py : Add new API endpoint configuration variables (URL, timeout, API key) to the ServerConfig class in blockscout_mcp_server/config.py when introducing a new external API.

Applied to files:

  • blockscout_mcp_server/config.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/config.py : For each new paginated tool, add a dedicated page size configuration variable to blockscout_mcp_server/config.py.

Applied to files:

  • blockscout_mcp_server/config.py
📚 Learning: 2025-08-14T22:57:36.348Z
Learnt from: akolotov
PR: blockscout/mcp-server#203
File: SPEC.md:500-501
Timestamp: 2025-08-14T22:57:36.348Z
Learning: For the blockscout/mcp-server project, SPEC.md should contain concise, high-level descriptions of features rather than detailed technical implementation details. The user akolotov prefers that detailed technical information be discoverable through the code itself rather than extensively documented in the specification file.

Applied to files:

  • SPEC.md
📚 Learning: 2025-08-21T04:53:52.774Z
Learnt from: akolotov
PR: blockscout/mcp-server#207
File: blockscout_mcp_server/tools/contract_tools.py:105-133
Timestamp: 2025-08-21T04:53:52.774Z
Learning: In blockscout_mcp_server/tools/contract_tools.py, the user prefers returning raw string content for file retrieval in inspect_contract_code rather than a structured model, citing that the file name parameter makes returning it again redundant.

Applied to files:

  • SPEC.md
  • README.md
  • blockscout_mcp_server/tools/contract_tools.py
  • API.md
📚 Learning: 2025-08-21T23:17:44.832Z
Learnt from: akolotov
PR: blockscout/mcp-server#207
File: blockscout_mcp_server/models.py:104-109
Timestamp: 2025-08-21T23:17:44.832Z
Learning: In blockscout_mcp_server/models.py, the ContractSourceFile model for inspect_contract_code file mode responses was deliberately chosen over returning plain strings. The user akolotov prefers the structured type approach (ContractSourceFile with file_content field) for file mode responses in the inspect_contract_code functionality.

Applied to files:

  • SPEC.md
  • README.md
  • blockscout_mcp_server/tools/contract_tools.py
  • API.md
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : Use the report_and_log_progress helper from tools/common.py for all progress reporting in tool functions, instead of calling ctx.report_progress directly.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : All tools MUST return a strongly-typed ToolResponse[YourDataModel] instead of generic ToolResponse[dict].

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : All tools MUST return a standardized ToolResponse[YourDataModel] object using the build_tool_response helper.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-07-10T19:21:50.271Z
Learnt from: akolotov
PR: blockscout/mcp-server#142
File: blockscout_mcp_server/api/routes.py:242-258
Timestamp: 2025-07-10T19:21:50.271Z
Learning: The user akolotov prefers a unified tool registration approach where a single function registers tools in both MCP and REST API instead of having separate registration points in blockscout_mcp_server/server.py and blockscout_mcp_server/api/routes.py. This would eliminate duplication and reduce maintenance burden.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-08-21T23:05:53.563Z
Learnt from: akolotov
PR: blockscout/mcp-server#207
File: blockscout_mcp_server/models.py:78-85
Timestamp: 2025-08-21T23:05:53.563Z
Learning: ContractMetadata in blockscout_mcp_server/models.py correctly uses ConfigDict(extra="allow") because it models data from the external Blockscout API endpoint /api/v2/smart-contracts/{address}, which returns different field sets depending on contract language (Solidity vs Vyper) and other contract-specific factors.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : For tools that query Blockscout API, use get_blockscout_base_url for dynamic chain resolution and make_blockscout_request for API calls.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
🧬 Code graph analysis (1)
blockscout_mcp_server/tools/contract_tools.py (3)
blockscout_mcp_server/cache.py (3)
  • CachedContract (99-103)
  • get (35-40)
  • get (115-125)
blockscout_mcp_server/models.py (5)
  • ContractAbiData (114-119)
  • ContractMetadata (75-103)
  • ContractReadData (122-125)
  • ContractSourceFile (107-110)
  • ToolResponse (337-362)
blockscout_mcp_server/tools/common.py (5)
  • _truncate_constructor_args (463-474)
  • get_blockscout_base_url (62-119)
  • report_and_log_progress (477-486)
  • make_blockscout_request (122-175)
  • build_tool_response (489-530)
🪛 LanguageTool
API.md

[grammar] ~442-~442: There might be a mistake here.
Context: ... | | file_name| string | No | The name of the s...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
API.md

436-436: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


444-444: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

⏰ 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). (2)
  • GitHub Check: Run Integration Tests
  • GitHub Check: Docker build and docker push
🔇 Additional comments (3)
blockscout_mcp_server/tools/contract_tools.py (3)

10-17: Typed ToolResponse and models import look consistent

Imports align with the project guideline to return strongly-typed ToolResponse[T]. No issues here.


118-166: Return type deviates from the original PR objective; confirm decision and align docs/tests

The PR objective states file mode returns “raw source code … as a plain string,” but the implementation returns a ContractSourceFile model. This is fine (and consistent with the project’s strongly-typed responses), but please explicitly confirm this change of contract, and ensure README/API/spec/tests uniformly reflect ContractSourceFile.file_content for file mode.

If you intended to keep the structured model, I can update API.md/README examples accordingly (some spots are still ambiguous).


166-166: LGTM: structured file return

Returning ContractSourceFile(file_content=...) keeps responses typed and extensible. This aligns with the project’s ToolResponse pattern.

Copy link

@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: 0

🧹 Nitpick comments (9)
tests/integration/test_contract_tools_integration.py (9)

18-18: Chain ID constants look good; consider centralizing.

Defining CHAIN_ID_MAINNET and CHAIN_ID_ARBITRUM improves readability. Consider moving chain ID constants into a shared test constants module (e.g., tests/constants.py) or tests/conftest.py to avoid duplication across files.

Also applies to: 20-20


335-345: Broaden skip conditions to handle aggregator/timeout errors; optionally add retries.

The environment has reported Chainscout aggregation errors for chain "1". Those may surface as RuntimeError (wrapped by the tool) or asyncio.TimeoutError. Expand the skip conditions or add lightweight retries to reduce false negatives in CI.

Apply this diff to the except block:

@@
-    except (aiohttp.ClientError, httpx.HTTPError, OSError) as e:
+    except (aiohttp.ClientError, httpx.HTTPError, OSError, asyncio.TimeoutError) as e:
         pytest.skip(f"Network connectivity issue: {e}")
+    except RuntimeError as e:
+        # Tool may wrap upstream aggregator failures in RuntimeError
+        if "Chainscout" in str(e) or "Could not retrieve or parse data for chain ID" in str(e):
+            pytest.skip(f"Blockscout aggregator issue: {e}")
+        raise

Additionally, add this import at the top of the file:

+import asyncio

347-356: Make the flattened-contract assertion less brittle.

Asserting the exact filename risks flakiness if verification metadata changes. Checking for a single .sol file preserves intent and stability.

Apply this diff:

-    assert result.data.source_code_tree_structure == ["EternalStorageProxy.sol"]
+    assert len(result.data.source_code_tree_structure) == 1
+    assert result.data.source_code_tree_structure[0].endswith(".sol")

358-367: Optional: Assert language for Stylus and guard for missing support.

Some Blockscout instances may label Stylus contracts as "stylus" or "rust", and older instances might not fully support Stylus metadata. Consider adding a soft assertion on language and a targeted skip if unsupported to improve signal while avoiding flakes.

Example:

-    assert len(result.data.source_code_tree_structure) > 1
+    assert len(result.data.source_code_tree_structure) > 1
+    # Optional language check (labels vary across instances)
+    assert result.data.language.lower() in {"stylus", "rust", "solidity", "vyper"}

369-382: Guard against empty file list before indexing; assert content type.

A simple precondition avoids IndexError if metadata unexpectedly contains no files (e.g., transient backend issue). Also validate the response shape matches the ContractSourceFile model choice noted in learnings.

Apply this diff:

-        file_name = meta.data.source_code_tree_structure[0]
+        assert len(meta.data.source_code_tree_structure) >= 1
+        file_name = meta.data.source_code_tree_structure[0]
@@
-    assert "pragma solidity" in content.data.file_content
+    assert isinstance(content.data.file_content, str)
+    assert "pragma solidity" in content.data.file_content.lower()

384-393: Multipart Solidity check is fine; consider strengthening with language assertion.

To better validate metadata integrity, you could also assert language "solidity" when available, while keeping the file-count check.

Example:

-    assert len(result.data.source_code_tree_structure) > 1
+    assert result.data.language.lower() == "solidity"
+    assert len(result.data.source_code_tree_structure) > 1

397-415: Good coverage for Vyper multipart; consider adding rationale and a fallback.

Nice validation of .vy main file and multi-file layout. Add a brief comment on why this address is considered stable, and optionally skip with a clear message if additional_sources disappear due to re-verification changes.

For example:

-    # This contract should have additional_sources with multiple Vyper files
+    # Historical, verified Vyper contract expected to have additional_sources with multiple files.
+    # If verification metadata changes upstream, skip to avoid flakiness.
@@
-    assert len(result.data.source_code_tree_structure) > 1
+    if len(result.data.source_code_tree_structure) <= 1:
+        pytest.skip("Contract no longer exposes multiple Vyper source files on the target explorer.")
+    assert len(result.data.source_code_tree_structure) > 1

419-426: Clarify expectations for unverified contracts; make assertion more robust.

Per tool design, metadata mode should succeed but expose no source files for unverified contracts. Assert emptiness without coupling to list type, and add a short note to document the behavior.

Apply this diff:

-    assert result.data.source_code_tree_structure == []
+    # For unverified contracts, the tool returns metadata with no source files.
+    assert not result.data.source_code_tree_structure

Also confirm that this aligns with the intended contract: no exception in metadata mode for unverified, and file-content mode should raise ValueError if attempted.


335-345: Optional: Add a tiny retry helper to reduce network-induced flakiness across new inspect tests.

To adhere to the integration test guidelines on resilience, a local helper with 2–3 attempts and short backoff can be applied uniformly to all new inspect_contract_code calls.

Add this helper near the top of the file:

import asyncio

async def _inspect_with_retries(*, attempts: int = 3, delay: float = 0.75, **kwargs):
    last_err = None
    for i in range(attempts):
        try:
            return await inspect_contract_code(**kwargs)
        except (aiohttp.ClientError, httpx.HTTPError, OSError, asyncio.TimeoutError) as e:
            last_err = e
        except RuntimeError as e:
            if "Chainscout" in str(e) or "Could not retrieve or parse data for chain ID" in str(e):
                last_err = e
            else:
                raise
        await asyncio.sleep(delay * (2**i))
    pytest.skip(f"Network/connectivity or aggregator issue after {attempts} attempts: {last_err}")

Then replace direct calls like:

result = await inspect_contract_code(chain_id=CHAIN_ID_MAINNET, address=address, ctx=mock_ctx)

with:

result = await _inspect_with_retries(chain_id=CHAIN_ID_MAINNET, address=address, ctx=mock_ctx)

Also applies to: 347-356, 358-367, 369-382, 384-393, 397-415, 419-426

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bd318a8 and b936957.

📒 Files selected for processing (1)
  • tests/integration/test_contract_tools_integration.py (5 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/000-role-and-task.mdc)

**/*.py: The MCP server must be implemented in Python, as you are a senior Python developer and the expertise is in Python.
The MCP server must wrap Blockscout APIs and expose blockchain data (balances, tokens, NFTs, contract metadata) via the Model Context Protocol (MCP).
The MCP server must communicate with AI agents/chat applications through stdin.

**/*.py: Regular Python modules should generally not exceed 500 lines of code (LOC). If a module approaches this limit, consider splitting it into multiple focused modules (e.g., address_tools.py and address_tools_advanced.py) to maintain readability and logical organization.
ALL import statements must be placed at the top of the Python module, immediately after the module docstring (if present) and before any other code. Never insert imports inline near where the functionality is used. Follow PEP 8 import order.
ALL linting and formatting issues must be resolved before committing or pushing code. Use the Ruff rules defined in 300-ruff-lint-and-format.mdc to identify and fix issues.

**/*.py: Always run ruff check . --fix and ruff format . on generated code before suggesting commits or opening a PR
Avoid using # noqa: E501 for ordinary code lines; split long lines instead. Only use # noqa: E501 for docstrings or string literals that must exceed 120 characters.
Use Ruff to enforce a 120-character line length, compatible with Black formatting

Files:

  • tests/integration/test_contract_tools_integration.py
tests/integration/test_*_integration.py

📄 CodeRabbit inference engine (.cursor/rules/200-development-testing-workflow.mdc)

Add integration tests for tool functions in tests/integration/test_{tool_module}_integration.py when adding new tools that interact with live APIs

tests/integration/test_*_integration.py: Tool-level integration tests targeting high-level MCP tool functions (e.g., get_latest_block, get_tokens_by_address) must be located in files matching tests/integration/test_*_integration.py and validate data extraction and schema against live API responses.
Integration tests for tools that return paginated data must use a loop with a max_pages_to_check limit to search across pages, and skip the test with a clear message if the target data is not found after the maximum number of pages.
Integration tests for tools supporting cursor-based pagination must perform a two-step test: first call without a cursor, extract the cursor, then call again with the cursor and assert the second page's data is different from the first.

Files:

  • tests/integration/test_contract_tools_integration.py
tests/integration/test_*.py

📄 CodeRabbit inference engine (.cursor/rules/200-development-testing-workflow.mdc)

Follow the guidelines in 220-integration-testing-guidelines.mdc when writing integration tests

Files:

  • tests/integration/test_contract_tools_integration.py
tests/integration/*.py

📄 CodeRabbit inference engine (.cursor/rules/220-integration-testing-guidelines.mdc)

tests/integration/*.py: All integration test functions must be decorated with @pytest.mark.integration.
Integration tests must use stable, non-volatile data points such as historical blocks, famous addresses, or established ENS names to ensure reliability across runs.
Integration tests should be resilient to temporary network issues by implementing retry logic and skipping the test with a clear message if connectivity cannot be established after several attempts.
Integration tests must not hardcode environment-specific URLs in assertions; instead, dynamically resolve URLs using the same helper functions as the code under test.
Integration test files must not exceed 500 lines of code (LOC); if a file approaches this limit, split tests into multiple focused files to maintain readability and logical organization.
Group related integration tests using descriptive class names or clear function naming patterns, and use descriptive test names indicating what contract or schema is being validated.
Separate helper-level integration tests from tool-level integration tests into different files when appropriate.
Include clear comments in integration tests explaining why specific test data was chosen, especially for stable targets.

Files:

  • tests/integration/test_contract_tools_integration.py
🧠 Learnings (5)
📓 Common learnings
Learnt from: akolotov
PR: blockscout/mcp-server#207
File: blockscout_mcp_server/models.py:104-109
Timestamp: 2025-08-21T23:17:44.832Z
Learning: In blockscout_mcp_server/models.py, the ContractSourceFile model for inspect_contract_code file mode responses was deliberately chosen over returning plain strings. The user akolotov prefers the structured type approach (ContractSourceFile with file_content field) for file mode responses in the inspect_contract_code functionality.
📚 Learning: 2025-07-22T00:14:04.976Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/220-integration-testing-guidelines.mdc:0-0
Timestamp: 2025-07-22T00:14:04.976Z
Learning: Applies to tests/integration/test_*_integration.py : Tool-level integration tests targeting high-level MCP tool functions (e.g., get_latest_block, get_tokens_by_address) must be located in files matching tests/integration/test_*_integration.py and validate data extraction and schema against live API responses.

Applied to files:

  • tests/integration/test_contract_tools_integration.py
📚 Learning: 2025-07-22T00:13:24.829Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/200-development-testing-workflow.mdc:0-0
Timestamp: 2025-07-22T00:13:24.829Z
Learning: Ensure your changes are well-tested by checking test coverage using pytest --cov=blockscout_mcp_server --cov-report=term-missing

Applied to files:

  • tests/integration/test_contract_tools_integration.py
📚 Learning: 2025-08-21T23:17:44.832Z
Learnt from: akolotov
PR: blockscout/mcp-server#207
File: blockscout_mcp_server/models.py:104-109
Timestamp: 2025-08-21T23:17:44.832Z
Learning: In blockscout_mcp_server/models.py, the ContractSourceFile model for inspect_contract_code file mode responses was deliberately chosen over returning plain strings. The user akolotov prefers the structured type approach (ContractSourceFile with file_content field) for file mode responses in the inspect_contract_code functionality.

Applied to files:

  • tests/integration/test_contract_tools_integration.py
📚 Learning: 2025-08-21T23:50:57.855Z
Learnt from: akolotov
PR: blockscout/mcp-server#207
File: blockscout_mcp_server/tools/contract_tools.py:79-91
Timestamp: 2025-08-21T23:50:57.855Z
Learning: In the Blockscout API, the `source_code` field in `/api/v2/smart-contracts/{address}` responses is guaranteed to be non-null for verified contracts. The `inspect_contract_code` tool in blockscout_mcp_server is specifically designed to work with verified contracts only.

Applied to files:

  • tests/integration/test_contract_tools_integration.py
🧬 Code graph analysis (1)
tests/integration/test_contract_tools_integration.py (2)
blockscout_mcp_server/tools/contract_tools.py (3)
  • get_contract_abi (170-212)
  • inspect_contract_code (119-166)
  • read_contract (243-375)
tests/conftest.py (1)
  • mock_ctx (8-13)
⏰ 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). (2)
  • GitHub Check: Docker build and docker push
  • GitHub Check: Run Integration Tests
🔇 Additional comments (4)
tests/integration/test_contract_tools_integration.py (4)

11-15: Importing the new tool is correct and consistent.

Bringing inspect_contract_code into the integration suite aligns the tests with the new API surface. No issues with import ordering or placement.


45-51: Switch to CHAIN_ID_MAINNET constant – LGTM.

Using the named constant improves clarity and reduces magic strings.


73-80: Consistent use of CHAIN_ID_MAINNET – LGTM.

No functional concerns; aligns with the rest of the suite.


118-125: Consistent use of CHAIN_ID_MAINNET – LGTM.

No functional concerns here.

@akolotov akolotov requested a review from Copilot August 22, 2025 00:28
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds a comprehensive contract code inspection feature to the MCP server, allowing users to explore verified smart contract source code and metadata with context-aware truncation and intelligent caching.

  • Implements inspect_contract_code tool with two-phase approach: metadata overview and selective file content retrieval
  • Adds in-memory LRU cache with TTL for contract data to improve performance across multiple queries
  • Exposes the new functionality through both MCP and REST API interfaces

Reviewed Changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
blockscout_mcp_server/tools/contract_tools.py Core implementation of contract inspection with caching and processing logic
blockscout_mcp_server/cache.py New ContractCache class for LRU/TTL caching of contract data
blockscout_mcp_server/models.py New ContractMetadata and ContractSourceFile models for structured responses
blockscout_mcp_server/tools/common.py Added constructor arguments truncation function
blockscout_mcp_server/api/routes.py REST API endpoint for contract inspection
blockscout_mcp_server/server.py MCP tool registration
tests/tools/test_contract_code_inspection.py Comprehensive unit tests for contract inspection logic
tests/test_cache.py Unit tests for contract cache functionality
tests/integration/test_contract_tools_integration.py Integration tests for various contract types

Copy link

@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: 0

🧹 Nitpick comments (1)
dxt/manifest-dev.json (1)

65-68: Clarify tool description to reflect structured file-mode response (ContractSourceFile).

Per retrieved learnings for PR #207, file mode returns a ContractSourceFile wrapper (with file_content), not a bare string. Update the description to avoid confusion and keep manifests consistent with the implemented API.

Apply:

-      "description": "Inspects a verified smart contract's source code, returning metadata or specific file content."
+      "description": "Inspects a verified smart contract's source code; returns metadata or a ContractSourceFile for a specific source file."

Note: This suggestion leverages the stored preference from akolotov that the structured ContractSourceFile response is the intended final design for file mode.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b936957 and bded8ea.

📒 Files selected for processing (2)
  • README.md (3 hunks)
  • dxt/manifest-dev.json (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: akolotov
PR: blockscout/mcp-server#207
File: API.md:0-0
Timestamp: 2025-08-22T00:01:57.070Z
Learning: In PR #207, after commit e021953, the user akolotov confirmed that the ContractSourceFile wrapper approach for inspect_contract_code file mode responses is the intended final design, making previous concerns about returning raw strings not applicable.
Learnt from: akolotov
PR: blockscout/mcp-server#207
File: blockscout_mcp_server/models.py:104-109
Timestamp: 2025-08-21T23:17:44.832Z
Learning: In blockscout_mcp_server/models.py, the ContractSourceFile model for inspect_contract_code file mode responses was deliberately chosen over returning plain strings. The user akolotov prefers the structured type approach (ContractSourceFile with file_content field) for file mode responses in the inspect_contract_code functionality.
📚 Learning: 2025-08-21T04:33:09.906Z
Learnt from: akolotov
PR: blockscout/mcp-server#207
File: dxt/manifest.json:5-5
Timestamp: 2025-08-21T04:33:09.906Z
Learning: In the blockscout/mcp-server repository, the DXT manifest version (dxt/manifest.json) operates independently from the MCP server version (pyproject.toml and blockscout_mcp_server/__init__.py). The manifest version should only be bumped when: (1) the connection method to the server changes, (2) the number of MCP tools changes, or (3) tool descriptions change. The MCP server version tracks implementation changes and follows its own versioning scheme.

Applied to files:

  • dxt/manifest-dev.json
📚 Learning: 2025-07-22T00:12:19.870Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/130-version-management.mdc:0-0
Timestamp: 2025-07-22T00:12:19.870Z
Learning: Applies to blockscout_mcp_server/__init__.py : When updating the version of the MCP server, update the `__version__` variable in `blockscout_mcp_server/__init__.py`.

Applied to files:

  • dxt/manifest-dev.json
📚 Learning: 2025-07-22T00:11:07.554Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/000-role-and-task.mdc:0-0
Timestamp: 2025-07-22T00:11:07.554Z
Learning: Applies to **/*.py : The MCP server must wrap Blockscout APIs and expose blockchain data (balances, tokens, NFTs, contract metadata) via the Model Context Protocol (MCP).

Applied to files:

  • dxt/manifest-dev.json
📚 Learning: 2025-07-22T00:12:19.870Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/130-version-management.mdc:0-0
Timestamp: 2025-07-22T00:12:19.870Z
Learning: Use the exact same version string in both `pyproject.toml` and `blockscout_mcp_server/__init__.py`.

Applied to files:

  • dxt/manifest-dev.json
📚 Learning: 2025-08-21T23:50:57.855Z
Learnt from: akolotov
PR: blockscout/mcp-server#207
File: blockscout_mcp_server/tools/contract_tools.py:79-91
Timestamp: 2025-08-21T23:50:57.855Z
Learning: In the Blockscout API, the `source_code` field in `/api/v2/smart-contracts/{address}` responses is guaranteed to be non-null for verified contracts. The `inspect_contract_code` tool in blockscout_mcp_server is specifically designed to work with verified contracts only.

Applied to files:

  • dxt/manifest-dev.json
🔇 Additional comments (1)
dxt/manifest-dev.json (1)

5-5: Manifest parity verified – ready to merge

All checks passed confirming the version bump and tool alignment:

  • Both dxt/manifest.json and dxt/manifest-dev.json are at version 0.4.0.
  • The inspect_contract_code tool appears in both manifests.
  • The ordering of tools matches exactly between dev and prod manifests.
  • The inspect_contract_code tool is registered in server.py and documented in SPEC.md, README.md, API.md, AGENTS.md, and llms.txt.

Copy link

@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: 0

♻️ Duplicate comments (1)
blockscout_mcp_server/tools/contract_tools.py (1)

41-47: Add progress logs for cache check/hit to improve UX and early ctx usage

Clients get no feedback when a cache hit returns early. Emit progress before/after the cache lookup.

     normalized_address = address.lower()
     cache_key = f"{chain_id}:{normalized_address}"
-    if cached := await contract_cache.get(cache_key):
-        return cached
+    await report_and_log_progress(
+        ctx,
+        progress=0.0,
+        total=3.0,
+        message=f"Inspecting {address} on chain {chain_id}: checking cache...",
+    )
+    if cached := await contract_cache.get(cache_key):
+        await report_and_log_progress(
+            ctx,
+            progress=3.0,
+            total=3.0,
+            message="Cache hit: using processed metadata.",
+        )
+        return cached
🧹 Nitpick comments (4)
blockscout_mcp_server/tools/contract_tools.py (4)

29-38: Stabilize file-path derivation with explicit language mapping and a safe fallback

Current logic treats non-"solidity" languages as Vyper by default. Use an explicit map and fall back to ".sol" for unknown languages to avoid accidental ".vy" for new/unknown types.

-    file_path = raw_data.get("file_path")
-    if not file_path or file_path == ".sol":
-        language = raw_data.get("language", "").lower()
-        if language == "solidity":
-            file_path = f"{raw_data.get('name', 'Contract')}.sol"
-        else:
-            file_path = f"{raw_data.get('name', 'Contract')}.vy"
+    file_path = raw_data.get("file_path")
+    if not file_path or file_path == ".sol":
+        language = (raw_data.get("language") or "").lower()
+        ext_map = {"solidity": "sol", "vyper": "vy"}
+        ext = ext_map.get(language, "sol")
+        file_path = f"{raw_data.get('name', 'Contract')}.{ext}"

79-91: Trim whitespace in additional source paths to avoid empty/duplicate keys

Minor hardening: strip whitespace on file_path before using it as a dict key. Prevents accidental "" keys and subtle duplicates.

-            for item in raw_data.get("additional_sources", []):
-                item_path = item.get("file_path")
-                if item_path:
-                    source_files[item_path] = item.get("source_code")
+            for item in raw_data.get("additional_sources", []):
+                item_path = (item.get("file_path") or "").strip()
+                if item_path:
+                    source_files[item_path] = item.get("source_code")

104-114: Make source_code_tree_structure deterministic

Sort file paths to ensure stable ordering for tests, caches, and clients.

-    metadata_copy["source_code_tree_structure"] = list(source_files.keys())
+    metadata_copy["source_code_tree_structure"] = sorted(source_files.keys())

121-169: User-friendly error: suggest closest filename on mismatch

When file_name is wrong, suggest the closest match to reduce retry loops.

-    if file_name not in processed.source_files:
-        available = ", ".join(processed.source_files.keys())
-        raise ValueError(
-            f"File '{file_name}' not found in the source code for this contract. Available files: {available}"
-        )
+    if file_name not in processed.source_files:
+        available_files = list(processed.source_files.keys())
+        match = get_close_matches(file_name, available_files, n=1)
+        hint = f" Did you mean '{match[0]}'?" if match else ""
+        available = ", ".join(available_files)
+        raise ValueError(
+            f"File '{file_name}' not found in the source code for this contract. Available files: {available}.{hint}"
+        )

Add this top-level import to comply with import rules:

from difflib import get_close_matches
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bded8ea and d4f5f59.

📒 Files selected for processing (1)
  • blockscout_mcp_server/tools/contract_tools.py (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/000-role-and-task.mdc)

**/*.py: The MCP server must be implemented in Python, as you are a senior Python developer and the expertise is in Python.
The MCP server must wrap Blockscout APIs and expose blockchain data (balances, tokens, NFTs, contract metadata) via the Model Context Protocol (MCP).
The MCP server must communicate with AI agents/chat applications through stdin.

**/*.py: Regular Python modules should generally not exceed 500 lines of code (LOC). If a module approaches this limit, consider splitting it into multiple focused modules (e.g., address_tools.py and address_tools_advanced.py) to maintain readability and logical organization.
ALL import statements must be placed at the top of the Python module, immediately after the module docstring (if present) and before any other code. Never insert imports inline near where the functionality is used. Follow PEP 8 import order.
ALL linting and formatting issues must be resolved before committing or pushing code. Use the Ruff rules defined in 300-ruff-lint-and-format.mdc to identify and fix issues.

**/*.py: Always run ruff check . --fix and ruff format . on generated code before suggesting commits or opening a PR
Avoid using # noqa: E501 for ordinary code lines; split long lines instead. Only use # noqa: E501 for docstrings or string literals that must exceed 120 characters.
Use Ruff to enforce a 120-character line length, compatible with Black formatting

Files:

  • blockscout_mcp_server/tools/contract_tools.py
blockscout_mcp_server/tools/*.py

📄 CodeRabbit inference engine (.cursor/rules/110-new-mcp-tool.mdc)

blockscout_mcp_server/tools/*.py: Create or modify a tool module file in blockscout_mcp_server/tools/ for each new tool, using @log_tool_invocation to decorate each tool function.
All tools MUST return a strongly-typed ToolResponse[YourDataModel] instead of generic ToolResponse[dict].
For tools that query Blockscout API, use get_blockscout_base_url for dynamic chain resolution and make_blockscout_request for API calls.
For tools that use fixed API endpoints (like BENS), use the appropriate request helper (e.g., make_bens_request) from tools/common.py.
All tools MUST return a standardized ToolResponse[YourDataModel] object using the build_tool_response helper.
For paginated tools, accept an optional cursor argument and use apply_cursor_to_params to handle incoming cursors.
For paginated tools, use create_items_pagination from tools/common.py to handle slicing and pagination in responses.
For paginated tools, include the exact notice 'SUPPORTS PAGINATION: If response includes 'pagination' field, use the provided next_call to get additional pages.' in the tool docstring.
When returning addresses from Blockscout API responses, simplify address objects to a single address string in the tool output.
Truncate large data fields (such as raw 'data' or deeply nested values) in tool responses to save LLM context, and add notes about truncation.
Recursively truncate long strings in nested data structures in tool responses, replacing them with a structured object to signal truncation and adding notes.
Always raise exceptions for error conditions (e.g., ValueError, RuntimeError, TimeoutError) instead of returning ToolResponse objects with error messages in notes.
Use the report_and_log_progress helper from tools/common.py for all progress reporting in tool functions, instead of calling ctx.report_progress directly.
When making multiple independent API calls in a tool, use asyncio.gather with return_exceptions=True for concurrent execution and proper error handling.

Files:

  • blockscout_mcp_server/tools/contract_tools.py
🧠 Learnings (10)
📓 Common learnings
Learnt from: akolotov
PR: blockscout/mcp-server#207
File: API.md:0-0
Timestamp: 2025-08-22T00:01:57.070Z
Learning: In PR #207, after commit e021953, the user akolotov confirmed that the ContractSourceFile wrapper approach for inspect_contract_code file mode responses is the intended final design, making previous concerns about returning raw strings not applicable.
Learnt from: akolotov
PR: blockscout/mcp-server#207
File: blockscout_mcp_server/models.py:104-109
Timestamp: 2025-08-21T23:17:44.832Z
Learning: In blockscout_mcp_server/models.py, the ContractSourceFile model for inspect_contract_code file mode responses was deliberately chosen over returning plain strings. The user akolotov prefers the structured type approach (ContractSourceFile with file_content field) for file mode responses in the inspect_contract_code functionality.
📚 Learning: 2025-08-21T23:17:44.832Z
Learnt from: akolotov
PR: blockscout/mcp-server#207
File: blockscout_mcp_server/models.py:104-109
Timestamp: 2025-08-21T23:17:44.832Z
Learning: In blockscout_mcp_server/models.py, the ContractSourceFile model for inspect_contract_code file mode responses was deliberately chosen over returning plain strings. The user akolotov prefers the structured type approach (ContractSourceFile with file_content field) for file mode responses in the inspect_contract_code functionality.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-08-21T23:50:57.855Z
Learnt from: akolotov
PR: blockscout/mcp-server#207
File: blockscout_mcp_server/tools/contract_tools.py:79-91
Timestamp: 2025-08-21T23:50:57.855Z
Learning: In the Blockscout API, the `source_code` field in `/api/v2/smart-contracts/{address}` responses is guaranteed to be non-null for verified contracts. The `inspect_contract_code` tool in blockscout_mcp_server is specifically designed to work with verified contracts only.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : Use the report_and_log_progress helper from tools/common.py for all progress reporting in tool functions, instead of calling ctx.report_progress directly.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-08-22T00:01:57.070Z
Learnt from: akolotov
PR: blockscout/mcp-server#207
File: API.md:0-0
Timestamp: 2025-08-22T00:01:57.070Z
Learning: In PR #207, after commit e021953, the user akolotov confirmed that the ContractSourceFile wrapper approach for inspect_contract_code file mode responses is the intended final design, making previous concerns about returning raw strings not applicable.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : All tools MUST return a strongly-typed ToolResponse[YourDataModel] instead of generic ToolResponse[dict].

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : All tools MUST return a standardized ToolResponse[YourDataModel] object using the build_tool_response helper.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-07-10T19:21:50.271Z
Learnt from: akolotov
PR: blockscout/mcp-server#142
File: blockscout_mcp_server/api/routes.py:242-258
Timestamp: 2025-07-10T19:21:50.271Z
Learning: The user akolotov prefers a unified tool registration approach where a single function registers tools in both MCP and REST API instead of having separate registration points in blockscout_mcp_server/server.py and blockscout_mcp_server/api/routes.py. This would eliminate duplication and reduce maintenance burden.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-08-21T23:05:53.563Z
Learnt from: akolotov
PR: blockscout/mcp-server#207
File: blockscout_mcp_server/models.py:78-85
Timestamp: 2025-08-21T23:05:53.563Z
Learning: ContractMetadata in blockscout_mcp_server/models.py correctly uses ConfigDict(extra="allow") because it models data from the external Blockscout API endpoint /api/v2/smart-contracts/{address}, which returns different field sets depending on contract language (Solidity vs Vyper) and other contract-specific factors.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : For tools that query Blockscout API, use get_blockscout_base_url for dynamic chain resolution and make_blockscout_request for API calls.

Applied to files:

  • blockscout_mcp_server/tools/contract_tools.py
🧬 Code graph analysis (1)
blockscout_mcp_server/tools/contract_tools.py (4)
blockscout_mcp_server/cache.py (3)
  • CachedContract (99-103)
  • get (35-40)
  • get (115-125)
blockscout_mcp_server/models.py (3)
  • ContractMetadata (75-103)
  • ContractSourceFile (107-110)
  • ToolResponse (337-362)
blockscout_mcp_server/tools/common.py (5)
  • _truncate_constructor_args (463-474)
  • get_blockscout_base_url (62-119)
  • report_and_log_progress (477-486)
  • make_blockscout_request (122-175)
  • build_tool_response (489-530)
blockscout_mcp_server/tools/decorators.py (1)
  • log_tool_invocation (13-49)
🪛 GitHub Actions: CI - Lint & Format
blockscout_mcp_server/tools/contract_tools.py

[error] 1-1: ruff format --check failed. 1 file would be reformatted (blockscout_mcp_server/tools/contract_tools.py). Run 'ruff format' to fix code style issues.

⏰ 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). (2)
  • GitHub Check: Docker build and docker push
  • GitHub Check: Run Integration Tests
🔇 Additional comments (5)
blockscout_mcp_server/tools/contract_tools.py (5)

10-17: Consistent, strongly-typed responses for file mode (ContractSourceFile) — good alignment with guidelines

Using ContractSourceFile for file mode keeps ToolResponse strongly-typed and matches the agreed design in this repo. No action needed.


49-63: Good progress reporting around network operations

Progress updates after resolving base URL and after fetch make the tool responsive and observable. Looks solid.


64-78: Sensible defaults for optional metadata fields

Using setdefault to ensure model-validate-safe inputs is appropriate and keeps ContractMetadata construction robust.


92-114: Copy-before-mutate resolves earlier side-effect concern

Processing/truncation on a copy avoids mutating the API payload and improves maintainability. Nicely done.


1-1: Formatting Applied – CI Formatting Checks Now Passing

The blockscout_mcp_server/tools/contract_tools.py file has been auto‐formatted with Ruff, and all lint and format checks now pass:

  • Ruff check with --fix reports “All checks passed!”
  • ruff format reformatted the file successfully, so CI’s formatting step should now succeed.

@akolotov akolotov merged commit 971ae2b into main Aug 22, 2025
7 of 8 checks passed
@akolotov akolotov deleted the codex/implement-inspect_contract_code-tool branch August 22, 2025 01:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add inspect_contract_code tool for smart contract source code analysis
1 participant