Skip to main content

mcpsignals (Python)

Drop-in instrumentation for MCP servers. Wrap your server, point it at a sink, and it records what agents did with your tools into a database you own - no hosted service, no account.

Requires Python 3.10 or newer. Targets the v2 mcp SDK (MCPServer / low-level Server, mcp>=2.0.0).

Install

pip install mcpsignals
# with a warehouse sink:
pip install "mcpsignals[postgres]"   # or [bigquery], [otlp]

Usage

from mcp.server.mcpserver import MCPServer
from mcpsignals import instrument

server = MCPServer("my-server")
instrument(server, server_name="my-server", server_version="1.0.0")


@server.tool()
def search(query: str) -> str:
    """Search something."""
    return f"results for {query}"

Call instrument() any time before the server starts handling requests. It appends to server.middleware, and that chain is rebuilt from the live list on every request, so calling it before or after your @server.tool() definitions makes no difference. The Node.js package differs here: it wraps registerTool, so it has to run before any tool is registered.

With no sink configured it writes JSON lines to stdout. On a stdio transport pass sinks=[ConsoleSink(stream=sys.stderr)], because the MCP spec reserves stdout for protocol messages. To write to your own warehouse instead:

from mcpsignals import instrument
from mcpsignals.sinks import PostgresSink

instrument(server, server_name="my-server", sinks=[PostgresSink(dsn="postgresql://...")])

Argument capture and redaction

Off by default: no tool arguments are recorded unless you opt in with capture_arguments=True. Even then, by default only argument keys and value types are recorded, never values. See the root README's redaction section before enabling this in anything handling real user data.

Request-scoped runtimes and manual flushing

instrument() returns the server unchanged. handle_for(server) returns an InstrumentHandle with two coroutines: flush() delivers everything buffered so far to every sink, and close() does a final flush, cancels the interval task, and unregisters the atexit hook. The handle is held in a weak registry keyed by the exact server object, so it lives as long as the server does; handle_for() returns None for a server that never went through instrument().

On a long-lived process, ignore the handle: the interval task and the atexit hook flush for you. Neither is reliable on a request-scoped host (a serverless function, or a server built fresh per request): the process can be frozen or discarded right after the response is sent, and atexit does not correspond to "this invocation is ending".

Pass flush_interval_s=None for manual mode. The buffer then skips both the interval task and the atexit hook, so the host owns every flush:

from mcp.server.mcpserver import MCPServer
from mcpsignals import handle_for, instrument


async def handle_request(request):
    server = MCPServer("my-server")
    instrument(
        server,
        server_name="my-server",
        sinks=[...],
        flush_interval_s=None,  # manual mode: the host flushes explicitly
    )

    @server.tool()
    def search(query: str) -> str:
        return f"results for {query}"

    response = await serve(request, server)

    await handle_for(server).flush()  # before returning the response
    return response

Call close() instead of flush() when the server is done for good: at the end of a test, or when a per-request server is discarded. In the default interval mode, close() is also what stops the interval task and removes the atexit hook, so a test suite that instruments many servers does not accumulate either. Both flush() and close() are safe to await more than once.

Known limitation: session_id

mcp 2.0.0's middleware-facing ServerRequestContext does not publicly expose the transport's connection-level session id (only the handler-facing Context class does, via a private accessor middleware doesn't have). session_id on emitted events is therefore only ever populated when intent capture is enabled and the calling agent supplies one - not from the underlying transport connection. See instrument.py for details; this will be revisited if/when upstream exposes it to middleware.

Full docs

See the root README for the redaction model, the sink comparison, and intent capture, and schema/events.md for the event field reference.

Release files for mcpsignals 2.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mcpsignals 2.2.0
File Size Uploaded
mcpsignals-2.2.0.tar.gz 22.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for mcpsignals 2.2.0
File Interpreter ABI Platform
mcpsignals-2.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 43.5 kB

Release files / mcpsignals-2.2.0.tar.gz

Download URL mcpsignals-2.2.0.tar.gz
Size 22.7 kB
Tags Source
SHA-256 checksum
How to use checksums
6fff2a43f4150df0dbed2bc49cc08da4c048899d5d3c117c63dfce8197f45bdd
BLAKE2b-256 checksum
How to use checksums
d84410dd6c29508238baeee7f701dd9c9ed21b002e1f89575d9c5b24987905ac
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.

Transparency log

Release files / mcpsignals-2.2.0-py3-none-any.whl

Download URL mcpsignals-2.2.0-py3-none-any.whl
Size 20.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
dff9a73ad834a136b1e165f3e3adf42c4f47302b7b479cbf49a8ca71b5633f30
BLAKE2b-256 checksum
How to use checksums
58495554dfa96f633158f7e7287e752d720ff8acac1cc9519d242dbfb0a4ba53
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.

Transparency log

Release history Release notifications | RSS feed

2.3.0

2 release files

This release

2.2.0 This release

2 release files

2.1.0

2 release files

2.0.2

2 release files

2.0.1

2 release files

2.0.0

2 release files

1.0.3

2 release files

1.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page