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 @@ -28,6 +28,10 @@ BLOCKSCOUT_PRO_API_CONFIG_TTL_SECONDS=300
# * read_contract — eth_call is routed through the PRO API JSON-RPC gateway, which requires the key; when unset, read_contract fails fast and makes no network call.
# Generate one at https://dev.blockscout.com.
BLOCKSCOUT_PRO_API_KEY=""
# Name of the request header an MCP client uses to supply its own PRO API key (MCP tools over HTTP only).
# When a client sends this header, its key takes precedence over BLOCKSCOUT_PRO_API_KEY for that request; absent it, the server key is used.
# A malformed client-supplied key fails any PRO API request that would use it, with no fallback; tools that don't use the PRO API are unaffected. Set to an empty string to disable client-supplied keys entirely.
BLOCKSCOUT_PRO_API_KEY_HEADER="Blockscout-MCP-Pro-Api-Key"
BLOCKSCOUT_CHAINS_LIST_TTL_SECONDS=300
BLOCKSCOUT_PROGRESS_INTERVAL_SECONDS="15.0"

Expand Down
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ mcp-server/
│ ├── analytics.py # Centralized Mixpanel analytics for tool invocations (HTTP mode only)
│ ├── telemetry.py # Fire-and-forget community telemetry reporting
│ ├── client_meta.py # Shared client metadata extraction helpers and defaults
│ ├── pro_api_key_context.py # Request-scoped client-supplied PRO API key state, resolver, and @pro_api_key_scope decorator
│ ├── cache.py # Simple in-memory cache for chain data
│ ├── web3_pool.py # Async Web3 connection pool manager
│ ├── models.py # Defines standardized Pydantic models for all tool responses
Expand Down Expand Up @@ -131,6 +132,7 @@ mcp-server/
│ ├── test_analytics_source.py # Unit tests for analytics source detection
│ ├── test_cache.py # Unit tests for cache behavior
│ ├── test_client_meta.py # Unit tests for client metadata extraction
│ ├── test_pro_api_key_context.py # Unit tests for client-supplied PRO API key resolution
│ ├── test_hatch_build.py # Unit tests for custom Hatch build hook helpers
│ ├── test_instructions_data.py # Unit tests for the InstructionsData payload model
│ ├── test_integration_helpers.py # Unit tests for integration test helpers
Expand Down Expand Up @@ -360,6 +362,7 @@ mcp-server/
* Provides a singleton configuration object that can be imported and used by other modules, especially by `tools/common.py` for API calls.
* `mcp_allowed_hosts: str`: Comma-separated list of allowed `Host` header values for DNS rebinding protection (default: empty, auto-detected based on bind host).
* `mcp_allowed_origins: str`: Comma-separated list of allowed `Origin` header values for DNS rebinding protection (default: empty, auto-detected based on bind host).
* `pro_api_key_header: str`: Name of the request header an MCP client uses to supply its own Blockscout PRO API key (default: `Blockscout-MCP-Pro-Api-Key`; empty string disables the feature).
* **`constants.py`**:
* Defines centralized constants used throughout the application, including data truncation limits.
* Ensures consistency between different parts of the application.
Expand All @@ -381,6 +384,10 @@ mcp-server/
* Provides `ClientMeta` dataclass and `extract_client_meta_from_ctx()` function.
* Falls back to User-Agent header when MCP client name is unavailable.
* Ensures consistent sentinel defaults ("N/A", "Unknown") across logging and analytics modules.
* **`pro_api_key_context.py`**:
* Owns request-scoped resolution of a client-supplied Blockscout PRO API key, kept separate from logging/observability.
* Provides a `ContextVar` of the per-request client-key state, a normalization/validation helper, `extract_client_pro_api_key_from_ctx()`, `resolve_pro_api_key()` (precedence: valid client key → server key → not-configured error; malformed client key → terminal error, no fallback), and the `@pro_api_key_scope` decorator.
* Honored only for genuine MCP calls (ignored when `ctx.call_source == "rest"`); the key is never logged or placed in cache keys.
* **`cache.py`**:
* Encapsulates in-memory caching of chain data with TTL management.
* **`web3_pool.py`**:
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
ENV BLOCKSCOUT_DISABLE_COMMUNITY_TELEMETRY="false"
ENV BLOCKSCOUT_INTERMEDIARY_HEADER="Blockscout-MCP-Intermediary"
ENV BLOCKSCOUT_INTERMEDIARY_ALLOWLIST="ClaudeDesktop,HigressPlugin,EvaluationSuite"
ENV BLOCKSCOUT_PRO_API_KEY_HEADER="Blockscout-MCP-Pro-Api-Key"

Check warning on line 52 in Dockerfile

View workflow job for this annotation

GitHub Actions / Docker build and docker push

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "BLOCKSCOUT_PRO_API_KEY_HEADER") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

# Set the default transport mode. Can be overridden at runtime with -e.
# Options: "stdio" (default), "http"
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ To obtain one, create an account at https://dev.blockscout.com (the free tier do
export BLOCKSCOUT_PRO_API_KEY=proapi_your_key_here
```

**Client-supplied keys (HTTP MCP only).** When the server runs in HTTP mode, an MCP client can supply its own PRO API key in a request header — by default `Blockscout-MCP-Pro-Api-Key`, configurable via `BLOCKSCOUT_PRO_API_KEY_HEADER` (set it to an empty string to disable client-supplied keys entirely). A client-supplied key takes precedence over `BLOCKSCOUT_PRO_API_KEY` for that request; if the client sends no key, the server falls back to its own configured key; if neither is present, the request fails with the not-configured error. A client key that is present but malformed fails any request that needs the PRO API with no fallback (the server never silently uses its own key in place of a bad client key); tools that don't use the PRO API are unaffected. This makes it possible to run a shared HTTP server where each client authenticates with its own key. The client-key header is honored only for genuine MCP tool calls — the REST API ignores it and always authenticates with the server's configured key.

### Running the Server

The server runs in `stdio` mode by default:
Expand Down
16 changes: 14 additions & 2 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,17 +193,26 @@ This architecture provides the flexibility of a multi-protocol server without th

All Blockscout data flows through the authenticated Blockscout PRO API gateway. Authentication and the request identity (`User-Agent`) are centralized here rather than scattered across individual tools.

The key's purpose is to ensure every request the server makes to the Blockscout API is authorized — not to act as an access-control gate on MCP functionality itself. An MCP response is therefore gated only insofar as it requires a fresh, authorized upstream request: when no such request is made (for example, a cache hit), there is nothing to authorize. This principle explains several behaviors documented below, including what does not require the key and why cached data may be served without validating a client-supplied key upstream.

**Credential**

- The single credential is the `BLOCKSCOUT_PRO_API_KEY` environment variable (`config.pro_api_key`, empty by default). When set, it is sent as an `Authorization: Bearer <key>` header on every PRO API request.
- The header is built and attached per request inside the request helpers, never configured on a shared HTTP client, so the key is never sent to other upstreams (BENS, Chainscout). A bare `Bearer` token is never emitted: the `Authorization` header is added only when the key is non-empty.

**Client-supplied credential (MCP tools over HTTP only)**

- In addition to the server-side key, an MCP client may supply its own PRO API key in a dedicated request header whose name is configured by `BLOCKSCOUT_PRO_API_KEY_HEADER` (`config.pro_api_key_header`, default `Blockscout-MCP-Pro-Api-Key`). Setting this config to an empty string disables the feature.
- Resolution is pure precedence with **no fallback on a bad client key**: a valid client-supplied key is used for that request; if the client supplies no key, the server-side key is used; if neither exists, the request fails with the not-configured error. A client key that is present but malformed (control characters, or over the length bound) is a terminal error for any PRO-authenticated request that would consume it — the server never silently falls back to its own key for a malformed client key. Tools that never call the PRO API (for example `get_chains_list` or ENS lookups) are unaffected: the malformed state is recorded for the invocation but only the PRO API request helpers consult it.
- The credential is resolved per request and scoped to a single tool invocation (see `blockscout_mcp_server/pro_api_key_context.py`). The client key is read only for genuine MCP calls (never in REST mode) and is never written to logs, analytics, or cache keys.
- Because the key authorizes upstream requests rather than gating MCP functionality, a response served entirely from cache (e.g. contract metadata/source) requires only that some effective key be present, not that the client-supplied key was validated upstream. A well-formed but invalid, expired, or out-of-credit client key may therefore receive cached PRO-gated data — no protected upstream request is made on its behalf. This is a deliberate consequence of the principle above, not a validation gap.

**Two transports, one scheme**

The server reaches the PRO API over two transports, each with its own header builder, but both follow the same `Bearer` scheme:

- **REST / data path** (`make_blockscout_request`, `make_blockscout_post_request`, `make_metadata_request`): headers come from `_pro_api_headers()` in `blockscout_mcp_server/tools/common.py` — always `User-Agent` and `Accept: application/json`, plus `Authorization: Bearer <key>` when a key is configured.
- **JSON-RPC path** for `read_contract` (the Async Web3 Connection Pool): base headers come from `_default_headers()` in `blockscout_mcp_server/web3_pool.py`, and `Authorization` is appended at request time by `_request_headers()`. The auth header is deliberately excluded from the pool's cache keys and resolved on every call (including cache hits), so the secret never enters internal cache dictionaries and the current key is always applied — even to already-pooled providers.
- **JSON-RPC path** for `read_contract` (the Async Web3 Connection Pool): the `Authorization` header is injected per request rather than stored on the shared pooled provider, so the key never enters the pool's cache keys and concurrent requests carrying different client keys cannot cross-contaminate (see `blockscout_mcp_server/web3_pool.py`).

**User-Agent**

Expand All @@ -212,11 +221,13 @@ The server reaches the PRO API over two transports, each with its own header bui

**Effect of a missing key**

The key requirement is enforced as a single chokepoint: each PRO API entry point checks `config.pro_api_key` first and raises a `ValueError` *before any network call*, so the server never issues a request the gateway is guaranteed to reject. The chain-support validation runs only after this check, keeping the key as the first gate.
The key requirement is enforced as a single chokepoint: each PRO API entry point resolves the effective key and raises a `ValueError` *before any network call*, so the server never issues a request the gateway is guaranteed to reject. The effective key is the client-supplied key when present and valid, otherwise the server-side key; resolution runs before chain-support validation, keeping the key as the first gate.

- **Primary data requests** (`make_blockscout_request` / `make_blockscout_post_request`) and **contract reads** (`Web3Pool.get`) fail fast — the tool returns a clear error and makes no network call.
- **Secondary metadata requests** (`make_metadata_request`, used by `get_address_info`) also fail fast, but callers treat this like any other metadata failure: the `metadata` field is returned `null` with an explanatory note while the primary data is still returned.

A malformed client key raises a distinct terminal error (no fallback); only the genuine absence of both a client key and a server key raises the not-configured error.

**What does not require the key**

- Chain discovery and validation read the PRO API *config* endpoint (`/api/json/config`) without authentication, so `get_chains_list` and chain-support checks work regardless of the key. Only *data access* is gated.
Expand All @@ -225,6 +236,7 @@ The key requirement is enforced as a single chokepoint: each PRO API entry point
**Extended HTTP / REST mode**

- The PRO API key stays server-side config; REST consumers never supply it. A REST client authenticates (if at all) to the MCP server itself, while the server authenticates to the PRO API with its own configured key.
- The client-supplied key header (`BLOCKSCOUT_PRO_API_KEY_HEADER`) is honored only for genuine MCP tool calls; the REST layer continues to ignore any client-supplied key and authenticates to the PRO API solely with the server's configured key. Extending client-supplied keys to REST is deliberately out of scope for this iteration.
- An `Authorization` header sent by a REST client is never forwarded to the PRO API. The data path builds PRO API headers solely from server config and does not read incoming request headers, and the Web3 pool explicitly strips any caller-supplied `Authorization` before constructing requests or cache keys.

**Error semantics**
Expand Down
2 changes: 1 addition & 1 deletion blockscout_mcp_server/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: LicenseRef-Blockscout
"""Blockscout MCP Server package."""

__version__ = "0.16.0.dev15"
__version__ = "0.16.0.dev16"
6 changes: 6 additions & 0 deletions blockscout_mcp_server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class ServerConfig(BaseSettings):
pro_api_config_ttl_seconds: int = 300
pro_api_config_refresh_retry_seconds: int = 30
pro_api_key: str = ""
pro_api_key_header: str = "Blockscout-MCP-Pro-Api-Key"

@field_validator("pro_api_base_url")
@classmethod
Expand All @@ -33,6 +34,11 @@ def normalize_pro_api_base_url(cls, value: str) -> str:
def normalize_pro_api_key(cls, value: str) -> str:
return value.strip()

@field_validator("pro_api_key_header")
@classmethod
def normalize_pro_api_key_header(cls, value: str) -> str:
return value.strip()

# Metadata configuration (PRO API metadata endpoint)
metadata_timeout: float = 30.0

Expand Down
2 changes: 2 additions & 0 deletions blockscout_mcp_server/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ See the [agent-skills README](https://github.com/blockscout/agent-skills) for fu
- **ChatGPT Apps:** Enable the Blockscout app from the [ChatGPT Apps marketplace](https://chatgpt.com/apps?q=Blockscout)
- **Cursor and Gemini CLI:** Direct HTTP URL with 180-second timeout

**Authentication (optional):** Tools reach Blockscout data through the Blockscout PRO API. A hosted server is normally already configured with its own key, but when you connect to an HTTP MCP server you may supply your own PRO API key in the `Blockscout-MCP-Pro-Api-Key` request header. A client-supplied key takes precedence over the server's own key for that request. The REST API interface ignores this header and always authenticates with the server's configured key.

**Key Tools:** `get_address_info`, `get_transactions_by_address`, `get_tokens_by_address`

### REST API Interface
Expand Down
Loading
Loading