Skip to content

Commit ba5cdea

Browse files
akolotovclaude
andcommitted
docs: clarify PRO API key authorizes upstream requests, not MCP access
Document that the PRO API key exists to authorize the server's upstream requests, not to gate MCP functionality, so serving cached contract data to an unvalidated client key is deliberate. Also align "Effect of a missing key" to the resolved-effective-key model. @coderabbitai ignore Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 31e245e commit ba5cdea

2 files changed

Lines changed: 8 additions & 1 deletion

File tree

SPEC.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ 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.
@@ -203,6 +205,7 @@ All Blockscout data flows through the authenticated Blockscout PRO API gateway.
203205
- 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.
204206
- 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.
205207
- 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.
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.
206209

207210
**Two transports, one scheme**
208211

@@ -218,7 +221,7 @@ The server reaches the PRO API over two transports, each with its own header bui
218221

219222
**Effect of a missing key**
220223

221-
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 via `require_pro_api_key()` (which calls `resolve_pro_api_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 `config.pro_api_key`; resolution runs before chain-support validation, keeping the key as the first gate.
222225

223226
- **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.
224227
- **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.

blockscout_mcp_server/tools/contract/_shared.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ async def _fetch_and_process_contract(chain_id: str, address: str) -> CachedCont
3333

3434
normalized_address = address.lower()
3535
cache_key = f"{chain_id}:{normalized_address}"
36+
# Serving a cached contract requires only that some effective key be present
37+
# (checked above), not that a client-supplied key was validated upstream.
38+
# This is deliberate: the key authorizes the server's upstream requests, and
39+
# a cache hit makes none, so there is nothing to authorize here.
3640
if cached := await contract_cache.get(cache_key):
3741
return cached
3842

0 commit comments

Comments
 (0)