Skip to content

Commit 9e75aae

Browse files
authored
Allow MCP clients to supply their own Blockscout PRO API key (#400)
1 parent 73ba60d commit 9e75aae

41 files changed

Lines changed: 2051 additions & 88 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ BLOCKSCOUT_PRO_API_CONFIG_TTL_SECONDS=300
2828
# * 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.
2929
# Generate one at https://dev.blockscout.com.
3030
BLOCKSCOUT_PRO_API_KEY=""
31+
# Name of the request header an MCP client uses to supply its own PRO API key (MCP tools over HTTP only).
32+
# 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.
33+
# 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.
34+
BLOCKSCOUT_PRO_API_KEY_HEADER="Blockscout-MCP-Pro-Api-Key"
3135
BLOCKSCOUT_CHAINS_LIST_TTL_SECONDS=300
3236
BLOCKSCOUT_PROGRESS_INTERVAL_SECONDS="15.0"
3337

AGENTS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ mcp-server/
2323
│ ├── analytics.py # Centralized Mixpanel analytics for tool invocations (HTTP mode only)
2424
│ ├── telemetry.py # Fire-and-forget community telemetry reporting
2525
│ ├── client_meta.py # Shared client metadata extraction helpers and defaults
26+
│ ├── pro_api_key_context.py # Request-scoped client-supplied PRO API key state, resolver, and @pro_api_key_scope decorator
2627
│ ├── cache.py # Simple in-memory cache for chain data
2728
│ ├── web3_pool.py # Async Web3 connection pool manager
2829
│ ├── models.py # Defines standardized Pydantic models for all tool responses
@@ -131,6 +132,7 @@ mcp-server/
131132
│ ├── test_analytics_source.py # Unit tests for analytics source detection
132133
│ ├── test_cache.py # Unit tests for cache behavior
133134
│ ├── test_client_meta.py # Unit tests for client metadata extraction
135+
│ ├── test_pro_api_key_context.py # Unit tests for client-supplied PRO API key resolution
134136
│ ├── test_hatch_build.py # Unit tests for custom Hatch build hook helpers
135137
│ ├── test_instructions_data.py # Unit tests for the InstructionsData payload model
136138
│ ├── test_integration_helpers.py # Unit tests for integration test helpers
@@ -360,6 +362,7 @@ mcp-server/
360362
* Provides a singleton configuration object that can be imported and used by other modules, especially by `tools/common.py` for API calls.
361363
* `mcp_allowed_hosts: str`: Comma-separated list of allowed `Host` header values for DNS rebinding protection (default: empty, auto-detected based on bind host).
362364
* `mcp_allowed_origins: str`: Comma-separated list of allowed `Origin` header values for DNS rebinding protection (default: empty, auto-detected based on bind host).
365+
* `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).
363366
* **`constants.py`**:
364367
* Defines centralized constants used throughout the application, including data truncation limits.
365368
* Ensures consistency between different parts of the application.
@@ -381,6 +384,10 @@ mcp-server/
381384
* Provides `ClientMeta` dataclass and `extract_client_meta_from_ctx()` function.
382385
* Falls back to User-Agent header when MCP client name is unavailable.
383386
* Ensures consistent sentinel defaults ("N/A", "Unknown") across logging and analytics modules.
387+
* **`pro_api_key_context.py`**:
388+
* Owns request-scoped resolution of a client-supplied Blockscout PRO API key, kept separate from logging/observability.
389+
* 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.
390+
* Honored only for genuine MCP calls (ignored when `ctx.call_source == "rest"`); the key is never logged or placed in cache keys.
384391
* **`cache.py`**:
385392
* Encapsulates in-memory caching of chain data with TTL management.
386393
* **`web3_pool.py`**:

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ ENV BLOCKSCOUT_MIXPANEL_API_HOST=""
4949
ENV BLOCKSCOUT_DISABLE_COMMUNITY_TELEMETRY="false"
5050
ENV BLOCKSCOUT_INTERMEDIARY_HEADER="Blockscout-MCP-Intermediary"
5151
ENV BLOCKSCOUT_INTERMEDIARY_ALLOWLIST="ClaudeDesktop,HigressPlugin,EvaluationSuite"
52+
ENV BLOCKSCOUT_PRO_API_KEY_HEADER="Blockscout-MCP-Pro-Api-Key"
5253

5354
# Set the default transport mode. Can be overridden at runtime with -e.
5455
# Options: "stdio" (default), "http"

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,8 @@ To obtain one, create an account at https://dev.blockscout.com (the free tier do
229229
export BLOCKSCOUT_PRO_API_KEY=proapi_your_key_here
230230
```
231231

232+
**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.
233+
232234
### Running the Server
233235

234236
The server runs in `stdio` mode by default:

SPEC.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,17 +193,26 @@ This architecture provides the flexibility of a multi-protocol server without th
193193

194194
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.
195195

196+
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.
197+
196198
**Credential**
197199

198200
- 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.
199201
- 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.
200202

203+
**Client-supplied credential (MCP tools over HTTP only)**
204+
205+
- 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.
206+
- 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.
207+
- 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.
208+
- 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.
209+
201210
**Two transports, one scheme**
202211

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

205214
- **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.
206-
- **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.
215+
- **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`).
207216

208217
**User-Agent**
209218

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

213222
**Effect of a missing key**
214223

215-
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.
224+
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.
216225

217226
- **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.
218227
- **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.
219228

229+
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.
230+
220231
**What does not require the key**
221232

222233
- 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.
@@ -225,6 +236,7 @@ The key requirement is enforced as a single chokepoint: each PRO API entry point
225236
**Extended HTTP / REST mode**
226237

227238
- 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.
239+
- 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.
228240
- 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.
229241

230242
**Error semantics**

blockscout_mcp_server/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# SPDX-License-Identifier: LicenseRef-Blockscout
22
"""Blockscout MCP Server package."""
33

4-
__version__ = "0.16.0.dev15"
4+
__version__ = "0.16.0.dev16"

blockscout_mcp_server/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class ServerConfig(BaseSettings):
2222
pro_api_config_ttl_seconds: int = 300
2323
pro_api_config_refresh_retry_seconds: int = 30
2424
pro_api_key: str = ""
25+
pro_api_key_header: str = "Blockscout-MCP-Pro-Api-Key"
2526

2627
@field_validator("pro_api_base_url")
2728
@classmethod
@@ -33,6 +34,11 @@ def normalize_pro_api_base_url(cls, value: str) -> str:
3334
def normalize_pro_api_key(cls, value: str) -> str:
3435
return value.strip()
3536

37+
@field_validator("pro_api_key_header")
38+
@classmethod
39+
def normalize_pro_api_key_header(cls, value: str) -> str:
40+
return value.strip()
41+
3642
# Metadata configuration (PRO API metadata endpoint)
3743
metadata_timeout: float = 30.0
3844

blockscout_mcp_server/llms.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ See the [agent-skills README](https://github.com/blockscout/agent-skills) for fu
2525
- **ChatGPT Apps:** Enable the Blockscout app from the [ChatGPT Apps marketplace](https://chatgpt.com/apps?q=Blockscout)
2626
- **Cursor and Gemini CLI:** Direct HTTP URL with 180-second timeout
2727

28+
**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.
29+
2830
**Key Tools:** `get_address_info`, `get_transactions_by_address`, `get_tokens_by_address`
2931

3032
### REST API Interface

0 commit comments

Comments
 (0)