Metadata-Version: 2.4
Name: quietmcp
Version: 0.1.0
Summary: 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-URL: Homepage, https://github.com/thejenilsoni/quietmcp
Project-URL: Issues, https://github.com/thejenilsoni/quietmcp/issues
Author: Jenil Soni
License: MIT
License-File: LICENSE
Keywords: agents,debugging,json-rpc,llm,logging,mcp,model-context-protocol,stdio
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Debuggers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Logging
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# quietmcp

> Keep your MCP server quiet on stdout — `print()` and log freely without
> corrupting the JSON-RPC stream.

![Python](https://img.shields.io/badge/python-3.9%2B-blue)
![License](https://img.shields.io/badge/license-MIT-green)

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:

```python
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

```bash
pip install quietmcp
```

Zero dependencies, pure standard library.

## The three pieces

### 1. `safe_print` — `print()` that goes to stderr

```python
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

```python
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:

```python
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):

```python
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 (`print`, `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:

```python
log = quietmcp.get_logger("my-server", trace="trace.jsonl")
log.info("tool call", extra={"tool": "search", "ms": 12})
```

```bash
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.
