Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ BLOCKSCOUT_CHAIN_CACHE_TTL_SECONDS=1800
BLOCKSCOUT_CHAINS_LIST_TTL_SECONDS=300
BLOCKSCOUT_PROGRESS_INTERVAL_SECONDS="15.0"

# Contracts Cache
BLOCKSCOUT_CONTRACTS_CACHE_MAX_NUMBER=10
BLOCKSCOUT_CONTRACTS_CACHE_TTL_SECONDS=3600

BLOCKSCOUT_BS_REQUEST_MAX_RETRIES="3"

# The number of items to return per page for the nft_tokens_by_address tool.
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ mcp-server/
│ ├── initialization_tools.py # Implements the __unlock_blockchain_analysis__ tool
│ ├── ens_tools.py # Implements ENS-related tools
│ ├── search_tools.py # Implements search-related tools (e.g., lookup_token_by_symbol)
│ ├── contract_tools.py # Implements contract-related tools (e.g., get_contract_abi)
│ ├── contract_tools.py # Implements contract-related tools (e.g., get_contract_abi, inspect_contract_code)
│ ├── address_tools.py # Implements address-related tools (e.g., get_address_info, get_tokens_by_address)
│ ├── block_tools.py # Implements block-related tools (e.g., get_latest_block, get_block_info)
│ ├── transaction_tools.py# Implements transaction-related tools (e.g., get_transactions_by_address, get_transaction_info)
Expand Down Expand Up @@ -255,7 +255,7 @@ mcp-server/
* `chains_tools.py`: Implements `get_chains_list`, returning a formatted list of blockchain chains with their IDs.
* `ens_tools.py`: Implements `get_address_by_ens_name` (fixed BENS endpoint, no chain_id).
* `search_tools.py`: Implements `lookup_token_by_symbol(chain_id, symbol)`.
* `contract_tools.py`: Implements `get_contract_abi(chain_id, address)`.
* `contract_tools.py`: Implements `get_contract_abi(chain_id, address)` and `inspect_contract_code(chain_id, address, file_name=None)`.
* `address_tools.py`: Implements `get_address_info(chain_id, address)` (includes public tags), `get_tokens_by_address(chain_id, address, cursor=None)`, `nft_tokens_by_address(chain_id, address, cursor=None)` with robust, cursor-based pagination.
* `block_tools.py`: Implements `get_block_info(chain_id, number_or_hash, include_transactions=False)`, `get_latest_block(chain_id)`.
* `transaction_tools.py`: Implements `get_transactions_by_address(chain_id, address, age_from, age_to, methods, cursor=None)`, `get_token_transfers_by_address(chain_id, address, age_from, age_to, token, cursor=None)`, `get_transaction_info(chain_id, hash, include_raw_input=False)`, `transaction_summary(chain_id, hash)`, `get_transaction_logs(chain_id, hash, cursor=None)`, etc.
20 changes: 20 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,26 @@ Retrieves the Application Binary Interface (ABI) for a smart contract.
curl "http://127.0.0.1:8000/v1/get_contract_abi?chain_id=1&address=0x..."
```

#### 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. |

**Example Request**

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

#### Get Address by ENS Name (`get_address_by_ens_name`)

Converts an ENS (Ethereum Name Service) name to its corresponding Ethereum address.
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ ENV BLOCKSCOUT_CHAINSCOUT_TIMEOUT="15.0"
ENV BLOCKSCOUT_CHAIN_CACHE_TTL_SECONDS="1800"
ENV BLOCKSCOUT_CHAINS_LIST_TTL_SECONDS="300"
ENV BLOCKSCOUT_PROGRESS_INTERVAL_SECONDS="15.0"
ENV BLOCKSCOUT_CONTRACTS_CACHE_MAX_NUMBER="10"
ENV BLOCKSCOUT_CONTRACTS_CACHE_TTL_SECONDS="3600"
ENV BLOCKSCOUT_NFT_PAGE_SIZE="10"
ENV BLOCKSCOUT_LOGS_PAGE_SIZE="10"
ENV BLOCKSCOUT_ADVANCED_FILTERS_PAGE_SIZE="10"
Expand Down
30 changes: 18 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ The official cloud-hosted instance at `https://mcp.blockscout.com/mcp/` provides

**Claude Desktop Setup:**

_Note: Docker is required for this setup_
> _Note: Docker is required for this setup_

1. Open Claude Desktop and click on Settings
2. Navigate to the "Developer" section
Expand Down Expand Up @@ -129,17 +129,18 @@ Refer to [TESTING.md](TESTING.md) for comprehensive instructions on running both
3. `get_address_by_ens_name(name)` - Converts an ENS domain name to its corresponding Ethereum address.
4. `lookup_token_by_symbol(chain_id, symbol)` - Searches for token addresses by symbol or name, returning multiple potential matches.
5. `get_contract_abi(chain_id, address)` - Retrieves the ABI (Application Binary Interface) for a smart contract.
6. `get_address_info(chain_id, address)` - Gets comprehensive information about an address including balance, ENS association, contract status, token details, and public tags.
7. `get_tokens_by_address(chain_id, address, cursor=None)` - Returns detailed ERC20 token holdings for an address with enriched metadata and market data.
8. `get_latest_block(chain_id)` - Returns the latest indexed block number and timestamp.
9. `get_transactions_by_address(chain_id, address, age_from, age_to, methods, cursor=None)` - Gets transactions for an address within a specific time range with optional method filtering.
10. `get_token_transfers_by_address(chain_id, address, age_from, age_to, token, cursor=None)` - Returns ERC-20 token transfers for an address within a specific time range.
11. `transaction_summary(chain_id, hash)` - Provides human-readable transaction summaries using Blockscout Transaction Interpreter.
12. `nft_tokens_by_address(chain_id, address, cursor=None)` - Retrieves NFT tokens owned by an address, grouped by collection.
13. `get_block_info(chain_id, number_or_hash, include_transactions=False)` - Returns block information including timestamp, gas used, burnt fees, and transaction count. Can optionally include a list of transaction hashes.
14. `get_transaction_info(chain_id, hash, include_raw_input=False)` - Gets comprehensive transaction information with decoded input parameters and detailed token transfers.
15. `get_transaction_logs(chain_id, hash, cursor=None)` - Returns transaction logs with decoded event data.
16. `read_contract(chain_id, address, abi, function_name, args=None, block='latest')` - Executes a read-only smart contract function and returns its result. The `abi` argument is a JSON object describing the specific function's signature.
6. `inspect_contract_code(chain_id, address, file_name=None)` - Allows getting the source files of verified contracts.
7. `get_address_info(chain_id, address)` - Gets comprehensive information about an address including balance, ENS association, contract status, token details, and public tags.
8. `get_tokens_by_address(chain_id, address, cursor=None)` - Returns detailed ERC20 token holdings for an address with enriched metadata and market data.
9. `get_latest_block(chain_id)` - Returns the latest indexed block number and timestamp.
10. `get_transactions_by_address(chain_id, address, age_from, age_to, methods, cursor=None)` - Gets transactions for an address within a specific time range with optional method filtering.
11. `get_token_transfers_by_address(chain_id, address, age_from, age_to, token, cursor=None)` - Returns ERC-20 token transfers for an address within a specific time range.
12. `transaction_summary(chain_id, hash)` - Provides human-readable transaction summaries using Blockscout Transaction Interpreter.
13. `nft_tokens_by_address(chain_id, address, cursor=None)` - Retrieves NFT tokens owned by an address, grouped by collection.
14. `get_block_info(chain_id, number_or_hash, include_transactions=False)` - Returns block information including timestamp, gas used, burnt fees, and transaction count. Can optionally include a list of transaction hashes.
15. `get_transaction_info(chain_id, hash, include_raw_input=False)` - Gets comprehensive transaction information with decoded input parameters and detailed token transfers.
16. `get_transaction_logs(chain_id, hash, cursor=None)` - Returns transaction logs with decoded event data.
17. `read_contract(chain_id, address, abi, function_name, args=None, block='latest')` - Executes a read-only smart contract function and returns its result. The `abi` argument is a JSON object describing the specific function's signature.

## Example Prompts for AI Agents

Expand Down Expand Up @@ -175,6 +176,11 @@ When the most recent reward distribution of Kinto token was made to the wallet
`0x7D467D99028199D99B1c91850C4dea0c82aDDF52` in Kinto chain?
```

```plaintext
Which methods of `0x1c479675ad559DC151F6Ec7ed3FbF8ceE79582B6` on the Ethereum
mainnet could emit `SequencerBatchDelivered`?
```

## Development & Deployment

### Local Installation
Expand Down
16 changes: 16 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,22 @@ This architecture provides the flexibility of a multi-protocol server without th
- **`decoded_input` Truncation**: The server recursively traverses the nested `parameters` of the decoded input. Any string value (e.g., a `bytes` or `string` parameter) exceeding the limit is replaced by a structured object: `{"value_sample": "...", "value_truncated": true}`. This preserves the overall structure of the decoded call while saving significant context.
- **Instructional Note**: If any field is truncated, a note is appended to the tool's output, providing a `curl` command to retrieve the complete, untruncated data, ensuring the agent has a path to the full information if needed.

**g) Contract Source Code and ABI Separation:**

To prevent LLM context overflow when exploring smart contracts, the server implements a strategic separation between ABI retrieval and source code inspection through dedicated tools with optimized access patterns.

- **Separate ABI Tool**: The `get_contract_abi` tool provides only the contract's ABI without source code, as ABI information alone is sufficient for most contract interaction scenarios. This avoids the significant context consumption that would result from combining ABI with potentially large source code in a single response.

- **Two-Phase Source Code Inspection**: The `inspect_contract_code` tool uses a deliberate two-phase approach for source exploration:
- **Phase 1 (Metadata Overview)**: When called without a specific `file_name`, the tool returns contract metadata (excluding ABI to avoid duplication) and a structured source file tree. This gives the LLM a complete overview of the contract's file organization without consuming excessive context.
- **Phase 2 (Selective File Reading)**: The LLM can then make targeted requests for specific files of interest (e.g., main contract logic) while potentially skipping standard interfaces (e.g., ERC20 implementations) that don't require inspection.

- **Constructor Arguments Truncation**: When constructor arguments in metadata exceed size limits, they are truncated using the same strategy as described in "Transaction Input Data Truncation".

- **Smart File Naming**: For single-file contracts (including flattened contracts), the server ensures a consistent file tree structure. When metadata doesn't provide a file name (common in Solidity contracts), the server constructs one using the pattern `<contract_name>.sol` for Solidity. For Vyper contracts, the file name is usually specified in the metadata.

- **Response Caching**: Since contract source exploration often involves multiple sequential requests for the same contract, the server implements in-memory caching of Blockscout API responses with LRU eviction and TTL expiry. This minimizes redundant API calls and improves response times for multi-file contract inspection workflows.

7. **HTTP Request Robustness**

Blockscout HTTP requests are centralized via the helper `make_blockscout_request`. To improve resilience against transient, transport-level issues observed in real-world usage (for example, incomplete chunked reads), the helper employs a small and conservative retry policy:
Expand Down
2 changes: 1 addition & 1 deletion blockscout_mcp_server/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Blockscout MCP Server package."""

__version__ = "0.8.0-dev"
__version__ = "0.9.0-dev"
15 changes: 14 additions & 1 deletion blockscout_mcp_server/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
)
from blockscout_mcp_server.tools.block_tools import get_block_info, get_latest_block
from blockscout_mcp_server.tools.chains_tools import get_chains_list
from blockscout_mcp_server.tools.contract_tools import get_contract_abi, read_contract
from blockscout_mcp_server.tools.contract_tools import (
get_contract_abi,
inspect_contract_code,
read_contract,
)
from blockscout_mcp_server.tools.ens_tools import get_address_by_ens_name
from blockscout_mcp_server.tools.initialization_tools import __unlock_blockchain_analysis__
from blockscout_mcp_server.tools.search_tools import lookup_token_by_symbol
Expand Down Expand Up @@ -160,6 +164,14 @@ async def get_contract_abi_rest(request: Request) -> Response:
return JSONResponse(tool_response.model_dump())


@handle_rest_errors
async def inspect_contract_code_rest(request: Request) -> Response:
"""REST wrapper for the inspect_contract_code tool."""
params = extract_and_validate_params(request, required=["chain_id", "address"], optional=["file_name"])
tool_response = await inspect_contract_code(**params, ctx=get_mock_context(request))
return JSONResponse(tool_response.model_dump())


@handle_rest_errors
async def read_contract_rest(request: Request) -> Response:
"""REST wrapper for the read_contract tool."""
Expand Down Expand Up @@ -284,6 +296,7 @@ async def list_tools_rest(_: Request) -> Response:
_add_v1_tool_route(mcp, "/get_token_transfers_by_address", get_token_transfers_by_address_rest)
_add_v1_tool_route(mcp, "/lookup_token_by_symbol", lookup_token_by_symbol_rest)
_add_v1_tool_route(mcp, "/get_contract_abi", get_contract_abi_rest)
_add_v1_tool_route(mcp, "/inspect_contract_code", inspect_contract_code_rest)
_add_v1_tool_route(mcp, "/read_contract", read_contract_rest)
_add_v1_tool_route(mcp, "/get_address_info", get_address_info_rest)
_add_v1_tool_route(mcp, "/get_tokens_by_address", get_tokens_by_address_rest)
Expand Down
48 changes: 46 additions & 2 deletions blockscout_mcp_server/cache.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""Simple in-memory cache for chain metadata."""

import time
from collections import OrderedDict

import anyio
from pydantic import BaseModel, Field

from blockscout_mcp_server.config import config
from blockscout_mcp_server.models import ChainInfo
Expand Down Expand Up @@ -80,7 +82,7 @@ def __init__(self) -> None:

def get_if_fresh(self) -> list[ChainInfo] | None:
"""Return cached chains if the snapshot is still fresh."""
if self.chains_snapshot is None or time.time() >= self.expiry_timestamp:
if self.chains_snapshot is None or time.monotonic() >= self.expiry_timestamp:
return None
return self.chains_snapshot

Expand All @@ -91,4 +93,46 @@ def needs_refresh(self) -> bool:
def store_snapshot(self, chains: list[ChainInfo]) -> None:
"""Store a fresh snapshot and compute its expiry timestamp."""
self.chains_snapshot = chains
self.expiry_timestamp = time.time() + config.chains_list_ttl_seconds
self.expiry_timestamp = time.monotonic() + config.chains_list_ttl_seconds


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.")


class ContractCache:
"""In-process, thread-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

async def get(self, key: str) -> CachedContract | None:
"""Retrieve an entry from the cache if it exists and is fresh."""
async with self._lock:
if key not in self._cache:
return None
contract_data, expiry_timestamp = self._cache[key]
if time.monotonic() >= expiry_timestamp:
self._cache.pop(key)
return None
self._cache.move_to_end(key)
return contract_data

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.monotonic() + self._ttl
self._cache[key] = (value, expiry_timestamp)
self._cache.move_to_end(key)
if len(self._cache) > self._max_size:
self._cache.popitem(last=False)


# Global singleton instance for the contract cache
contract_cache = ContractCache()
3 changes: 3 additions & 0 deletions blockscout_mcp_server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ class ServerConfig(BaseSettings):
chains_list_ttl_seconds: int = 300 # Default 5 minutes
progress_interval_seconds: float = 15.0 # Default interval for periodic progress updates

contracts_cache_max_number: int = 10 # Default 10 contracts
contracts_cache_ttl_seconds: int = 3600 # Default 1 hour

nft_page_size: int = 10
logs_page_size: int = 10
advanced_filters_page_size: int = 10
Expand Down
23 changes: 12 additions & 11 deletions blockscout_mcp_server/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,18 @@ All tools are available via both MCP and REST API interfaces:
3. **`get_address_by_ens_name`** - Converts an ENS name to its Ethereum address
4. **`lookup_token_by_symbol`** - Searches for tokens by symbol or name
5. **`get_contract_abi`** - Retrieves the ABI for a smart contract
6. **`get_address_info`** - Gets comprehensive information about an address
7. **`get_tokens_by_address`** - Returns ERC20 token holdings for an address
8. **`get_latest_block`** - Returns the latest indexed block number and timestamp
9. **`get_transactions_by_address`** - Gets transactions for an address with time range filtering
10. **`get_token_transfers_by_address`** - Returns ERC-20 token transfers for an address
11. **`transaction_summary`** - Provides human-readable transaction summaries
12. **`nft_tokens_by_address`** - Retrieves NFT tokens owned by an address
13. **`get_block_info`** - Returns detailed block information
14. **`get_transaction_info`** - Gets comprehensive transaction information
15. **`get_transaction_logs`** - Returns transaction logs with decoded event data
16. **`read_contract`** - Executes a read-only smart contract function
6. **`inspect_contract_code`** - Inspects a verified contract's source code
7. **`get_address_info`** - Gets comprehensive information about an address
8. **`get_tokens_by_address`** - Returns ERC20 token holdings for an address
9. **`get_latest_block`** - Returns the latest indexed block number and timestamp
10. **`get_transactions_by_address`** - Gets transactions for an address with time range filtering
11. **`get_token_transfers_by_address`** - Returns ERC-20 token transfers for an address
12. **`transaction_summary`** - Provides human-readable transaction summaries
13. **`nft_tokens_by_address`** - Retrieves NFT tokens owned by an address
14. **`get_block_info`** - Returns detailed block information
15. **`get_transaction_info`** - Gets comprehensive transaction information
16. **`get_transaction_logs`** - Returns transaction logs with decoded event data
17. **`read_contract`** - Executes a read-only smart contract function

## When to Use Each Interface

Expand Down
Loading