Allow MCP clients to supply their own Blockscout PRO API key#400
Conversation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…O API Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Too many files changed? Review this PR in Change Stack to see how the pieces fit before you dive in. Warning Review limit reached
More reviews will be available in 20 minutes and 23 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (13)
WalkthroughPer-request client-supplied Blockscout PRO API key support for MCP tools: adds a configurable header and ServerConfig field, implements ContextVar-backed extraction/normalization/resolution (client-first, no fallback for malformed client keys), provides a require_pro_api_key gate and ChangesClient-Supplied PRO API Key Support
Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- Centralize the not-configured raise in a new `require_pro_api_key()` helper so every PRO API entry point (`make_blockscout_request`, `make_blockscout_post_request`, `make_metadata_request`, `_fetch_and_process_contract`, `Web3Pool.get`) emits the same error text and names the configurable client header when enabled. - Tighten `_MAX_KEY_LENGTH` from 4096 to 256 (~4x current key length of 79) so the bound rejects abuse without limiting plausible future formats. - Log unexpected ctx-shape failures in `extract_client_pro_api_key_from_ctx` at DEBUG (with `exc_info`) so the defensive bare-except no longer hides bugs silently. - Reword the test-shape comment in `_normalize_key` and document the blanket `@pro_api_key_scope` policy plus the required decorator stacking order (must sit inside `@log_tool_invocation`). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
tests/tools/contract/test_fetch_and_process_contract.py (1)
262-262: 💤 Low valueMove import to top of file.
This import should be at the top of the module with the other imports from
pro_api_key_context. As per coding guidelines, all import statements must be placed at the top of the Python module.♻️ Suggested fix
At line 8, update the import to include
_Malformed:-from blockscout_mcp_server.pro_api_key_context import _client_key_state, _Valid +from blockscout_mcp_server.pro_api_key_context import _client_key_state, _Malformed, _ValidThen remove line 262:
- from blockscout_mcp_server.pro_api_key_context import _Malformed - cached = CachedContract(metadata={}, source_files={})🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/tools/contract/test_fetch_and_process_contract.py` at line 262, Move the from blockscout_mcp_server.pro_api_key_context import _Malformed statement to the top of the module alongside the other imports from pro_api_key_context (e.g., update the existing import at the top to also import _Malformed) and delete the duplicate import at line 262 so the module only imports _Malformed once at the file header.tests/test_pro_api_key_context.py (1)
219-236: ⚡ Quick winExercise malformed-key redaction via the real extraction→scope→resolve path.
These two tests currently inject
_Malformed()directly, so they don’t validate the integration path where raw client input is parsed and then rejected. Routing the assertion throughpro_api_key_scopewith malformed header values will better protect against regressions in the actual request flow.As per coding guidelines,
tests/**/*.pyunit tests must provide comprehensive coverage including error handling and edge cases.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_pro_api_key_context.py` around lines 219 - 236, The tests should exercise the real extraction→scope→resolve path instead of directly injecting _Malformed(): update both tests to send malformed header values through pro_api_key_scope (use the same raw_value strings) so the flow goes through the extractor and scope creation before resolve_pro_api_key() is called; replace the _set_key_state(_Malformed()) injection with creating a pro_api_key_scope from a request/header containing the malformed value and then call resolve_pro_api_key() (or the same resolve helper) so the ValueError assertion confirms the redaction behavior in the actual request path.tests/tools/test_decorators.py (1)
280-281: ⚡ Quick winMake the no-leak log assertion case-insensitive.
The current check only blocks one header casing. Normalizing
caplog.textto lowercase makes this test catch leaked header names/values regardless of logger casing behavior.Suggested patch
- assert client_key not in caplog.text - assert "Blockscout-MCP-Pro-Api-Key" not in caplog.text + log_text = caplog.text.lower() + assert client_key.lower() not in log_text + assert "blockscout-mcp-pro-api-key" not in log_textAs per coding guidelines,
tests/tools/**/*.pyunit tests must provide comprehensive coverage including edge cases.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/tools/test_decorators.py` around lines 280 - 281, The test currently asserts header leakage using a case-sensitive match; update the two assertions to perform case-insensitive checks by comparing caplog.text.lower() against client_key.lower() and against "blockscout-mcp-pro-api-key" (lowercased) so leaked header names/values are caught regardless of logger casing behavior; modify the two assertions that reference caplog.text and the literal "Blockscout-MCP-Pro-Api-Key" accordingly in tests/tools/test_decorators.py.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@SPEC.md`:
- Around line 226-227: Update the contradictory wording so the section
consistently states the auth gate checks the resolved effective key: replace
references that say the gate checks config.pro_api_key first with language that
it checks resolve_pro_api_key() (the resolved effective key), and clarify that a
valid client-supplied key passes, a malformed client key raises a distinct
terminal error (no fallback), and only absence of both client and server keys
raises the not-configured error; ensure both the sentence referencing
config.pro_api_key and the later sentence about guards reference
resolve_pro_api_key() uniformly.
In `@tests/test_analytics.py`:
- Around line 200-203: The header-name leak check is case-sensitive; change the
second assertion to compare lowercased strings so casing won't miss leaks:
compute a lowercased version of all_text (use all_text.lower()) and assert that
"blockscout-mcp-pro-api-key".lower() is not in that lowercased text (leave the
client_key assertion as-is). Update the assertion using the existing all_text
and the header string "Blockscout-MCP-Pro-Api-Key" to perform the
case-insensitive check.
In `@tests/tools/test_common_blockscout_request_auth.py`:
- Around line 241-255: Add an assertion that the AsyncMock progress callback was
actually awaited during the periodic-progress path: after awaiting
make_request_with_periodic_progress and before resetting _client_key_state (or
immediately after result is obtained), assert that mock_ctx.report_progress was
awaited at least once (e.g. call mock_ctx.report_progress.assert_awaited() or
assert mock_ctx.report_progress.await_count > 0). This ensures the test for
make_request_with_periodic_progress / make_blockscout_request verifies progress
beats were emitted.
---
Nitpick comments:
In `@tests/test_pro_api_key_context.py`:
- Around line 219-236: The tests should exercise the real
extraction→scope→resolve path instead of directly injecting _Malformed(): update
both tests to send malformed header values through pro_api_key_scope (use the
same raw_value strings) so the flow goes through the extractor and scope
creation before resolve_pro_api_key() is called; replace the
_set_key_state(_Malformed()) injection with creating a pro_api_key_scope from a
request/header containing the malformed value and then call
resolve_pro_api_key() (or the same resolve helper) so the ValueError assertion
confirms the redaction behavior in the actual request path.
In `@tests/tools/contract/test_fetch_and_process_contract.py`:
- Line 262: Move the from blockscout_mcp_server.pro_api_key_context import
_Malformed statement to the top of the module alongside the other imports from
pro_api_key_context (e.g., update the existing import at the top to also import
_Malformed) and delete the duplicate import at line 262 so the module only
imports _Malformed once at the file header.
In `@tests/tools/test_decorators.py`:
- Around line 280-281: The test currently asserts header leakage using a
case-sensitive match; update the two assertions to perform case-insensitive
checks by comparing caplog.text.lower() against client_key.lower() and against
"blockscout-mcp-pro-api-key" (lowercased) so leaked header names/values are
caught regardless of logger casing behavior; modify the two assertions that
reference caplog.text and the literal "Blockscout-MCP-Pro-Api-Key" accordingly
in tests/tools/test_decorators.py.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 381f728e-6a5b-4fab-9d9d-57c66db6a52c
📒 Files selected for processing (41)
.env.exampleAGENTS.mdDockerfileREADME.mdSPEC.mdblockscout_mcp_server/__init__.pyblockscout_mcp_server/config.pyblockscout_mcp_server/llms.txtblockscout_mcp_server/pro_api_key_context.pyblockscout_mcp_server/tools/address/get_address_info.pyblockscout_mcp_server/tools/address/get_tokens_by_address.pyblockscout_mcp_server/tools/address/nft_tokens_by_address.pyblockscout_mcp_server/tools/block/get_block_info.pyblockscout_mcp_server/tools/block/get_block_number.pyblockscout_mcp_server/tools/chains/get_chains_list.pyblockscout_mcp_server/tools/common.pyblockscout_mcp_server/tools/contract/_shared.pyblockscout_mcp_server/tools/contract/get_contract_abi.pyblockscout_mcp_server/tools/contract/inspect_contract_code.pyblockscout_mcp_server/tools/contract/read_contract.pyblockscout_mcp_server/tools/direct_api/direct_api_call.pyblockscout_mcp_server/tools/ens/get_address_by_ens_name.pyblockscout_mcp_server/tools/initialization/unlock_blockchain_analysis.pyblockscout_mcp_server/tools/search/lookup_token_by_symbol.pyblockscout_mcp_server/tools/transaction/get_token_transfers_by_address.pyblockscout_mcp_server/tools/transaction/get_transaction_info.pyblockscout_mcp_server/tools/transaction/get_transactions_by_address.pyblockscout_mcp_server/web3_pool.pypyproject.tomlserver.jsontests/integration/test_common_helpers.pytests/test_analytics.pytests/test_config.pytests/test_pro_api_key_context.pytests/test_pro_api_key_http_transport.pytests/test_web3_pool.pytests/tools/contract/test_fetch_and_process_contract.pytests/tools/test_common_blockscout_request_auth.pytests/tools/test_common_metadata.pytests/tools/test_common_post_request.pytests/tools/test_decorators.py
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>
Trim implementation mechanics from the "Blockscout PRO API Authentication" section per the SPEC.md preference for concise, high-level descriptions: drop private symbol names (decorator/ContextVar internals, JSON-RPC injection internals) in favor of module pointers, while preserving the security invariants (per-request scoping, no log/cache leakage, no cross-contamination) and removing the redundant resolved-key paragraph. @coderabbitai ignore Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Route malformed-key redaction tests through the real extraction → scope → resolve path instead of injecting _Malformed() directly, so redaction is verified in the actual request flow. - Move the _Malformed import to the module header in the contract test. - Make header name/value leak checks case-insensitive in the decorator and analytics tests. - Assert the periodic-progress callback was actually awaited. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…oth test sections Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
What
Lets an MCP client supply its own Blockscout PRO API key on a per-request basis (HTTP MCP transport only), so the resolved effective key — client-supplied when present, otherwise the server's configured key — authenticates every PRO API request. Closes #392.
Why
Previously the server could only ever use the single
BLOCKSCOUT_PRO_API_KEYconfigured at deploy time. Multi-tenant / hosted deployments need each client to bring its own key so rate limits and billing attribute to the caller, without the server holding every client's secret.How it works
Blockscout-MCP-Pro-Api-Key) carries the client key on HTTP MCP requests.ContextVarholds the client key's state (absent / valid / malformed) for the duration of a tool invocation; a@pro_api_key_scopedecorator on every tool sets and resets it with strict discipline.resolve_pro_api_key()centralizes precedence: a valid client key wins, a malformed one fails fast, otherwise we fall back to the server's configured key.tools/common.pyhelpers, contract fetch, and the Web3 pool) resolve the effective key at request time and injectAuthorizationper-request — the pooled Web3 providers no longer store the secret.Phases (one commit each)
pro_api_key_headerconfiguration settingpro_api_key_contextmodule (state types, ContextVar, resolver, scope decorator)pro_api_key_scopedecorator to all tools0.16.0.dev16)Testing
uv run pytest tests/)test_make_blockscout_request_client_key_via_context_var, which ran (not skipped) and authenticated a request to the live gateway using only the client-supplied keyruff check .andruff format --check .both cleanReviewer notes
0.16.0.dev16inpyproject.toml,blockscout_mcp_server/__init__.py, andserver.json;mcpb/manifest.jsonis intentionally unchanged.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Configuration
Behavior Changes
Privacy