Skip to content

Commit e86738a

Browse files
akolotovclaude
andcommitted
Phase 7: Documentation and version bump
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 376e22f commit e86738a

9 files changed

Lines changed: 29 additions & 4 deletions

File tree

.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
@@ -129,6 +130,7 @@ mcp-server/
129130
│ ├── test_analytics_source.py # Unit tests for analytics source detection
130131
│ ├── test_cache.py # Unit tests for cache behavior
131132
│ ├── 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
132134
│ ├── test_hatch_build.py # Unit tests for custom Hatch build hook helpers
133135
│ ├── test_instructions_data.py # Unit tests for the InstructionsData payload model
134136
│ ├── test_integration_helpers.py # Unit tests for integration test helpers
@@ -358,6 +360,7 @@ mcp-server/
358360
* Provides a singleton configuration object that can be imported and used by other modules, especially by `tools/common.py` for API calls.
359361
* `mcp_allowed_hosts: str`: Comma-separated list of allowed `Host` header values for DNS rebinding protection (default: empty, auto-detected based on bind host).
360362
* `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).
361364
* **`constants.py`**:
362365
* Defines centralized constants used throughout the application, including data truncation limits.
363366
* Ensures consistency between different parts of the application.
@@ -379,6 +382,10 @@ mcp-server/
379382
* Provides `ClientMeta` dataclass and `extract_client_meta_from_ctx()` function.
380383
* Falls back to User-Agent header when MCP client name is unavailable.
381384
* 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.
382389
* **`cache.py`**:
383390
* Encapsulates in-memory caching of chain data with TTL management.
384391
* **`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: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,18 @@ All Blockscout data flows through the authenticated Blockscout PRO API gateway.
198198
- 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.
199199
- 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.
200200

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+
201207
**Two transports, one scheme**
202208

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

205211
- **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.
207213

208214
**User-Agent**
209215

@@ -217,6 +223,8 @@ The key requirement is enforced as a single chokepoint: each PRO API entry point
217223
- **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.
218224
- **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.
219225

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+
220228
**What does not require the key**
221229

222230
- 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
225233
**Extended HTTP / REST mode**
226234

227235
- 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.
228237
- 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.
229238

230239
**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.dev14"
4+
__version__ = "0.16.0.dev15"

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

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "blockscout-mcp-server"
3-
version = "0.16.0.dev14"
3+
version = "0.16.0.dev15"
44
description = "MCP server for Blockscout"
55
requires-python = ">=3.11"
66
dependencies = [

server.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
33
"name": "com.blockscout/mcp-server",
44
"description": "MCP server for Blockscout",
5-
"version": "0.16.0.dev14",
5+
"version": "0.16.0.dev15",
66
"websiteUrl": "https://blockscout.com",
77
"repository": {
88
"url": "https://github.com/blockscout/mcp-server",

0 commit comments

Comments
 (0)