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
@@ -129,6 +130,7 @@ mcp-server/
129
130
│ ├── test_analytics_source.py # Unit tests for analytics source detection
130
131
│ ├── test_cache.py # Unit tests for cache behavior
131
132
│ ├── test_client_meta.py # Unit tests for client metadata extraction
133
+
│ ├── test_pro_api_key_context.py # Unit tests for client-supplied PRO API key resolution
132
134
│ ├── test_hatch_build.py # Unit tests for custom Hatch build hook helpers
133
135
│ ├── test_instructions_data.py # Unit tests for the InstructionsData payload model
134
136
│ ├── test_integration_helpers.py # Unit tests for integration test helpers
@@ -358,6 +360,7 @@ mcp-server/
358
360
* Provides a singleton configuration object that can be imported and used by other modules, especially by `tools/common.py` for API calls.
359
361
*`mcp_allowed_hosts: str`: Comma-separated list of allowed `Host` header values for DNS rebinding protection (default: empty, auto-detected based on bind host).
360
362
*`mcp_allowed_origins: str`: Comma-separated list of allowed `Origin` header values for DNS rebinding protection (default: empty, auto-detected based on bind host).
363
+
*`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).
361
364
***`constants.py`**:
362
365
* Defines centralized constants used throughout the application, including data truncation limits.
363
366
* Ensures consistency between different parts of the application.
@@ -379,6 +382,10 @@ mcp-server/
379
382
* Provides `ClientMeta` dataclass and `extract_client_meta_from_ctx()` function.
380
383
* Falls back to User-Agent header when MCP client name is unavailable.
381
384
* Ensures consistent sentinel defaults ("N/A", "Unknown") across logging and analytics modules.
385
+
***`pro_api_key_context.py`**:
386
+
* Owns request-scoped resolution of a client-supplied Blockscout PRO API key, kept separate from logging/observability.
387
+
* 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.
388
+
* Honored only for genuine MCP calls (ignored when `ctx.call_source == "rest"`); the key is never logged or placed in cache keys.
382
389
***`cache.py`**:
383
390
* 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
+10-1Lines changed: 10 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -198,12 +198,18 @@ All Blockscout data flows through the authenticated Blockscout PRO API gateway.
198
198
- 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
199
- 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
200
201
+
**Client-supplied credential (MCP tools over HTTP only)**
202
+
203
+
- 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.
204
+
- 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.
205
+
- The credential is resolved per request via a request-scoped `ContextVar` in `blockscout_mcp_server/pro_api_key_context.py`. The `@pro_api_key_scope` decorator (applied alongside `@log_tool_invocation` on every tool, but kept independent of it) records the client-key state for the duration of a single tool invocation, and the PRO API request helpers call `resolve_pro_api_key()` at the moment they build the `Authorization` header. The header is read only for genuine MCP calls (skipped when `ctx.call_source == "rest"`) and the client key is never written to logs, analytics, or cache keys.
206
+
201
207
**Two transports, one scheme**
202
208
203
209
The server reaches the PRO API over two transports, each with its own header builder, but both follow the same `Bearer` scheme:
204
210
205
211
-**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.
212
+
-**JSON-RPC path** for `read_contract` (the Async Web3 Connection Pool): base (non-secret) headers come from `_default_headers()` in `blockscout_mcp_server/web3_pool.py` and are the only header material in the pool's cache keys. `Authorization` is injected at request time inside the provider's `_make_http_request`, which calls `resolve_pro_api_key()` for each individual JSON-RPC POST. Because the secret is built per request rather than stored on the shared provider, it never enters the pool's cache keys, pooled providers are never mutated with a caller's key, and two concurrent requests carrying different client keys cannot cross-contaminate.
207
213
208
214
**User-Agent**
209
215
@@ -217,6 +223,8 @@ The key requirement is enforced as a single chokepoint: each PRO API entry point
217
223
-**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
224
-**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
225
226
+
These guards check the *resolved* effective key (`resolve_pro_api_key()`): a valid client-supplied key satisfies them, a malformed client key raises a distinct terminal error (no fallback), and only the genuine absence of both a client key and a server key raises the not-configured error.
227
+
220
228
**What does not require the key**
221
229
222
230
- 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 +233,7 @@ The key requirement is enforced as a single chokepoint: each PRO API entry point
225
233
**Extended HTTP / REST mode**
226
234
227
235
- 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.
236
+
- 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
237
- 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