raindrop-mcp
Raindrop instrumentation for Python MCP servers. Works with the official SDK's mcp 1.x (FastMCP) and mcp 2.x (MCPServer), with the standalone fastmcp package (2.x and 3.x+), with a hand-wired low-level mcp.server.Server, and with any server built on them: wrap it once (or launch with raindrop-mcp) and every request the server answers lands in Raindrop as a tool span on an mcp_activity event: tool calls, prompts, resources, and the failures no handler sees (unknown tools, bad arguments, cancelled and dropped requests). Python twin of @raindrop-ai/mcp.
Install
pip install raindrop-mcp
Quick Start
No code changes: launch the server through raindrop-mcp with the write key in the environment.
RAINDROP_WRITE_KEY=... raindrop-mcp python server.py # or: raindrop-mcp python -m my_server
RAINDROP_PROJECT_ID picks the project, RAINDROP_MCP_CAPTURE_CONTENT=false ships names, counts, errors and client identity but never arguments, results or intent, and RAINDROP_MCP_DISABLED=1 turns the launcher off without editing the command. Use either the launcher or wrap(), not both.
In code:
import os
from mcp.server.fastmcp import FastMCP # mcp 2.x: from mcp.server.mcpserver import MCPServer
from raindrop_mcp import RaindropMcp
raindrop = RaindropMcp(api_key=os.environ.get("RAINDROP_API_KEY"))
mcp = raindrop.wrap(FastMCP("my-server"))
@mcp.tool()
def search(query: str) -> str:
...
mcp.run()
Omitting api_key disables shipping (a warning is emitted) but never crashes the server.
Third-party servers usually build their own FastMCP at import time. Wrap the class before importing them, or wrap the instance afterwards (tools already registered are instrumented too):
from mcp.server.fastmcp import FastMCP
from raindrop_mcp import RaindropMcp
RaindropMcp(api_key=...).wrap(FastMCP) # every FastMCP built from now on
import some_mcp_server # noqa: E402
some_mcp_server.mcp.run()
The standalone fastmcp package works the same way:
from fastmcp import FastMCP
from raindrop_mcp import RaindropMcp
mcp = RaindropMcp(api_key=...).wrap(FastMCP("Demo"))
@mcp.tool
def add(a: int, b: int) -> int:
return a + b
A hand-wired low-level server works too: raindrop.wrap(Server("my-server")) on an mcp.server.lowlevel.Server instance or the class.
What is recorded
Every JSON-RPC request the server answers is one call: tools/call (named by the tool), prompts/get <name>, resources/read <uri>, completion/complete, and so on, with mcp.method on the event and span. Failures carry mcp.error_type:
mcp.error_type |
When |
|---|---|
tool_error |
The tool returned an isError result. |
handler_throw |
The tool raised. |
rpc_<code> |
The server answered a JSON-RPC error (unknown tool or prompt, invalid arguments). |
cancelled |
The client cancelled the request before the response. |
closed_before_response |
The connection closed while the request was still pending. |
Lifecycle traffic never ships: initialize, server/discover, subscriptions/listen, ping and every */list, whether they succeed, fail or are still open when the connection closes.
An instrumented client that puts a W3C traceparent in params._meta has it recorded as mcp.traceparent. Credentials in URIs (user:pass@, query strings, fragments) are stripped from labels, arguments and error text.
Turn tracking
MCP has no turn boundary, so by default every tool advertises an optional _turn_id argument and every result ends with a _turn_id: <id> text block. The model copies it on its next call and omits it on a new prompt, so the calls of one prompt share one event (one trace, one tool span per call). The argument is removed before your tool runs. A tool that raises still echoes the id, so the model's next call stays on the same event.
If the client sends _meta["io.modelcontextprotocol/aiInvocation"].turnId (SEP-2817), that id is used and nothing is echoed.
turn_tracking=False leaves schemas and results untouched (this also disables intent); every call is then its own event.
Intent and client
Every tool also advertises an optional _intent argument: one sentence on what the user is trying to accomplish, which the model fills on the first call of a prompt. It is stripped before your tool runs, recorded as the mcp.intent property, and appended to the event input under a Session intent: line after MCP session started. Set intent=False to leave it out. It is free text the model wrote, so the key-based secret redaction does not apply to it; list _intent in redact_arguments to keep the field advertised but out of telemetry.
The MCP client that made the call (clientInfo, e.g. claude-code 2.1.0, Cursor, codex-mcp-client) is recorded as mcp.client_name / mcp.client_version on every span and event. It comes from the per-request _meta of 2026-era clients, or from the initialize handshake on older ones.
Options
| Option | Default | Description |
|---|---|---|
api_key |
– | Raindrop write key. Without one, nothing ships and a warning is emitted. |
endpoint |
https://api.raindrop.ai/v1/ |
Ingest endpoint. |
project_id |
– | Raindrop project slug. |
event_name |
mcp_activity |
Event name. |
turn_tracking |
True |
Advertise and echo _turn_id to group a prompt's calls into one event. |
intent |
True |
Advertise _intent so the model states the user's goal on the first call of a prompt. |
user_id |
OAuth subject → hashed bearer token → session:<id> → anonymous |
Fixed id or callable(call) -> str | None. Returning None skips the call. |
convo_id |
transport session, else the caller's UTC day | Fixed id or callable(call) -> str | None. |
properties |
– | Extra event properties, a dict or callable(call) -> dict. |
redact_arguments |
[] |
Argument names never captured. Keys that look like secrets (password, authorization, api_key, …) are always redacted, and credentials inside URL values (user:pass@host, ?token=) are replaced in every captured string. |
redact_results |
[] |
Tools, prompts or methods (resources/read) whose results are never captured, e.g. ones that return credentials. |
await_delivery |
True |
Wait (at most 5s, off the event loop) for delivery before returning each tool result. Needed on serverless. Set False on long-running servers to keep tool latency unchanged. |
max_text_field_chars |
1,000,000 |
Per-field character cap for captured arguments and results. |
capture_content |
True |
False ships tool names, counts, errors and client identity but never arguments, results or intent. |
protocol |
True |
Record every request the server answers, not only the tools it ran. |
debug |
False |
Log shipping details. |
app_git |
auto-detect | Application Git identity; False disables. |
client |
– | An existing raindrop.Raindrop client to ship through (multi-project setups). |
call passed to the callables is a McpCallContext(tool_name, args, session_id, context), where context is the SDK's per-request Context.
Event properties
mcp.turn_id, mcp.tool (the latest call in the turn), mcp.method, mcp.is_error, mcp.error_type on failures, mcp.traceparent when the client sent one, mcp.client_name / mcp.client_version when the client identified itself, mcp.intent when the model filled _intent, mcp.session_id when the transport has one, plus anything from properties. Tool spans carry the arguments, the result text (or the error), mcp.turn_id, mcp.session_id, the client and the intent.
Known limitations
- Protocol-level calls (prompts, resources, handler-less failures) are recorded after the reply is sent and never wait for delivery; their caller identity comes from the request's HTTP headers (bearer token, session id), not the OAuth subject.
- On standalone
fastmcp2.x a tool that raises cannot echo the id (itsToolResulthas no error form), so the model's next call starts a new event; 3.x+ echoes like the official SDK. _turn_idand_intentare added to the tool's JSON schema. A tool that declares either name itself keeps it: the field is neither advertised by this package nor stripped from the arguments for that tool, and a warning is emitted at registration since that tool then has no intent (or no turn grouping).- Grouping depends on the model echoing
_turn_id; clients that strip unknown result blocks start a new event per call. - Delivery runs on the Raindrop SDK's background thread;
await_deliveryflushes before returning but cannot wait for a flush that thread already started.
Testing
pip install -e ".[dev]" && pytest
The suite runs unchanged against mcp<2 and mcp>=2.
Metadata
Release files for raindrop-mcp 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| raindrop_mcp-0.1.0.tar.gz | 45.7 kB | Without provenance | Details for raindrop_mcp-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| raindrop_mcp-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for raindrop_mcp-0.1.0-py3-none-any.whl |
Total release size: 73.9 kB
Release files / raindrop_mcp-0.1.0.tar.gz
| Download URL | raindrop_mcp-0.1.0.tar.gz |
|---|---|
| Size | 45.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
67a2e673fa8d9446d5f9dba0270147af65f0d368af227b203699cc6729710b76
|
|
BLAKE2b-256 checksum How to use checksums |
8d4e183f17a1a5b727d87bd86a4b307aabc138958bb238bc41d7e265a210bacd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / raindrop_mcp-0.1.0-py3-none-any.whl
| Download URL | raindrop_mcp-0.1.0-py3-none-any.whl |
|---|---|
| Size | 28.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
84b280644cb6107caf33698ab6a3ae45511d4f1cbc01ccc04ac0339d1e473e8b
|
|
BLAKE2b-256 checksum How to use checksums |
7881f7db18c7b152acd7daadc83869cc8913c08c1eb76ba4feed7bceba02d98e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Security for raindrop-mcp 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.