Skip to main content

baton-proxy

Transparent MCP proxy. Wraps a stdio MCP server as a subprocess, or bridges to a remote Streamable-HTTP MCP server (--url); injects an annotation tool and three intent parameters into the handshake, and emits friction events to one or more sinks (stderr, a JSONL file, or a Baton Console).

Zero changes to the underlying MCP server. The proxy is the MCP server from Claude's perspective; the real server is either its child process (stdio) or the endpoint it forwards to (--url).

┌──────────┐      ┌───────────────┐      ┌────────────────────┐
│  Claude  │ ◀──▶ │  baton-proxy  │ ◀──▶ │ your MCP server    │
└──────────┘      └───────┬───────┘      └────────────────────┘
                          │
                          │ async fan-out, pick any subset
                          ▼
   ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
   │  stderr:        │  │  file://        │  │  Baton Console  │
   │  JSONL stream   │  │  JSONL file     │  │  (HTTPS POST)   │
   └─────────────────┘  └─────────────────┘  └─────────────────┘

The docs are at goodtiming.ai/docs.html#proxy: the full configuration reference, what gets emitted, the intent parameters and the sink ladder. This page is the short version.

Quick start

pipx install baton-proxy  # or: pip install baton-proxy

pipx installs the CLI into its own isolated venv and puts baton-proxy on your PATH, so Claude's config can invoke it directly without env activation. Plain pip install works if you already manage your own Python env. Python 3.11+, pure stdlib, no third-party runtime dependencies.

Replace your MCP server entry in Claude's config:

// Before
{ "command": "npx", "args": ["@vendor/mcp-server"] }

// After. Zero-config: events go to stderr + /tmp/baton-proxy.jsonl
{ "command": "baton-proxy", "args": ["--", "npx", "@vendor/mcp-server"] }

That's the entire install. Start a new Claude session and drive the wrapped server.

For a remote server, name it with --url instead of a command after --. The two forms are mutually exclusive, and BATON_UPSTREAM_AUTH_TOKEN is sent upstream as a bearer token:

{
  "command": "baton-proxy",
  "args": ["--url", "https://mcp.example.com/mcp"],
  "env": { "BATON_UPSTREAM_AUTH_TOKEN": "..." }
}

The token goes in the entry's env, not your shell: the MCP client starts the proxy with a fixed six-name environment allowlist, so an exported value never arrives. The upstream then rejects the call and the proxy degrades to a synthetic empty tool list: a server that looks connected and offers nothing.

Either form is started by your MCP client, not by you: the proxy speaks JSON-RPC on stdin, so running it straight from a shell just waits for input.

Try it in one command: scan

Preview the friction an agent is likely to hit on a server you run, with no permanent install and no change to your Claude config:

uvx baton-proxy scan --config github

scan targets a server you've already configured in Claude (by name), reusing that entry's saved credentials. It writes an ephemeral config, drives a headless agent (claude -p, billed to your own auth) through the wrapped server, and renders ./baton-report.md. Everything runs locally: nothing leaves your machine, and you type no secrets. The report is labeled preflight/inferred. It previews likely friction rather than real-user data, which is what the permanent wrap above captures.

A friction report only delivers its insight on a server you actually run, with its real tools, its real auth and your real workflows. That is why scan resolves a configured entry rather than scanning a stranger's server. It reads --config <name> from ~/.claude.json or ./.mcp.json; point at a specific file with --config-file ./.mcp.json.

Where events go

BATON_EVENT_SINK takes a comma-separated list, and the URL scheme picks the sink: stderr: writes JSON Lines to stderr, file:///tmp/events.jsonl appends one JSON object per event, and https://console.example.com POSTs to {url}/v0/events. The default is stderr:,file:///tmp/baton-proxy.jsonl, so a bare install writes only to your own machine.

A misconfigured sink fails loudly at startup rather than silently dropping events. The full variable list, covering timeouts, tenant shape, the upstream token and the intent-parameter mode, is in the configuration reference.

Payload scrubbing

Always on. Nothing turns it off, not an environment variable and not a flag: the proxy constructs its scrubber unconditionally. Tool params, results and error bodies run through the same ruleset the Baton SDK ships: email, Bearer values, sk-* and AKIA* keys, JWTs, North-American-format phone numbers, Luhn-checked card numbers, plus force-redaction on sensitive field names.

It is pattern matching, not a guarantee. A name and a street address pass through untouched. Decide what your server puts in tool params and results on that basis. What it does and does not catch.

Trust properties

  • Open source, Apache 2.0. Auditable end-to-end.
  • Fail-open. A Console outage, a network issue or an instrumentation bug never breaks the MCP pipe. If injection or stripping raises, the message is forwarded unmodified.
  • Outbound-only. The proxy never accepts inbound connections. Events go to the configured sink, either an HTTPS POST out or a local file write, and that is the only egress surface.
  • Source-side scrubbing, on by default, with the limits stated above.
  • Emission off the hot path. Events are enqueued onto a background thread; the I/O pump does not wait for the POST.

Trust model. baton-proxy and the wrapped MCP server run in the same trust domain (same user, vendor's own MCP server). The proxy filters BATON_* out of the upstream subprocess env as a least-privilege measure: the upstream has no need for Baton credentials, and accidental leakage paths such as debug logging or crash-report env dumps should not see them. This is not a cross-process trust boundary: do not use baton-proxy to instrument an MCP server you do not trust.

More

goodtiming.ai/docs.html#proxy The docs: configuration, what it captures, the intent parameters, the Console
docs/SPEC.md The wire protocol, in the baton repo. The contract a collector consumes
baton-sdk The in-process alternative: capture from inside a server you own, with no proxy hop
CHANGELOG.md What has shipped

Apache-2.0.

Metadata

Release files for baton-proxy 0.6.10

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

Source distribution (sdist)

Source distribution for baton-proxy 0.6.10
File Size Uploaded
baton_proxy-0.6.10.tar.gz 390.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for baton-proxy 0.6.10
File Interpreter ABI Platform
baton_proxy-0.6.10-py3-none-any.whl Python 3 none any Details

Total release size: 485.8 kB

Release files / baton_proxy-0.6.10.tar.gz

Download URL baton_proxy-0.6.10.tar.gz
Size 390.7 kB
Tags Source
SHA-256 checksum
How to use checksums
cf1302d7561f4c8f1e40f48264c6ef1d461e42c8e5e9fa9b98166406a487f556
BLAKE2b-256 checksum
How to use checksums
b0a1a5bd151e9a5553fb49fc4be66829822c751a7046ce74d04ae4abe7a9d294
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 18, 2026.

Transparency log

Release files / baton_proxy-0.6.10-py3-none-any.whl

Download URL baton_proxy-0.6.10-py3-none-any.whl
Size 95.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5d796d0cb07720180f2a889d80a4d1056889bed57fcac0c18f0353b892f99f65
BLAKE2b-256 checksum
How to use checksums
cedaffa77cbcdd128983778e2f44f10efb92ee661312678c5d3377cac43e365b
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 18, 2026.

Transparency log

Release history Release notifications | RSS feed

0.6.17

2 release files

0.6.16

2 release files

0.6.12

2 release files

0.6.11

2 release files

This release

0.6.10 This release

2 release files

0.6.9

2 release files

0.6.8

2 release files

0.6.7

2 release files

0.6.6

2 release files

0.6.5

2 release files

0.6.4

2 release files

0.6.3

2 release files

0.6.2

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.5

2 release files

0.5.4

2 release files

0.5.3

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

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