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.
Metadata
Release files for quietmcp 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 | |
|---|---|---|---|
| quietmcp-0.1.0.tar.gz | 32.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| quietmcp-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 46.1 kB
Release files / quietmcp-0.1.0.tar.gz
| Download URL | quietmcp-0.1.0.tar.gz |
|---|---|
| Size | 32.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3b9843f22c53df8f0b9ca096417c0499e0f42a20ec27f5f91c34336f8e87a75e
|
|
BLAKE2b-256 checksum How to use checksums |
d9f67a99ba5f310bd71039125513729e25d0e5557d50ee07cf5e69aa710534aa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.13
|
Release files / quietmcp-0.1.0-py3-none-any.whl
| Download URL | quietmcp-0.1.0-py3-none-any.whl |
|---|---|
| Size | 14.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d4c3359aee046758bc6f1e6f2d9a0f18e31c50ca0da07996057a286d317cf155
|
|
BLAKE2b-256 checksum How to use checksums |
2ca6062311441476b22d03051dd8a2bce1784c196837db69289f7b3acef03a37
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.13
|