Skip to content

Conversation

akolotov
Copy link
Collaborator

@akolotov akolotov commented Aug 13, 2025

Closes #173

Summary by CodeRabbit

  • New Features

    • Opt-in Mixpanel analytics for HTTP modes (requires env vars); captures anonymous stable client IDs, call source, and client metadata with User-Agent fallback.
  • Documentation

    • Spec and README updated with analytics integration details and client-identification fallback.
  • Chores

    • Added analytics env vars to examples and Docker config, added Mixpanel dependency, bumped version to 0.8.0-dev.
  • Tests

    • New tests for analytics tracking, IP extraction, client-meta extraction, call-source, and decorator integration.

@akolotov akolotov self-assigned this Aug 13, 2025
Copy link

coderabbitai bot commented Aug 13, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds opt-in Mixpanel analytics gated to HTTP mode + token, client metadata extraction utilities, REST request propagation into mock contexts with call_source, decorator wiring to emit analytics, config/env/Docker/packaging updates for Mixpanel, new tests, and bumps package version to 0.8.0-dev.

Changes

Cohort / File(s) Summary
Analytics & client metadata
blockscout_mcp_server/analytics.py, blockscout_mcp_server/client_meta.py
New Mixpanel analytics and ClientMeta utilities: deterministic distinct_id, IP/header extraction, call_source determination, gated by HTTP mode + token, defensive error handling, and client-meta extraction with User-Agent fallback.
Decorator integration
blockscout_mcp_server/tools/decorators.py
Replaces local defaults with extract_client_meta_from_ctx and calls analytics.track_tool_invocation from the log decorator (exceptions swallowed).
Server enablement
blockscout_mcp_server/server.py
Imports analytics and calls analytics.set_http_mode(True) on the HTTP path.
REST request propagation
blockscout_mcp_server/api/dependencies.py, blockscout_mcp_server/api/routes.py
Adds _RequestContextWrapper and request_context on MockCtx, sets call_source="rest"; REST routes now accept request: Request and forward it to get_mock_context(request).
Config, env, Docker, packaging
.env.example, Dockerfile, blockscout_mcp_server/config.py, pyproject.toml
Adds BLOCKSCOUT_MIXPANEL_TOKEN and BLOCKSCOUT_MIXPANEL_API_HOST examples; Dockerfile exposes API host and documents token handling; ServerConfig loads .env and adds mixpanel_token/mixpanel_api_host; adds mixpanel dependency; bumps package version.
Spec & docs
SPEC.md, AGENTS.md, README.md
Documents User-Agent fallback, Mixpanel analytics design/wiring, new modules, REST context behavior, and updates example prompts.
Version & test config
blockscout_mcp_server/__init__.py, pytest.ini
Bumps __version__ to "0.8.0-dev"; pytest.ini adds --ignore=temp.
Tests
tests/test_analytics.py, tests/test_analytics_helpers.py, tests/test_analytics_source.py, tests/test_client_meta.py, tests/tools/test_decorators.py
New unit tests for analytics behavior, IP extraction, distinct_id stability, call_source determination, client meta extraction, and decorator analytics invocation; includes fixtures to reset analytics state.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant REST_API as REST API
  participant GetMock as get_mock_context
  participant Decorator as @log_tool_invocation
  participant Analytics
  Client->>REST_API: HTTP request (tool endpoint)
  REST_API->>GetMock: get_mock_context(request)
  GetMock-->>REST_API: MockCtx(request_context, call_source="rest")
  REST_API->>Decorator: invoke tool(ctx=MockCtx, args)
  Decorator->>Analytics: track_tool_invocation(ctx, tool_name, args, client_meta)
  Analytics-->>Decorator: event sent if enabled
  Decorator-->>REST_API: tool result
  REST_API-->>Client: HTTP response
Loading
sequenceDiagram
  participant MCP_Client as MCP Client
  participant MCP_Server as MCP Server
  participant Decorator as @log_tool_invocation
  participant Analytics
  MCP_Client->>MCP_Server: MCP tool call (session.client_params)
  MCP_Server->>Decorator: invoke tool(ctx, args)
  Decorator->>Analytics: track_tool_invocation(ctx, tool_name, args, client_meta)
  Analytics-->>Decorator: no-op unless HTTP mode + token
  Decorator-->>MCP_Server: tool result
  MCP_Server-->>MCP_Client: MCP response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

codex

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the "Integrations" page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/mixpanel-tracking-intro

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🔭 Outside diff range comments (4)
blockscout_mcp_server/api/dependencies.py (1)

35-38: Type get_mock_context signature

Add typing to the dependency provider and drop the ignore.

-def get_mock_context(request=None) -> MockCtx:  # type: ignore[no-untyped-def]
+def get_mock_context(request: Optional["Request"] = None) -> MockCtx:
     """Dependency provider to get a mock context for stateless REST calls."""
     return MockCtx(request=request)
blockscout_mcp_server/server.py (2)

125-127: Move import to module top (imports must be at the top per guidelines)

Inline import inside main_command violates the repo guideline “ALL import statements must be placed at the top of the Python module”. Move it to the top and keep the call conditional.

-            from blockscout_mcp_server.api.routes import register_api_routes
-
             register_api_routes(mcp)

Add at the top with other imports:

+from blockscout_mcp_server.api.routes import register_api_routes

Potential circular import: if this triggers a cycle, consider moving route registration helpers to a small module that doesn’t import server.py.


83-83: Remove noqa E501 on a comment; wrap the line instead

Guideline: avoid # noqa: E501 on ordinary code lines. Break the long TODO into multiple lines.

-# TODO: structured_output is disabled for all tools so far to preserve the LLM context since it adds to the `list/tools` response ~20K tokens.  # noqa: E501
+# TODO: structured_output is disabled for all tools so far to preserve the LLM context.
+# It adds ~20K tokens to the `list/tools` response.
tests/test_analytics.py (1)

49-75: Assert analytics 'source' property and add a REST-source test

track_tool_invocation populates a "source" property via _determine_call_source. Assert it here, and consider adding a dedicated test for the "rest" marker.

Apply this diff to extend the current test and add a new one:

 def test_tracks_with_headers(monkeypatch):
@@
-        assert kwargs.get("meta") == {"ip": "203.0.113.5"}
+        assert kwargs.get("meta") == {"ip": "203.0.113.5"}
+        assert args[2]["source"] == "mcp"
+
+
+def test_tracks_sets_source_rest_when_marker_present(monkeypatch):
+    monkeypatch.setattr(server_config, "mixpanel_token", "test-token", raising=False)
+    headers = {"x-forwarded-for": "198.51.100.1", "user-agent": "pytest-UA"}
+    req = DummyRequest(headers=headers)
+    ctx = DummyCtx(request=req, client_name="clientA", client_version="1.0.0")
+    ctx.call_source = "rest"
+    with patch("blockscout_mcp_server.analytics.Mixpanel") as mp_cls:
+        mp_instance = MagicMock()
+        mp_cls.return_value = mp_instance
+        analytics.set_http_mode(True)
+        analytics.track_tool_invocation(
+            ctx,
+            "tool_rest",
+            {"y": 3},
+            client_meta=ClientMeta(name="clientA", version="1.0.0", protocol="2024-11-05", user_agent="pytest-UA"),
+        )
+        args, kwargs = mp_instance.track.call_args
+        assert args[1] == "tool_rest"
+        assert args[2]["source"] == "rest"
🧹 Nitpick comments (16)
.env.example (1)

39-43: Tighten dotenv style for new keys (ordering and quotes).
To satisfy dotenv linters and keep consistency: place API_HOST before TOKEN and avoid quotes for empty values.

Apply this diff:

-# Optional Mixpanel analytics (HTTP mode only). Set token to enable; leave empty to disable.
-# Use API host for regional endpoints (e.g., EU). No tracking occurs in stdio mode.
-BLOCKSCOUT_MIXPANEL_TOKEN=""
-BLOCKSCOUT_MIXPANEL_API_HOST=""
+# Optional Mixpanel analytics (HTTP mode only). Set token to enable; leave empty to disable.
+# Use API host for regional endpoints (e.g., EU). No tracking occurs in stdio mode.
+BLOCKSCOUT_MIXPANEL_API_HOST=
+BLOCKSCOUT_MIXPANEL_TOKEN=
blockscout_mcp_server/config.py (1)

5-7: Model config via SettingsConfigDict looks correct

Switch to SettingsConfigDict with env_prefix and .env support is appropriate. Consider adding env_file_encoding to be explicit.

-    model_config = SettingsConfigDict(env_prefix="BLOCKSCOUT_", env_file=".env")
+    model_config = SettingsConfigDict(env_prefix="BLOCKSCOUT_", env_file=".env", env_file_encoding="utf-8")
AGENTS.md (2)

22-24: Project tree updated — also add env variables to docs

Since analytics/client_meta were added, please also add BLOCKSCOUT_MIXPANEL_TOKEN and BLOCKSCOUT_MIXPANEL_API_HOST to the environment variables section below for discoverability.

Proposed addition under the “.env.example” list:

  • BLOCKSCOUT_MIXPANEL_TOKEN: Mixpanel project token to enable analytics (HTTP mode only). Leave empty to disable.
  • BLOCKSCOUT_MIXPANEL_API_HOST: Optional custom API host (e.g., api-eu.mixpanel.com).

211-222: Clarify analytics opt-in behavior and PII considerations

Document explicitly that analytics:

  • Are emitted only in HTTP mode and only when BLOCKSCOUT_MIXPANEL_TOKEN is set.
  • Include client IP (from headers) and user-agent; operators should ensure this aligns with their privacy policy.

I can draft a short “Privacy considerations” subsection if helpful.

blockscout_mcp_server/client_meta.py (1)

21-61: Harden header extraction and simplify case-insensitive lookup

Starlette Headers are case-insensitive; you can rely on that and pass a default to get, avoiding a second lookup and an extra try/except layer.

-        # Read User-Agent from HTTP request (if present)
-        try:
-            request = getattr(getattr(ctx, "request_context", None), "request", None)
-            if request is not None:
-                headers = request.headers or {}
-                ua = headers.get("user-agent") or headers.get("User-Agent") or ""
-                user_agent = ua
-        except Exception:  # pragma: no cover
-            pass
+        # Read User-Agent from HTTP request (if present)
+        request = getattr(getattr(ctx, "request_context", None), "request", None)
+        if request is not None:
+            headers = request.headers or {}
+            user_agent = headers.get("user-agent", "")  # case-insensitive on Starlette Headers

Optional: log at debug level on unexpected ctx shapes instead of broad swallowing to aid troubleshooting.

blockscout_mcp_server/server.py (1)

123-131: Use logger instead of print for startup messages

Startup messages should go through the configured logger for consistency with logging_utils.

-            print(f"Starting Blockscout MCP Server with REST API on {http_host}:{http_port}")
+            logger.info("Starting Blockscout MCP Server with REST API on %s:%s", http_host, http_port)
@@
-            print(f"Starting Blockscout MCP Server in HTTP Streamable mode on {http_host}:{http_port}")
+            logger.info("Starting Blockscout MCP Server in HTTP Streamable mode on %s:%s", http_host, http_port)

Add near the imports:

import logging
logger = logging.getLogger(__name__)
tests/test_analytics_helpers.py (2)

14-27: Add coverage for header precedence when both XFF and X-Real-IP are present

Current tests cover independent cases. Add a case where both headers are present to lock in precedence (XFF over X-Real-IP), and also verify case-insensitivity for header names.

Apply this diff to add two focused tests:

@@
 def test_extract_request_ip_fallbacks():
@@
     assert ip2 == "10.0.0.5"
+
+
+def test_extract_request_ip_precedence_when_both_headers_present():
+    headers = {"X-Forwarded-For": "198.51.100.10, 203.0.113.20", "X-Real-IP": "192.0.2.9"}
+    request = SimpleNamespace(headers=headers, client=SimpleNamespace(host="10.0.0.1"))
+    ctx = SimpleNamespace(request_context=SimpleNamespace(request=request))
+    ip = _extract_request_ip(ctx)
+    # Prefer X-Forwarded-For, left-most IP
+    assert ip == "198.51.100.10"
+
+
+def test_extract_request_ip_case_insensitive_headers():
+    headers = {"X-Forwarded-For": "203.0.113.30"}
+    request = SimpleNamespace(headers=headers, client=SimpleNamespace(host="10.0.0.1"))
+    ctx = SimpleNamespace(request_context=SimpleNamespace(request=request))
+    ip = _extract_request_ip(ctx)
+    assert ip == "203.0.113.30"

30-38: Strengthen distinct_id variability checks across all components

You validate stability and a change on version. Add quick assertions for changes in IP and client_name too.

Apply this diff to extend the test:

 def test_build_distinct_id_stable():
@@
-    c = _build_distinct_id("1.2.3.4", "client", "1.1")
-    assert c != a
+    c = _build_distinct_id("1.2.3.4", "client", "1.1")
+    assert c != a
+    d = _build_distinct_id("5.6.7.8", "client", "1.0")
+    assert d != a
+    e = _build_distinct_id("1.2.3.4", "clientZ", "1.0")
+    assert e != a
tests/test_analytics_source.py (1)

17-21: Clarify test intent and add edge-case for empty explicit marker

The function doesn’t inspect session to decide source; it defaults to "mcp" regardless. The comment suggests session presence influences the result, which could mislead future readers. Also, add an explicit-empty-marker test to lock in the fallback behavior.

Apply this diff to adjust the comment and add an edge-case test:

 def test_determine_call_source_mcp_when_session_present():
-    # Even with no explicit marker, presence of client_params should be treated as mcp
+    # No explicit marker still defaults to 'mcp' regardless of session presence
     session = SimpleNamespace(client_params=SimpleNamespace())
     ctx = SimpleNamespace(session=session)
     assert _determine_call_source(ctx) == "mcp"
+
+
+def test_determine_call_source_empty_string_defaults_to_mcp():
+    ctx = SimpleNamespace(call_source="")
+    assert _determine_call_source(ctx) == "mcp"
tests/tools/test_decorators.py (1)

58-83: Reuse mock_ctx fixture for context instead of crafting a separate MagicMock

Keeps tests uniform and aligns with guidelines for tests/tools. You can still inject the session on the fixture.

Apply this diff:

-async def test_log_tool_invocation_mcp_context(caplog: pytest.LogCaptureFixture) -> None:
+async def test_log_tool_invocation_mcp_context(caplog: pytest.LogCaptureFixture, mock_ctx: Context) -> None:
@@
-    full_mock_ctx = MagicMock()
-    full_mock_ctx.session = mock_session
-
-    await dummy_tool(1, ctx=full_mock_ctx)
+    mock_ctx.session = mock_session
+    await dummy_tool(1, ctx=mock_ctx)
tests/test_client_meta.py (1)

30-39: Add test for User-Agent fallback when client name is undefined

extract_client_meta_from_ctx falls back to User-Agent as client_name when clientInfo.name is missing. Add a test to lock this behavior in.

Apply this diff to add the test:

 def test_extract_client_meta_partial():
@@
     assert meta.protocol == UNKNOWN_PROTOCOL_VERSION
+
+
+def test_extract_client_meta_uses_user_agent_when_name_missing():
+    # No clientInfo; user agent present in HTTP request
+    headers = {"User-Agent": "ua-test/9.9.9"}
+    request = SimpleNamespace(headers=headers)
+    ctx = SimpleNamespace(request_context=SimpleNamespace(request=request))
+
+    meta = extract_client_meta_from_ctx(ctx)
+    assert meta.name == "ua-test/9.9.9"
+    assert meta.version == UNDEFINED_CLIENT_VERSION
+    assert meta.protocol == UNKNOWN_PROTOCOL_VERSION
tests/test_analytics.py (1)

24-32: Avoid direct writes to a private module attribute when resetting state

Directly mutating _mp_client works but couples tests to an internal detail. Prefer a small testing helper (e.g., analytics._reset_for_tests()) or, at minimum, using monkeypatch to set the attribute.

Apply this diff to utilize monkeypatch for setting the private attribute in the autouse fixture:

-@pytest.fixture(autouse=True)
-def reset_mode_and_client():
-    analytics.set_http_mode(False)
-    # Ensure private module state is reset between tests
-    analytics._mp_client = None  # type: ignore[attr-defined]
-    yield
-    analytics.set_http_mode(False)
-    analytics._mp_client = None  # type: ignore[attr-defined]
+@pytest.fixture(autouse=True)
+def reset_mode_and_client(monkeypatch):
+    analytics.set_http_mode(False)
+    # Ensure private module state is reset between tests
+    monkeypatch.setattr(analytics, "_mp_client", None, raising=False)  # type: ignore[attr-defined]
+    yield
+    analytics.set_http_mode(False)
+    monkeypatch.setattr(analytics, "_mp_client", None, raising=False)  # type: ignore[attr-defined]
SPEC.md (1)

496-497: Clarify the fallback behavior for client identification.

The documentation states that the server falls back to the HTTP User-Agent header when the client name cannot be determined. Consider adding more context about when this fallback occurs and any limitations (e.g., User-Agent might not always be available in stdio mode).

blockscout_mcp_server/analytics.py (2)

17-22: Consider more specific exception handling for import errors.

While the current approach handles missing Mixpanel gracefully, catching all exceptions might hide other import-related issues. Consider catching only ImportError specifically.

 try:
     # Import lazily; tests will mock this
     from mixpanel import Consumer, Mixpanel
-except Exception:  # pragma: no cover - import errors covered by no-op behavior in tests
+except ImportError:  # pragma: no cover - import errors covered by no-op behavior in tests
     Consumer = object  # type: ignore[assignment]
     Mixpanel = object  # type: ignore[assignment]

119-176: Well-structured tracking function with proper gating and error handling.

The function implements multiple layers of safety:

  1. HTTP mode gating
  2. Mixpanel client availability check
  3. Comprehensive exception handling
  4. Flexible metadata sourcing (passed or extracted)

The TODO comment on line 160 should be addressed after initial validation.

Would you like me to create an issue to track the removal of the temporary logging statement after Mixpanel analytics validation is complete?

blockscout_mcp_server/api/routes.py (1)

241-243: Verify parameter usage in deprecated endpoint.

The get_address_logs_rest function accepts a request parameter but doesn't use it since it immediately returns a deprecation response. While this is harmless, consider if the request should be used for logging deprecation attempts.

Consider logging deprecation attempts for monitoring:

 async def get_address_logs_rest(request: Request) -> Response:
     """REST wrapper for the get_address_logs tool. This endpoint is deprecated."""
+    # Could log the deprecation attempt with client info from request
     return create_deprecation_response()
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5549d50 and c4d427f.

📒 Files selected for processing (19)
  • .env.example (1 hunks)
  • AGENTS.md (2 hunks)
  • Dockerfile (1 hunks)
  • SPEC.md (1 hunks)
  • blockscout_mcp_server/__init__.py (1 hunks)
  • blockscout_mcp_server/analytics.py (1 hunks)
  • blockscout_mcp_server/api/dependencies.py (2 hunks)
  • blockscout_mcp_server/api/routes.py (6 hunks)
  • blockscout_mcp_server/client_meta.py (1 hunks)
  • blockscout_mcp_server/config.py (2 hunks)
  • blockscout_mcp_server/server.py (2 hunks)
  • blockscout_mcp_server/tools/decorators.py (2 hunks)
  • pyproject.toml (2 hunks)
  • pytest.ini (1 hunks)
  • tests/test_analytics.py (1 hunks)
  • tests/test_analytics_helpers.py (1 hunks)
  • tests/test_analytics_source.py (1 hunks)
  • tests/test_client_meta.py (1 hunks)
  • tests/tools/test_decorators.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (14)
**/*.py

📄 CodeRabbit Inference Engine (.cursor/rules/000-role-and-task.mdc)

**/*.py: The MCP server must be implemented in Python, as you are a senior Python developer and the expertise is in Python.
The MCP server must wrap Blockscout APIs and expose blockchain data (balances, tokens, NFTs, contract metadata) via the Model Context Protocol (MCP).
The MCP server must communicate with AI agents/chat applications through stdin.

**/*.py: Regular Python modules should generally not exceed 500 lines of code (LOC). If a module approaches this limit, consider splitting it into multiple focused modules (e.g., address_tools.py and address_tools_advanced.py) to maintain readability and logical organization.
ALL import statements must be placed at the top of the Python module, immediately after the module docstring (if present) and before any other code. Never insert imports inline near where the functionality is used. Follow PEP 8 import order.
ALL linting and formatting issues must be resolved before committing or pushing code. Use the Ruff rules defined in 300-ruff-lint-and-format.mdc to identify and fix issues.

**/*.py: Always run ruff check . --fix and ruff format . on generated code before suggesting commits or opening a PR
Avoid using # noqa: E501 for ordinary code lines; split long lines instead. Only use # noqa: E501 for docstrings or string literals that must exceed 120 characters.
Use Ruff to enforce a 120-character line length, compatible with Black formatting

Files:

  • blockscout_mcp_server/api/dependencies.py
  • tests/test_analytics_source.py
  • tests/test_client_meta.py
  • blockscout_mcp_server/server.py
  • tests/test_analytics_helpers.py
  • blockscout_mcp_server/client_meta.py
  • blockscout_mcp_server/analytics.py
  • tests/test_analytics.py
  • tests/tools/test_decorators.py
  • blockscout_mcp_server/__init__.py
  • blockscout_mcp_server/config.py
  • blockscout_mcp_server/api/routes.py
  • blockscout_mcp_server/tools/decorators.py
{Dockerfile,docker-compose.yml}

📄 CodeRabbit Inference Engine (.cursor/rules/000-role-and-task.mdc)

The MCP server must be able to run locally or through Docker.

Files:

  • Dockerfile
Dockerfile

📄 CodeRabbit Inference Engine (.cursor/rules/110-new-mcp-tool.mdc)

Dockerfile: Add new API environment variables to the Dockerfile when introducing a new external API.
For each new paginated tool, add the page size environment variable to the Dockerfile.

Files:

  • Dockerfile
tests/test_*.py

📄 CodeRabbit Inference Engine (.cursor/rules/200-development-testing-workflow.mdc)

Create or update the appropriate unit test file when adding new functionality or modifying existing code: Other modules in tests/test_{module_name}.py

Files:

  • tests/test_analytics_source.py
  • tests/test_client_meta.py
  • tests/test_analytics_helpers.py
  • tests/test_analytics.py
.env.example

📄 CodeRabbit Inference Engine (.cursor/rules/110-new-mcp-tool.mdc)

.env.example: Add new API environment variables to .env.example when introducing a new external API.
For each new paginated tool, add the page size environment variable to .env.example.

Files:

  • .env.example
blockscout_mcp_server/server.py

📄 CodeRabbit Inference Engine (.cursor/rules/110-new-mcp-tool.mdc)

Register each new tool in blockscout_mcp_server/server.py by importing the tool function and registering it with the MCP server using the @mcp.tool() decorator.

Files:

  • blockscout_mcp_server/server.py
tests/tools/test_*.py

📄 CodeRabbit Inference Engine (.cursor/rules/200-development-testing-workflow.mdc)

Create or update the appropriate unit test file when adding new functionality or modifying existing code: Tool functions in tests/tools/test_{tool_module}.py

Files:

  • tests/tools/test_decorators.py
tests/tools/*

📄 CodeRabbit Inference Engine (.cursor/rules/210-unit-testing-guidelines.mdc)

tests/tools/*: Each unit test in tests/tools/* must be narrow and specific; a single test should verify one specific behavior or scenario. If a test covers multiple scenarios or input parameter groups, split it into separate tests.
Use the mock_ctx pytest fixture from tests/conftest.py for mocking the MCP Context object in tests; do not create manual MagicMock instances for the context within test functions.
When testing tools that return a ToolResponse object, do not parse JSON from string results in your test. Instead, mock the serialization function (json.dumps) if used internally, and make assertions on the structured ToolResponse object and its attributes.
When testing tools that transform a list of items, programmatically generate the expected_result from the mock_api_response to keep tests maintainable, while still documenting the transformation logic.
Always verify the number of calls to mock_ctx.report_progress in tests to ensure progress tracking is tested.
Assert that mocked API helper functions (such as make_blockscout_request) are called exactly once with the correct api_path and params in tests.
For tools using make_request_with_periodic_progress, mock the wrapper itself and assert that it was called with the correct arguments (request_function, request_args, etc.).
Unit test files in tests/tools/* must not exceed 500 lines of code (LOC). If a file approaches this limit, split tests into multiple files to maintain readability and focus.
Write tests covering success scenarios, error cases, and edge cases in unit test files.
Ensure all external API calls in tests are properly mocked using unittest.mock.patch and AsyncMock.
Group related tests using descriptive class names or clear function naming patterns.

Files:

  • tests/tools/test_decorators.py
pyproject.toml

📄 CodeRabbit Inference Engine (.cursor/rules/010-implementation-rules.mdc)

If any new package needs to be installed it needs to be done with 'sudo which uv pip install --system ""' command. Don't forget to update pyproject.toml with the new package.

When updating the version of the MCP server, update the version field in the [project] section of pyproject.toml.

Files:

  • pyproject.toml
blockscout_mcp_server/__init__.py

📄 CodeRabbit Inference Engine (.cursor/rules/130-version-management.mdc)

When updating the version of the MCP server, update the __version__ variable in blockscout_mcp_server/__init__.py.

Files:

  • blockscout_mcp_server/__init__.py
blockscout_mcp_server/config.py

📄 CodeRabbit Inference Engine (.cursor/rules/110-new-mcp-tool.mdc)

blockscout_mcp_server/config.py: Add new API endpoint configuration variables (URL, timeout, API key) to the ServerConfig class in blockscout_mcp_server/config.py when introducing a new external API.
For each new paginated tool, add a dedicated page size configuration variable to blockscout_mcp_server/config.py.

Files:

  • blockscout_mcp_server/config.py
blockscout_mcp_server/api/routes.py

📄 CodeRabbit Inference Engine (.cursor/rules/150-rest-api-implementation.mdc)

blockscout_mcp_server/api/routes.py: REST handler functions in api/routes.py MUST be named after the MCP tool they wrap, with an _rest suffix (e.g., get_block_info_rest).
All request parameter extraction and validation MUST be handled by the extract_and_validate_params helper from api/helpers.py.
All REST handlers MUST be decorated with the @handle_rest_errors decorator from api/helpers.py. Do not implement custom try...except blocks inside the handlers.
All REST API endpoints MUST be registered under the /v1/ path prefix in register_api_routes using the _add_v1_tool_route helper.

When implementing API routes that serve static files (e.g., index.html or llms.txt), load these files once during module import or server startup and store their contents in module-level variables. Route handlers should return the cached data instead of reading from disk on each request. Provide fallback error handling if preloading fails so the server responds gracefully.

Files:

  • blockscout_mcp_server/api/routes.py
AGENTS.md

📄 CodeRabbit Inference Engine (.cursor/rules/110-new-mcp-tool.mdc)

Update AGENTS.md to document new or modified tool modules, including updates to the directory tree and examples sections.

Files:

  • AGENTS.md
blockscout_mcp_server/tools/*.py

📄 CodeRabbit Inference Engine (.cursor/rules/110-new-mcp-tool.mdc)

blockscout_mcp_server/tools/*.py: Create or modify a tool module file in blockscout_mcp_server/tools/ for each new tool, using @log_tool_invocation to decorate each tool function.
All tools MUST return a strongly-typed ToolResponse[YourDataModel] instead of generic ToolResponse[dict].
For tools that query Blockscout API, use get_blockscout_base_url for dynamic chain resolution and make_blockscout_request for API calls.
For tools that use fixed API endpoints (like BENS), use the appropriate request helper (e.g., make_bens_request) from tools/common.py.
All tools MUST return a standardized ToolResponse[YourDataModel] object using the build_tool_response helper.
For paginated tools, accept an optional cursor argument and use apply_cursor_to_params to handle incoming cursors.
For paginated tools, use create_items_pagination from tools/common.py to handle slicing and pagination in responses.
For paginated tools, include the exact notice 'SUPPORTS PAGINATION: If response includes 'pagination' field, use the provided next_call to get additional pages.' in the tool docstring.
When returning addresses from Blockscout API responses, simplify address objects to a single address string in the tool output.
Truncate large data fields (such as raw 'data' or deeply nested values) in tool responses to save LLM context, and add notes about truncation.
Recursively truncate long strings in nested data structures in tool responses, replacing them with a structured object to signal truncation and adding notes.
Always raise exceptions for error conditions (e.g., ValueError, RuntimeError, TimeoutError) instead of returning ToolResponse objects with error messages in notes.
Use the report_and_log_progress helper from tools/common.py for all progress reporting in tool functions, instead of calling ctx.report_progress directly.
When making multiple independent API calls in a tool, use asyncio.gather with return_exceptions=True for concurrent execution and proper error handling.

Files:

  • blockscout_mcp_server/tools/decorators.py
🧠 Learnings (20)
📚 Learning: 2025-07-22T00:13:40.792Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/210-unit-testing-guidelines.mdc:0-0
Timestamp: 2025-07-22T00:13:40.792Z
Learning: Applies to tests/tools/* : Use the `mock_ctx` pytest fixture from tests/conftest.py for mocking the MCP Context object in tests; do not create manual MagicMock instances for the context within test functions.

Applied to files:

  • blockscout_mcp_server/api/dependencies.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to Dockerfile : Add new API environment variables to the Dockerfile when introducing a new external API.

Applied to files:

  • Dockerfile
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/config.py : Add new API endpoint configuration variables (URL, timeout, API key) to the ServerConfig class in blockscout_mcp_server/config.py when introducing a new external API.

Applied to files:

  • Dockerfile
  • blockscout_mcp_server/config.py
📚 Learning: 2025-07-22T00:11:07.554Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/000-role-and-task.mdc:0-0
Timestamp: 2025-07-22T00:11:07.554Z
Learning: Applies to **/*.py : The MCP server must wrap Blockscout APIs and expose blockchain data (balances, tokens, NFTs, contract metadata) via the Model Context Protocol (MCP).

Applied to files:

  • Dockerfile
  • pyproject.toml
  • blockscout_mcp_server/__init__.py
  • SPEC.md
  • AGENTS.md
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to .env.example : Add new API environment variables to .env.example when introducing a new external API.

Applied to files:

  • .env.example
📚 Learning: 2025-07-22T00:13:24.829Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/200-development-testing-workflow.mdc:0-0
Timestamp: 2025-07-22T00:13:24.829Z
Learning: Applies to tests/integration/test_common_helpers.py : Add integration tests for helper functions in tests/integration/test_common_helpers.py when modifying helper functions in tools/common.py

Applied to files:

  • tests/test_analytics_helpers.py
📚 Learning: 2025-07-22T00:14:04.976Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/220-integration-testing-guidelines.mdc:0-0
Timestamp: 2025-07-22T00:14:04.976Z
Learning: Applies to tests/integration/*.py : All integration test functions must be decorated with pytest.mark.integration.

Applied to files:

  • pytest.ini
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : Create or modify a tool module file in blockscout_mcp_server/tools/ for each new tool, using log_tool_invocation to decorate each tool function.

Applied to files:

  • blockscout_mcp_server/analytics.py
📚 Learning: 2025-07-22T00:13:40.792Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/210-unit-testing-guidelines.mdc:0-0
Timestamp: 2025-07-22T00:13:40.792Z
Learning: Applies to tests/tools/* : Ensure all external API calls in tests are properly mocked using unittest.mock.patch and AsyncMock.

Applied to files:

  • tests/tools/test_decorators.py
📚 Learning: 2025-07-22T00:12:19.870Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/130-version-management.mdc:0-0
Timestamp: 2025-07-22T00:12:19.870Z
Learning: Use the exact same version string in both `pyproject.toml` and `blockscout_mcp_server/__init__.py`.

Applied to files:

  • pyproject.toml
  • blockscout_mcp_server/__init__.py
📚 Learning: 2025-07-22T00:12:19.870Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/130-version-management.mdc:0-0
Timestamp: 2025-07-22T00:12:19.870Z
Learning: Applies to blockscout_mcp_server/__init__.py : When updating the version of the MCP server, update the `__version__` variable in `blockscout_mcp_server/__init__.py`.

Applied to files:

  • pyproject.toml
  • blockscout_mcp_server/__init__.py
📚 Learning: 2025-07-22T00:12:19.870Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/130-version-management.mdc:0-0
Timestamp: 2025-07-22T00:12:19.870Z
Learning: Applies to pyproject.toml : When updating the version of the MCP server, update the `version` field in the `[project]` section of `pyproject.toml`.

Applied to files:

  • pyproject.toml
  • blockscout_mcp_server/__init__.py
📚 Learning: 2025-07-22T00:12:19.870Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/130-version-management.mdc:0-0
Timestamp: 2025-07-22T00:12:19.870Z
Learning: Update both `pyproject.toml` and `blockscout_mcp_server/__init__.py` simultaneously to maintain consistency across the codebase.

Applied to files:

  • pyproject.toml
  • blockscout_mcp_server/__init__.py
📚 Learning: 2025-07-22T00:13:24.829Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/200-development-testing-workflow.mdc:0-0
Timestamp: 2025-07-22T00:13:24.829Z
Learning: Ensure your changes are well-tested by checking test coverage using pytest --cov=blockscout_mcp_server --cov-report=term-missing

Applied to files:

  • pyproject.toml
  • blockscout_mcp_server/__init__.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/models.py : When defining Pydantic models for external API responses, use ConfigDict(extra="allow") to permit additional fields.

Applied to files:

  • blockscout_mcp_server/config.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/server.py : Register each new tool in blockscout_mcp_server/server.py by importing the tool function and registering it with the MCP server using the mcp.tool() decorator.

Applied to files:

  • SPEC.md
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : All tools MUST return a standardized ToolResponse[YourDataModel] object using the build_tool_response helper.

Applied to files:

  • SPEC.md
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : For tools that query Blockscout API, use get_blockscout_base_url for dynamic chain resolution and make_blockscout_request for API calls.

Applied to files:

  • SPEC.md
📚 Learning: 2025-07-22T00:12:50.427Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/150-rest-api-implementation.mdc:0-0
Timestamp: 2025-07-22T00:12:50.427Z
Learning: Applies to blockscout_mcp_server/api/routes.py : REST handler functions in api/routes.py MUST be named after the MCP tool they wrap, with an _rest suffix (e.g., get_block_info_rest).

Applied to files:

  • blockscout_mcp_server/api/routes.py
📚 Learning: 2025-07-29T04:02:27.836Z
Learnt from: CR
PR: blockscout/mcp-server#0
File: .cursor/rules/110-new-mcp-tool.mdc:0-0
Timestamp: 2025-07-29T04:02:27.836Z
Learning: Applies to blockscout_mcp_server/tools/*.py : For tools that use fixed API endpoints (like BENS), use the appropriate request helper (e.g., make_bens_request) from tools/common.py.

Applied to files:

  • blockscout_mcp_server/api/routes.py
🧬 Code Graph Analysis (9)
tests/test_analytics_source.py (1)
blockscout_mcp_server/analytics.py (1)
  • _determine_call_source (101-116)
tests/test_client_meta.py (1)
blockscout_mcp_server/client_meta.py (1)
  • extract_client_meta_from_ctx (21-61)
blockscout_mcp_server/server.py (1)
blockscout_mcp_server/analytics.py (1)
  • set_http_mode (34-47)
tests/test_analytics_helpers.py (1)
blockscout_mcp_server/analytics.py (2)
  • _build_distinct_id (94-98)
  • _extract_request_ip (71-91)
blockscout_mcp_server/analytics.py (2)
blockscout_mcp_server/client_meta.py (2)
  • ClientMeta (14-18)
  • extract_client_meta_from_ctx (21-61)
blockscout_mcp_server/api/dependencies.py (1)
  • info (26-28)
tests/test_analytics.py (2)
blockscout_mcp_server/client_meta.py (1)
  • ClientMeta (14-18)
blockscout_mcp_server/analytics.py (2)
  • set_http_mode (34-47)
  • track_tool_invocation (119-175)
tests/tools/test_decorators.py (2)
blockscout_mcp_server/tools/decorators.py (1)
  • log_tool_invocation (13-49)
tests/conftest.py (1)
  • mock_ctx (8-13)
blockscout_mcp_server/api/routes.py (1)
blockscout_mcp_server/api/dependencies.py (1)
  • get_mock_context (35-37)
blockscout_mcp_server/tools/decorators.py (2)
blockscout_mcp_server/client_meta.py (1)
  • extract_client_meta_from_ctx (21-61)
blockscout_mcp_server/analytics.py (1)
  • track_tool_invocation (119-175)
🪛 GitHub Check: Docker build and docker push
Dockerfile

[warning] 35-35: Sensitive data should not be used in the ARG or ENV commands
SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "BLOCKSCOUT_MIXPANEL_TOKEN")
More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

🪛 dotenv-linter (3.3.0)
.env.example

[warning] 41-41: [QuoteCharacter] The value has quote characters (', ")


[warning] 42-42: [QuoteCharacter] The value has quote characters (', ")


[warning] 42-42: [UnorderedKey] The BLOCKSCOUT_MIXPANEL_API_HOST key should go before the BLOCKSCOUT_MIXPANEL_TOKEN key

🪛 LanguageTool
SPEC.md

[style] ~515-~515: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...available). - MCP client version. - MCP protocol version. - Tool arguments (c...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🔇 Additional comments (22)
blockscout_mcp_server/__init__.py (1)

3-3: Version consistency confirmed
The version in blockscout_mcp_server/__init__.py (0.8.0-dev) matches the version field in pyproject.toml. Ready to merge.

pytest.ini (1)

3-3: Pytest ignore path addition is fine.
Ignoring a temp directory during collection is reasonable and non-invasive.

pyproject.toml (2)

3-3: Project version updated appropriately.
Aligned with blockscout_mcp_server/init.py and PR scope.


15-16: Mixpanel runtime dependency addition acknowledged.
Pinning Mixpanel is appropriate given the new analytics integration.

blockscout_mcp_server/api/dependencies.py (1)

17-19: Good: Exposes request_context for analytics

Exposing request_context aligned with the intended analytics shape without coupling to Starlette types at runtime.

blockscout_mcp_server/config.py (1)

38-41: Mixpanel config fields added; ensure documented and default handling

Fields look fine; analytics should treat empty strings as disabled (as per analytics.set_http_mode). Ensure AGENTS.md and .env.example include:

  • BLOCKSCOUT_MIXPANEL_TOKEN
  • BLOCKSCOUT_MIXPANEL_API_HOST (optional, e.g., api-eu.mixpanel.com)

Do you want me to add the entries to AGENTS.md and .env.example in this PR?

blockscout_mcp_server/client_meta.py (1)

8-10: Sensible sentinel defaults

Using "N/A" and "Unknown" sentinels keeps logs/analytics consistent and easy to filter.

blockscout_mcp_server/server.py (1)

134-135: Analytics enablement in HTTP mode looks correct

Gated enablement aligns with analytics.set_http_mode design and avoids side effects in stdio mode.

tests/test_analytics_helpers.py (1)

6-12: LGTM: Correct precedence for X-Forwarded-For and left-most IP parsing

The test accurately verifies preference for X-Forwarded-For and correctly selects the left-most IP.

tests/test_analytics_source.py (2)

6-9: LGTM: Explicit source marker honored

The test correctly asserts that an explicit call_source of "rest" is returned as-is.


11-15: LGTM: Default to MCP when no explicit marker

Matches the current implementation which defaults to "mcp" in absence of a marker.

tests/test_client_meta.py (2)

11-20: LGTM: Full metadata extraction happy-path

The assertions match the extraction logic for name, version, and protocol.


22-28: LGTM: Defaults when everything is missing

Covers the baseline defaults for name/version/protocol.

tests/test_analytics.py (1)

7-8: No action needed: ClientMeta is correctly re-exported
blockscout_mcp_server/analytics.py imports ClientMeta (and extract_client_meta_from_ctx) from client_meta on line 24, and no __all__ is defined to restrict exports. The test import will work as written.

blockscout_mcp_server/tools/decorators.py (3)

7-8: LGTM! Clean integration with the new analytics module.

The imports are properly structured following PEP 8 conventions, with standard library imports first, followed by local imports. The new analytics functionality is cleanly integrated without breaking existing behavior.


24-28: Good use of the centralized metadata extraction helper.

Using the shared extract_client_meta_from_ctx helper ensures consistency across the codebase and eliminates code duplication. The metadata extraction is now properly encapsulated and reusable.


30-40: Excellent defensive programming for analytics tracking.

The try-except block ensures that analytics failures never interrupt tool execution, which is critical for maintaining reliability. The explicit pass of client_meta to track_tool_invocation avoids redundant metadata extraction.

SPEC.md (1)

500-527: Well-documented analytics implementation with clear opt-in requirements.

The Mixpanel analytics section provides comprehensive documentation of the tracking behavior, including:

  • Clear activation requirements (HTTP mode + token)
  • Detailed property tracking
  • Anonymous identity generation approach
  • REST API integration support

The opt-in nature and HTTP-only restriction appropriately limits analytics to scenarios where it makes sense.

blockscout_mcp_server/analytics.py (3)

50-69: Good singleton pattern implementation for Mixpanel client.

The lazy initialization and singleton pattern ensure efficient resource usage. The function properly handles both custom and default API hosts, with appropriate error handling and logging.


71-92: Comprehensive IP extraction with proper header precedence.

The implementation correctly prioritizes proxy headers (X-Forwarded-For, X-Real-IP) over direct client IP, following standard practices for applications behind proxies. The parsing of X-Forwarded-For to extract the leftmost IP is correct.


94-99: No change required: correct use of uuid.NAMESPACE_URL

The function prefixes your fingerprint with a full URL and generates a UUIDv5 from that URL. According to RFC 4122, NameSpace_URL is the appropriate namespace for URL-based identifiers—NameSpace_DNS applies only to raw DNS names. You can ignore the suggestion to switch to uuid.NAMESPACE_DNS.

Likely an incorrect or invalid review comment.

blockscout_mcp_server/api/routes.py (1)

78-85: LGTM! Consistent request context propagation.

All REST wrapper functions now properly accept and forward the request parameter to get_mock_context(), enabling analytics and metadata extraction for REST API calls. The implementation is consistent across all 18 updated handlers.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
blockscout_mcp_server/analytics.py (1)

125-182: Robust tracking implementation with fallback behavior.

The main tracking function properly handles both provided and extracted client metadata, builds comprehensive event properties, and includes defensive error handling to prevent analytics failures from breaking tool functionality. The temporary logging on Line 167-172 will help with initial validation.

Consider adding a configuration flag to control the verbose logging on Lines 167-172, as this could become noisy in production:

        # TODO: Remove this log after validating Mixpanel analytics end-to-end
-        logger.info(
-            "Mixpanel event prepared: distinct_id=%s tool=%s properties=%s",
-            distinct_id,
-            tool_name,
-            properties,
-        )
+        if getattr(config, "mixpanel_debug_logging", False):
+            logger.info(
+                "Mixpanel event prepared: distinct_id=%s tool=%s properties=%s",
+                distinct_id,
+                tool_name,
+                properties,
+            )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51b51aa and 2158e92.

📒 Files selected for processing (7)
  • blockscout_mcp_server/analytics.py (1 hunks)
  • blockscout_mcp_server/client_meta.py (1 hunks)
  • blockscout_mcp_server/config.py (2 hunks)
  • tests/test_analytics.py (1 hunks)
  • tests/test_analytics_helpers.py (1 hunks)
  • tests/test_analytics_source.py (1 hunks)
  • tests/test_client_meta.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • tests/test_analytics_source.py
  • tests/test_analytics.py
  • tests/test_client_meta.py
  • tests/test_analytics_helpers.py
  • blockscout_mcp_server/client_meta.py
  • blockscout_mcp_server/config.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit Inference Engine (.cursor/rules/000-role-and-task.mdc)

**/*.py: The MCP server must be implemented in Python, as you are a senior Python developer and the expertise is in Python.
The MCP server must wrap Blockscout APIs and expose blockchain data (balances, tokens, NFTs, contract metadata) via the Model Context Protocol (MCP).
The MCP server must communicate with AI agents/chat applications through stdin.

**/*.py: Regular Python modules should generally not exceed 500 lines of code (LOC). If a module approaches this limit, consider splitting it into multiple focused modules (e.g., address_tools.py and address_tools_advanced.py) to maintain readability and logical organization.
ALL import statements must be placed at the top of the Python module, immediately after the module docstring (if present) and before any other code. Never insert imports inline near where the functionality is used. Follow PEP 8 import order.
ALL linting and formatting issues must be resolved before committing or pushing code. Use the Ruff rules defined in 300-ruff-lint-and-format.mdc to identify and fix issues.

**/*.py: Always run ruff check . --fix and ruff format . on generated code before suggesting commits or opening a PR
Avoid using # noqa: E501 for ordinary code lines; split long lines instead. Only use # noqa: E501 for docstrings or string literals that must exceed 120 characters.
Use Ruff to enforce a 120-character line length, compatible with Black formatting

Files:

  • blockscout_mcp_server/analytics.py
🧬 Code Graph Analysis (1)
blockscout_mcp_server/analytics.py (2)
blockscout_mcp_server/client_meta.py (3)
  • ClientMeta (14-18)
  • extract_client_meta_from_ctx (43-79)
  • get_header_case_insensitive (21-40)
blockscout_mcp_server/api/dependencies.py (1)
  • info (33-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run Integration Tests
  • GitHub Check: Docker build and docker push
🔇 Additional comments (7)
blockscout_mcp_server/analytics.py (7)

1-9: Comprehensive module docstring with clear gating conditions.

The module docstring clearly explains the tracking conditions and the deterministic distinct_id approach, which is helpful for understanding the analytics behavior.


17-22: Proper handling of optional Mixpanel dependency.

The lazy import with graceful fallback handles cases where Mixpanel is not installed, which is good for optional analytics functionality.


38-52: HTTP mode gating implementation looks good.

The function correctly initializes the Mixpanel client when HTTP mode is enabled and provides appropriate logging for both enabled and disabled states. The best-effort client initialization helps validate configuration at startup.


54-73: Singleton pattern with proper error handling.

The Mixpanel client singleton implementation correctly handles configuration and gracefully degrades on initialization failures without breaking the application flow.


75-98: Comprehensive IP extraction following proxy standards.

The IP extraction logic correctly prioritizes proxy headers (X-Forwarded-For, X-Real-IP) before falling back to direct client IP, following standard practices for applications behind load balancers or proxies.


100-105: Deterministic ID generation using UUID5.

The use of UUID5 with a namespace and composite fingerprint ensures consistent distinct_id generation across sessions while maintaining user privacy through deterministic hashing.


107-123: Clear call source determination logic.

The function correctly prioritizes explicit markers from REST contexts while defaulting to 'mcp' for standard MCP calls, providing good traceability of request origins.

@akolotov akolotov requested a review from Copilot August 13, 2025 21:08
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces opt-in Mixpanel analytics tracking for MCP tool invocations in HTTP modes. The implementation adds anonymous user tracking based on stable client fingerprints, captures tool usage patterns, and provides comprehensive client metadata extraction capabilities.

  • Analytics tracking is enabled only in HTTP modes (MCP-over-HTTP and REST) when BLOCKSCOUT_MIXPANEL_TOKEN is set
  • Anonymous stable client identification using IP, client name, and version fingerprints
  • Comprehensive client metadata extraction with fallback to User-Agent headers

Reviewed Changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
pyproject.toml Adds Mixpanel dependency and bumps version to 0.8.0-dev
blockscout_mcp_server/analytics.py Core analytics implementation with Mixpanel integration
blockscout_mcp_server/client_meta.py Shared client metadata extraction utilities
blockscout_mcp_server/tools/decorators.py Integrates analytics tracking into existing log decorator
blockscout_mcp_server/server.py Enables analytics in HTTP mode
blockscout_mcp_server/config.py Adds Mixpanel configuration settings
blockscout_mcp_server/api/* Updates REST API to pass request context for analytics
tests/* Comprehensive test coverage for analytics functionality

You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
README.md (1)

174-176: Fix README example: require token contract + concrete output; confirm Kinto chain resolvability

Short: the README prompt at README.md:174-176 is vague and grammatically awkward — require the token contract address and explicit output fields (tx hash, timestamp, amount, sender). I searched the repo and found "Kinto" only in README, so I cannot confirm the chain is resolvable via Chainscout/Blockscout.

Please update/verify:

  • README.md:174-176 — clarify wording and add token contract + desired return fields.

Suggested diff:

- When the most recent reward distribution of Kinto token was made to the wallet
- `0x7D467D99028199D99B1c91850C4dea0c82aDDF52` in Kinto chain?
+ When was the most recent transfer of the KINTO token (provide the token contract address) to the wallet
+ `0x7D467D99028199D99B1c91850C4dea0c82aDDF52` on the Kinto chain? Return tx hash, timestamp, amount, and sender.

Action required from author:

  • Confirm that "Kinto" is resolvable by your Chainscout/Blockscout configuration (provide chain_id or Blockscout URL), or replace with a known chain.
  • Provide the KINTO token contract address to use in the example so the README can show a concrete query/output.
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2158e92 and 254c25e.

📒 Files selected for processing (2)
  • README.md (2 hunks)
  • blockscout_mcp_server/analytics.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • blockscout_mcp_server/analytics.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run Integration Tests
  • GitHub Check: Docker build and docker push
🔇 Additional comments (1)
README.md (1)

160-162: Specify explicit chain_id and expected transaction-summary fields (confirm Chainscout discovery)

All tools require a chain_id. I searched the repo: "Redstone" appears only in README.md (line ~161) and there is no local mapping / RECOMMENDED_CHAINS entry for it, so I could not verify Chainscout discoverability from the repository. Please replace the ambiguous README example with an explicit chain_id and list the expected output fields.

Files to update:

  • README.md (lines ~160-162) — replace the ambiguous prompt with an explicit chain_id and required fields.
  • If Redstone should be supported, add/confirm its chain_id in your chain mappings (RECOMMENDED_CHAINS / constants) or ensure get_blockscout_base_url(chain_id) resolves it via Chainscout.

Suggested diff:

- Tell me more about the transaction `0xf8a55721f7e2dcf85690aaf81519f7bc820bc58a878fa5f81b12aef5ccda0efb`
- on Redstone rollup.
+ Summarize transaction `0xf8a55721f7e2dcf85690aaf81519f7bc820bc58a878fa5f81b12aef5ccda0efb` on Redstone (chain_id=<CHAIN_ID>).
+ Include: status, from, to, method name, decoded input parameters, native value, gas used/fees, and token transfers.

Please confirm the exact chain_id your server uses for Redstone and whether Chainscout exposes that chain_id so I can re-verify.

@akolotov akolotov merged commit babb439 into main Aug 13, 2025
8 checks passed
@akolotov akolotov deleted the feat/mixpanel-tracking-intro branch August 13, 2025 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add Mixpanel Analytics for Tool Invocation
1 participant