Keep your MCP server quiet on stdout — safe print/logging and a stdout guard so debug output can't corrupt the JSON-RPC stream.
Project description
quietmcp
Keep your MCP server quiet on stdout —
print()and log freely without corrupting the JSON-RPC stream.
On an MCP stdio server, stdout is the protocol — every byte is part of the
JSON-RPC stream the client parses. So the most natural thing in the world, a
print("got here"), silently corrupts that stream and the client breaks with a
baffling parse error. Worse, a third-party library you don't control can print a
progress bar and take your server down with it.
The fix is mundane — send everything that isn't protocol to stderr — but easy to get wrong and easy for a dependency to undo. quietmcp makes it effortless:
import quietmcp
real_stdout = quietmcp.install() # stray writes to stdout now go to stderr
log = quietmcp.get_logger("my-server") # logs to stderr, never stdout
log.info("server starting")
quietmcp.safe_print("debug: tool called") # print(), but safe
# hand the REAL stdout to your MCP transport for JSON-RPC:
run_stdio_server(stdout=real_stdout)
Now nothing — your code or your dependencies — can poison the protocol with a stray text write.
Install
pip install quietmcp
Zero dependencies, pure standard library.
The three pieces
1. safe_print — print() that goes to stderr
from quietmcp import safe_print
safe_print("about to call tool", tool_name) # identical to print(), but on stderr
2. get_logger — a logger that can't reach stdout
log = quietmcp.get_logger("my-server", level=logging.DEBUG,
file="server.log", json=False, trace="trace.jsonl")
It logs to stderr (or a file), and — critically — sets propagate=False so your
records never bubble up to a root logger whose handler might be pointed at stdout.
It's idempotent, so calling it again with the same name won't stack duplicate
handlers. Pass stream=sys.stdout and it refuses with a clear error: that's the bug
it exists to prevent.
3. install() — guard stdout against everything
The piece that saves you from libraries you don't control. install() replaces
sys.stdout with a guard and returns the real stream:
real_stdout = quietmcp.install(mode="redirect") # default: stray writes -> stderr
| mode | a stray print() to stdout… |
|---|---|
"redirect" (default) |
is written to stderr instead (you still see it) |
"drop" |
is silently discarded |
"strict" |
raises StdoutPollutionError — great for catching the offender in tests/CI |
The guard only intercepts text writes. The binary layer (sys.stdout.buffer) —
which the Python MCP SDK uses to emit JSON-RPC — passes straight through to the real
stdout. So protocol output works untouched while print()/logging is contained.
That's exactly the split you want, and it's why install() + the MCP SDK just work
together.
Use it as a context manager when you'd rather scope it (e.g. in tests):
from quietmcp import protect_stdout, StdoutPollutionError
with protect_stdout("strict") as real_stdout:
run_one_request() # any stray stdout write in here raises
Note: this guards Python-level writes (
logging,sys.stdout.write). A subprocess or a C extension writing to fd 1 directly is out of scope — those need OS-level redirection.
Capture and view a trace
Pass trace="…" to get_logger (or attach a TraceHandler) to tee structured
records to a JSONL file, then read it back later — your own logs, kept, instead of
scrolling off stderr:
log = quietmcp.get_logger("my-server", trace="trace.jsonl")
log.info("tool call", extra={"tool": "search", "ms": 12})
python -m quietmcp view trace.jsonl # readable table in the terminal
python -m quietmcp view trace.jsonl --level WARNING # filter to WARNING and up
python -m quietmcp view trace.jsonl --html trace.html # a self-contained browsable page
Programmatically: read_trace, render_trace, and render_trace_html.
What it does not do
- It doesn't speak MCP. It has no dependency on any MCP SDK and adds no protocol features — it just keeps your stdout clean so the SDK you already use keeps working.
- It doesn't redirect OS file descriptors. Subprocesses/C libraries writing to
fd 1 directly aren't intercepted (that needs
os.dup2-level redirection). - It's not an observability platform. The trace viewer is a local convenience, not a hosted dashboard.
Part of an agent-reliability suite
quietmcp is the first of a small set of framework-agnostic tools for shipping agents and agent infrastructure you can trust. More are on the way.
Contributing
See CONTRIBUTING.md in the source distribution. The bar: zero dependencies, never write to
stdout, and ship tests + docs with every change. MIT licensed.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file quietmcp-0.1.0.tar.gz.
File metadata
- Download URL: quietmcp-0.1.0.tar.gz
- Upload date:
- Size: 32.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3b9843f22c53df8f0b9ca096417c0499e0f42a20ec27f5f91c34336f8e87a75e
|
|
| MD5 |
c6bd06161157ca3cfa9b3489b7c50c2a
|
|
| BLAKE2b-256 |
d9f67a99ba5f310bd71039125513729e25d0e5557d50ee07cf5e69aa710534aa
|
File details
Details for the file quietmcp-0.1.0-py3-none-any.whl.
File metadata
- Download URL: quietmcp-0.1.0-py3-none-any.whl
- Upload date:
- Size: 14.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d4c3359aee046758bc6f1e6f2d9a0f18e31c50ca0da07996057a286d317cf155
|
|
| MD5 |
db85895abde29e01556cc77d5566702b
|
|
| BLAKE2b-256 |
2ca6062311441476b22d03051dd8a2bce1784c196837db69289f7b3acef03a37
|