You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
# * 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.
29
29
# Generate one at https://dev.blockscout.com.
30
30
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.
│ ├── pro_api_key_context.py # Request-scoped client-supplied PRO API key state, resolver, and @pro_api_key_scope decorator
26
27
│ ├── cache.py # Simple in-memory cache for chain data
27
28
│ ├── web3_pool.py # Async Web3 connection pool manager
28
29
│ ├── models.py # Defines standardized Pydantic models for all tool responses
@@ -131,6 +132,7 @@ mcp-server/
131
132
│ ├── test_analytics_source.py # Unit tests for analytics source detection
132
133
│ ├── test_cache.py # Unit tests for cache behavior
133
134
│ ├── 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
134
136
│ ├── test_hatch_build.py # Unit tests for custom Hatch build hook helpers
135
137
│ ├── test_instructions_data.py # Unit tests for the InstructionsData payload model
136
138
│ ├── test_integration_helpers.py # Unit tests for integration test helpers
@@ -360,6 +362,7 @@ mcp-server/
360
362
* Provides a singleton configuration object that can be imported and used by other modules, especially by `tools/common.py` for API calls.
361
363
*`mcp_allowed_hosts: str`: Comma-separated list of allowed `Host` header values for DNS rebinding protection (default: empty, auto-detected based on bind host).
362
364
*`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).
363
366
***`constants.py`**:
364
367
* Defines centralized constants used throughout the application, including data truncation limits.
365
368
* Ensures consistency between different parts of the application.
@@ -381,6 +384,10 @@ mcp-server/
381
384
* Provides `ClientMeta` dataclass and `extract_client_meta_from_ctx()` function.
382
385
* Falls back to User-Agent header when MCP client name is unavailable.
383
386
* 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.
384
391
***`cache.py`**:
385
392
* Encapsulates in-memory caching of chain data with TTL management.
**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.
Copy file name to clipboardExpand all lines: SPEC.md
+14-2Lines changed: 14 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -193,17 +193,26 @@ This architecture provides the flexibility of a multi-protocol server without th
193
193
194
194
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.
195
195
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
+
196
198
**Credential**
197
199
198
200
- 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.
199
201
- 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.
200
202
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
+
201
210
**Two transports, one scheme**
202
211
203
212
The server reaches the PRO API over two transports, each with its own header builder, but both follow the same `Bearer` scheme:
204
213
205
214
-**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`).
207
216
208
217
**User-Agent**
209
218
@@ -212,11 +221,13 @@ The server reaches the PRO API over two transports, each with its own header bui
212
221
213
222
**Effect of a missing key**
214
223
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.
216
225
217
226
-**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.
218
227
-**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.
219
228
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
+
220
231
**What does not require the key**
221
232
222
233
- 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
225
236
**Extended HTTP / REST mode**
226
237
227
238
- 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.
228
240
- 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.
Copy file name to clipboardExpand all lines: blockscout_mcp_server/llms.txt
+2Lines changed: 2 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -25,6 +25,8 @@ See the [agent-skills README](https://github.com/blockscout/agent-skills) for fu
25
25
- **ChatGPT Apps:** Enable the Blockscout app from the [ChatGPT Apps marketplace](https://chatgpt.com/apps?q=Blockscout)
26
26
- **Cursor and Gemini CLI:** Direct HTTP URL with 180-second timeout
27
27
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.
0 commit comments