-
Notifications
You must be signed in to change notification settings - Fork 9
feat: add contract code inspection tool #207
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Note Other AI code review bot(s) detectedCodeRabbit 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 failedThe pull request is closed. WalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
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 detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (1)
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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, + }, Trueblockscout_mcp_server/llms.txt (1)
46-57
: Alignllms.txt
withAPI.md
ordering and add REST path forinspect_contract_code
I ran a quick comparison between the tool list in
blockscout_mcp_server/llms.txt
and the entries inAPI.md
and noticed a few drifts that could lead to confusion:
- The
inspect_contract_code
entry inllms.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 inAPI.md
. In particular:
get_address_by_ens_name
andlookup_token_by_symbol
appear much earlier inllms.txt
than inAPI.md
.- Core contract endpoints in
API.md
(e.g.get_contract_abi
,inspect_contract_code
, thenget_address_by_ens_name
) are interleaved with address- and token-related tools in the LLM list.- There’s a new
read_contract
entry inllms.txt
that isn’t present inAPI.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 inAPI.md
, and update theinspect_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 codeOptional: 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 newinspect_contract_code
testsThe test files for
inspect_contract_code
have been added, so we should document them alongside the existingget_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
: whenfile_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 changesContractCache uses
time.time()
for TTL; ChainCache usestime.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. Sinceanyio
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 accessedblockscout_mcp_server/server.py (1)
96-96
: Tool registration added; consider centralizing registration to avoid duplicationRegistration 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; usevalidation_alias
and add boundsPydantic v2’s BaseSettings uses
validation_alias
(oralias
) to bind environment variables, and anyenv_prefix
is ignored when an alias is set. By specifyingcontracts_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 toenv=
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 descriptionTo 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 resilientScanner 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 insteadExact 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 contractAdd 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 contentEnsure 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 errorConsider 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 failuresMultiple 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_idYou 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 clarityUse 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-safeanyio.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 itemsBefore 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 upstreamSince 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 progressTiny 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 filesStrengthen 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 resolutionYou 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 adoptedIf 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 behaviorGiven _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.metadataAlso applies to: 126-159
blockscout_mcp_server/tools/contract_tools.py (5)
28-30
: Unusedctx
parameter in private helper.
_fetch_and_process_contract(..., ctx: Context)
doesn’t usectx
, 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 whensource_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 forsource_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
There was a problem hiding this 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 missingfile_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 == sblockscout_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.
📒 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 runruff check . --fix
andruff 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 themock_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.
There was a problem hiding this 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 usestime.monotonic
; notime.time
calls remain.Minor optional nitpick—your
fake_time()
helpers currently returnint
, whereastime.monotonic()
returnsfloat
. To mirror the real API and avoid any subtle type-checking wrinkles, you can adjust each test’sfake_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.
📒 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 runruff check . --fix
andruff 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 themock_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)
There was a problem hiding this 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 exceptionsThe 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 eAlternative (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 filesCurrent 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 UXOptional, 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 diffsSort 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 exceptionPer 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 stepWhen 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 retriesOn 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 safeDeep 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 mismatchesYou 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.
📒 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 runruff check . --fix
andruff 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 resolutionThis is aligned with our helpers usage and provides user-visible milestones. No issues.
51-64
: Sane defaults for missing metadata fieldsDefaulting commonly missing fields to None avoids KeyErrors downstream and stabilizes the model. Looks good.
112-114
: LRU+TTL cache population is correctCaching the pre-processed bundle (metadata + files) matches the design and avoids re-work. Good.
157-201
: ABI fetch flow is clean and idiomaticGood use of base URL resolution, progress reporting, and minimal post-processing into a typed model.
10-23
: Imports are correctly grouped and use project helpersPEP 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 fromweb3.utils.abi
and its signature (abi_element, *args, abi_codec=None, **kwargs
) matches our usage (web3py.readthedocs.io). No changes needed.
There was a problem hiding this 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 guidelinesPer guidelines, tests in
tests/tools/*
should verify progress reporting. This case makes two sequential calls, soreport_progress
andinfo
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 reportingGreat 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 pathA 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 pathOn 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 guaranteesYou already assert call counts. Add argument assertions to ensure URL map correctness passed to
chain_cache.bulk_set
(e.g.,{"1": "https://eth"}
) andapi_path="/api/chains"
for the request. If you’d like, I can propose the exactassert_has_awaits
pattern usingunittest.mock.call
and update imports accordingly.
32-32
: Optional: standardize AsyncMock assertions across testsSome places use
assert_awaited_once()
while others useawait_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.
📒 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 runruff check . --fix
andruff 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 themock_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 implementationPatching
time.monotonic
insideblockscout_mcp_server.cache
and setting cachedexpiry_timestamp
viatime.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 solidPre-seeding the snapshot and future
expiry_timestamp
withtime.monotonic()
is correct and ensures no network call. Assertions on no request and double progress logging are appropriate.
There was a problem hiding this 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 sureruff 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 contractYou 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 lookupMinor 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.
📒 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 runruff check . --fix
andruff 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 themock_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 a
ContractSourceFileobject whose
file_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 existsAfter 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 forget_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) viamcp.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.
There was a problem hiding this 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 checksUse 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) usagesApply 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_nameMinor 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 surprisesWhile 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 modeIf 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 descriptionBe 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 modeDocs 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 resolutionYour 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 accordinglyOnly 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.datatests/tools/test_contract_code_inspection.py (7)
6-11
: Import hygiene: remove unused MagicMock if not used after changesIf 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 stringIf 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 branchConsider 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 argumentsGuidelines 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 mappingNice 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 testsIntegration 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 modeThe 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.
📒 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 runruff check . --fix
andruff 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 themock_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 a
ContractSourceFileobject whose
file_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 objectPer 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
inblockscout_mcp_server/models.py
defines only thefile_content
field and does not includecontent
orfile_name
(lines 105–108). The call incontract_tools.py:166
aligns with this model, so no changes are needed.
There was a problem hiding this 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 fieldsHarden 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 typesSpell 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 MD036Use 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 languagesGuard 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 observabilityOn 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 logYou 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 metadataSort 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 usabilitySort 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 returnsContractSourceFile
withfile_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 modeDemonstrate 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.
📒 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 runruff check . --fix
andruff 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 consistentImports 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/testsThe 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 reflectContractSourceFile.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 returnReturning
ContractSourceFile(file_content=...)
keeps responses typed and extensible. This aligns with the project’s ToolResponse pattern.
There was a problem hiding this 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
andCHAIN_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) orasyncio.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}") + raiseAdditionally, 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_structureAlso 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.
📒 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 runruff check . --fix
andruff 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.
There was a problem hiding this 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 |
There was a problem hiding this 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.
📒 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 mergeAll 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.
There was a problem hiding this 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 usageClients 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 fallbackCurrent 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 keysMinor 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 deterministicSort 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 mismatchWhen 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.
📒 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 runruff check . --fix
andruff 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 guidelinesUsing 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 operationsProgress updates after resolving base URL and after fetch make the tool responsive and observable. Looks solid.
64-78
: Sensible defaults for optional metadata fieldsUsing setdefault to ensure model-validate-safe inputs is appropriate and keeps ContractMetadata construction robust.
92-114
: Copy-before-mutate resolves earlier side-effect concernProcessing/truncation on a copy avoids mutating the API payload and improves maintainability. Nicely done.
1-1
: Formatting Applied – CI Formatting Checks Now PassingThe
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.
Summary
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
Configuration
Documentation
Tests