From 3da732d7025c26070aed4951486395ef8b20663e Mon Sep 17 00:00:00 2001 From: Iftach Yakar Date: Tue, 24 Mar 2026 14:40:45 +0200 Subject: [PATCH] feat: add anonymous tool usage analytics + disclose in privacy notice Log tool name (no args, no circuit data) on each invocation via a shared decorator. Disclosure added to hello_quantum privacy_notice and README Privacy & Security section. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 2 ++ src/stim_mcp_server/analytics.py | 17 +++++++++++++++++ src/stim_mcp_server/server.py | 3 +++ src/stim_mcp_server/tools/analysis.py | 6 ++++-- src/stim_mcp_server/tools/circuit_management.py | 8 +++++--- src/stim_mcp_server/tools/health.py | 7 +++++-- src/stim_mcp_server/tools/simulation.py | 4 +++- src/stim_mcp_server/tools/visualization.py | 4 +++- 8 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 src/stim_mcp_server/analytics.py diff --git a/README.md b/README.md index ab776a3..5dab58f 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,8 @@ Circuit IDs are 128-bit random tokens (UUID v4), making them practically impossi **If you care about the privacy of your circuits, run the MCP server locally** (see [Running locally](#running-locally)). The remote server is intended for experimentation and learning, not sensitive work. +Tool invocations on the remote server are logged anonymously (tool name only, no arguments or circuit data) for usage analytics. + ## Examples > "Create a Bell state and sample it 1000 times" diff --git a/src/stim_mcp_server/analytics.py b/src/stim_mcp_server/analytics.py new file mode 100644 index 0000000..2cfa532 --- /dev/null +++ b/src/stim_mcp_server/analytics.py @@ -0,0 +1,17 @@ +"""Tool usage analytics — logs tool name only, no arguments or user data.""" + +from __future__ import annotations + +import functools +import logging + +logger = logging.getLogger("stim_mcp.tools") + + +def log_tool_call(fn): + """Wrap a tool function to emit a structured log line on each invocation.""" + @functools.wraps(fn) + def wrapper(*args, **kwargs): + logger.info("tool_call tool=%s", fn.__name__) + return fn(*args, **kwargs) + return wrapper diff --git a/src/stim_mcp_server/server.py b/src/stim_mcp_server/server.py index 2e5cc82..9b943d9 100644 --- a/src/stim_mcp_server/server.py +++ b/src/stim_mcp_server/server.py @@ -2,8 +2,11 @@ from __future__ import annotations +import logging import os +logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s %(message)s") + from mcp.server.fastmcp import FastMCP from .circuit_store import CircuitStore diff --git a/src/stim_mcp_server/tools/analysis.py b/src/stim_mcp_server/tools/analysis.py index 501c9b5..2695a22 100644 --- a/src/stim_mcp_server/tools/analysis.py +++ b/src/stim_mcp_server/tools/analysis.py @@ -7,6 +7,8 @@ import stim +from stim_mcp_server.analytics import log_tool_call + _store = None @@ -125,5 +127,5 @@ def inject_noise( def register(mcp, store) -> None: global _store _store = store - mcp.tool()(analyze_errors) - mcp.tool()(inject_noise) + mcp.tool()(log_tool_call(analyze_errors)) + mcp.tool()(log_tool_call(inject_noise)) diff --git a/src/stim_mcp_server/tools/circuit_management.py b/src/stim_mcp_server/tools/circuit_management.py index 04cf865..c6b223d 100644 --- a/src/stim_mcp_server/tools/circuit_management.py +++ b/src/stim_mcp_server/tools/circuit_management.py @@ -6,6 +6,8 @@ import stim +from stim_mcp_server.analytics import log_tool_call + SUPPORTED_TASKS = [ "repetition_code:memory", "surface_code:rotated_memory_x", @@ -143,6 +145,6 @@ def generate_circuit( def register(mcp, store) -> None: global _store _store = store - mcp.tool()(create_circuit) - mcp.tool()(append_operation) - mcp.tool()(generate_circuit) + mcp.tool()(log_tool_call(create_circuit)) + mcp.tool()(log_tool_call(append_operation)) + mcp.tool()(log_tool_call(generate_circuit)) diff --git a/src/stim_mcp_server/tools/health.py b/src/stim_mcp_server/tools/health.py index 7e330da..9d676af 100644 --- a/src/stim_mcp_server/tools/health.py +++ b/src/stim_mcp_server/tools/health.py @@ -6,6 +6,8 @@ import stim +from stim_mcp_server.analytics import log_tool_call + _store = None @@ -20,7 +22,8 @@ def hello_quantum() -> str: "This is a shared server. Circuit IDs are 128-bit random tokens " "and are not guessable, but there is no user authentication or " "access control. Do not use this server for sensitive circuits. " - "For private use, run the MCP server locally." + "For private use, run the MCP server locally. " + "Tool invocations are logged anonymously (tool name only) for usage analytics." ), } ) @@ -29,4 +32,4 @@ def hello_quantum() -> str: def register(mcp, store) -> None: global _store _store = store - mcp.tool()(hello_quantum) + mcp.tool()(log_tool_call(hello_quantum)) diff --git a/src/stim_mcp_server/tools/simulation.py b/src/stim_mcp_server/tools/simulation.py index 8c96872..5fb61c9 100644 --- a/src/stim_mcp_server/tools/simulation.py +++ b/src/stim_mcp_server/tools/simulation.py @@ -4,6 +4,8 @@ import json +from stim_mcp_server.analytics import log_tool_call + _store = None @@ -59,4 +61,4 @@ def sample_circuit(circuit_id: str, shots: int = 1000) -> str: def register(mcp, store) -> None: global _store _store = store - mcp.tool()(sample_circuit) + mcp.tool()(log_tool_call(sample_circuit)) diff --git a/src/stim_mcp_server/tools/visualization.py b/src/stim_mcp_server/tools/visualization.py index 9112d9b..a23264f 100644 --- a/src/stim_mcp_server/tools/visualization.py +++ b/src/stim_mcp_server/tools/visualization.py @@ -8,6 +8,8 @@ import cairosvg from mcp.server.fastmcp import Image +from stim_mcp_server.analytics import log_tool_call + _store = None @@ -78,4 +80,4 @@ def get_circuit_diagram( def register(mcp, store) -> None: global _store _store = store - mcp.tool()(get_circuit_diagram) + mcp.tool()(log_tool_call(get_circuit_diagram))