quietmcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ name: CI (quietmcp)
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths: ["projects/quietmcp/**"]
7
+ pull_request:
8
+ paths: ["projects/quietmcp/**"]
9
+
10
+ defaults:
11
+ run:
12
+ working-directory: projects/quietmcp
13
+
14
+ jobs:
15
+ test:
16
+ runs-on: ${{ matrix.os }}
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ os: [ubuntu-latest]
21
+ python-version: ["3.9", "3.10", "3.11", "3.12"]
22
+ # quietmcp claims no OS restriction; verify the example/launch scripts run
23
+ # under Windows' default cp1252 console (where naive Unicode prints crash).
24
+ include:
25
+ - os: windows-latest
26
+ python-version: "3.12"
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: actions/setup-python@v5
30
+ with:
31
+ python-version: ${{ matrix.python-version }}
32
+ - name: Install
33
+ run: pip install -e ".[dev]"
34
+ - name: Lint
35
+ run: ruff check .
36
+ - name: Run tests
37
+ run: pytest
38
+ - name: Run the example
39
+ run: python examples/quickstart.py
40
+ - name: Smoke-test launch scripts
41
+ env:
42
+ NO_PAUSE: "1"
43
+ run: |
44
+ python launch/demo-walkthrough.py
45
+ pip install rich # only needed to render the demo image, not to use quietmcp
46
+ python launch/make_demo_svg.py
@@ -0,0 +1,21 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+
11
+ # Test / tooling
12
+ .pytest_cache/
13
+ .mypy_cache/
14
+ .ruff_cache/
15
+ .coverage
16
+ htmlcov/
17
+
18
+ # OS / editor
19
+ .DS_Store
20
+ .idea/
21
+ .vscode/
@@ -0,0 +1,43 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/), and this project adheres to
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [0.1.0] - 2026-06-13
8
+
9
+ ### Added
10
+ - `safe_print(...)` — a `print()` drop-in that writes to **stderr**, so it can't
11
+ corrupt an MCP stdio server's JSON-RPC stream.
12
+ - `get_logger(name, *, level, stream, file, json, trace)` — a logger that writes to
13
+ stderr/a file (never stdout), sets `propagate=False` (so records can't bubble to a
14
+ root handler aimed at stdout), is idempotent, and **refuses** `stream=sys.stdout`.
15
+ - `install()` / `uninstall()` / `protect_stdout()` — guard `sys.stdout` so stray
16
+ **text** writes (yours or a dependency's) are redirected to stderr (`"redirect"`),
17
+ dropped (`"drop"`), or raise `StdoutPollutionError` (`"strict"`). Returns the real
18
+ stdout for protocol writes; leaves `sys.stdout.buffer` passing through to the real
19
+ stream, so the MCP SDK's JSON-RPC bytes are untouched.
20
+ - Trace capture + local viewer: `TraceHandler` tees structured records to a JSONL
21
+ file; `read_trace` / `render_trace` (text) / `render_trace_html` (a self-contained
22
+ page) read them back. CLI: `python -m quietmcp view trace.jsonl [--level L] [--html PATH]`.
23
+ - Zero-dependency, fully typed, with a runnable `examples/quickstart.py` and a full
24
+ offline test suite.
25
+
26
+ ### Changed
27
+ - `python -m quietmcp view` fails cleanly with a short stderr message and exit code
28
+ 2 when the trace file is missing or isn't valid JSONL, instead of dumping a raw
29
+ traceback. (Errors go to stderr — never stdout.)
30
+
31
+ ### Fixed
32
+ - `read_trace` reads with `utf-8-sig`, so a trace written as UTF-8-with-BOM (common
33
+ from Windows editors/tooling) loads instead of failing with an "Unexpected UTF-8
34
+ BOM" error. BOM-less UTF-8 is unaffected.
35
+ - The `examples/quickstart.py` and `launch/demo-walkthrough.py` demos reconfigure
36
+ their own output to UTF-8, so they no longer crash on Windows' default cp1252
37
+ console when stdout is piped (the protocol stream the library guards is untouched).
38
+
39
+ ### Notes
40
+ - Honest scope: quietmcp has no dependency on any MCP SDK (it just keeps stdout
41
+ clean for the one you use), and it guards Python-level writes — not raw OS file
42
+ descriptor 1 (a subprocess or C extension writing to fd 1 needs OS-level
43
+ redirection).
@@ -0,0 +1,39 @@
1
+ # Contributing to quietmcp
2
+
3
+ Thanks for your interest! quietmcp aims to stay **tiny, dependency-free, and
4
+ laser-focused** on one thing: never letting non-protocol output reach an MCP
5
+ server's stdout. Contributions that keep it that way are welcome.
6
+
7
+ ## Dev setup
8
+
9
+ ```bash
10
+ python -m venv .venv && source .venv/bin/activate
11
+ pip install -e ".[dev]"
12
+ pytest
13
+ ruff check .
14
+ ```
15
+
16
+ The whole test suite runs **offline** — no network, no MCP server, no subprocess.
17
+ Stream behaviour is tested with in-memory `StringIO`s and pytest's `capsys`.
18
+
19
+ ## Guidelines
20
+
21
+ - **Zero dependencies.** quietmcp must not depend on any MCP SDK or third-party
22
+ package; it should compose with whatever the user already has.
23
+ - **Never write to stdout.** Every code path either targets stderr/a file, or
24
+ passes protocol bytes through the real stdout deliberately. New features must
25
+ preserve that invariant (and ideally test it with `capsys`).
26
+ - **Fail loud on misuse.** Pointing a logger at stdout, or a stray write in
27
+ `strict` mode, should raise a clear error — that's the bug class we exist to catch.
28
+ - **Every change ships with tests and docs.**
29
+
30
+ ## Ideas / good first issues
31
+
32
+ - An OS-level fd-redirection helper (`os.dup2`) for stdout written by subprocesses
33
+ or C extensions, as an opt-in beyond the Python-level guard.
34
+ - Async-friendly helpers / an example wiring quietmcp into the official `mcp` SDK's
35
+ `stdio_server`.
36
+ - A `--follow` (tail) mode for `python -m quietmcp view`.
37
+ - Bridging to MCP's own `notifications/message` logging.
38
+
39
+ Open an issue to discuss before large changes. Be kind; assume good intent.
quietmcp-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jenil Soni
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.4
2
+ Name: quietmcp
3
+ Version: 0.1.0
4
+ 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.
5
+ Project-URL: Homepage, https://github.com/thejenilsoni/quietmcp
6
+ Project-URL: Issues, https://github.com/thejenilsoni/quietmcp/issues
7
+ Author: Jenil Soni
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,debugging,json-rpc,llm,logging,mcp,model-context-protocol,stdio
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Debuggers
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: System :: Logging
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.9
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7.0; extra == 'dev'
22
+ Requires-Dist: ruff>=0.6; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # quietmcp
26
+
27
+ > Keep your MCP server quiet on stdout — `print()` and log freely without
28
+ > corrupting the JSON-RPC stream.
29
+
30
+ ![Python](https://img.shields.io/badge/python-3.9%2B-blue)
31
+ ![License](https://img.shields.io/badge/license-MIT-green)
32
+
33
+ On an MCP **stdio** server, `stdout` *is* the protocol — every byte is part of the
34
+ JSON-RPC stream the client parses. So the most natural thing in the world, a
35
+ `print("got here")`, silently corrupts that stream and the client breaks with a
36
+ baffling parse error. Worse, a third-party library you don't control can `print` a
37
+ progress bar and take your server down with it.
38
+
39
+ The fix is mundane — send everything that isn't protocol to **stderr** — but easy
40
+ to get wrong and easy for a dependency to undo. **quietmcp** makes it effortless:
41
+
42
+ ```python
43
+ import quietmcp
44
+
45
+ real_stdout = quietmcp.install() # stray writes to stdout now go to stderr
46
+ log = quietmcp.get_logger("my-server") # logs to stderr, never stdout
47
+ log.info("server starting")
48
+ quietmcp.safe_print("debug: tool called") # print(), but safe
49
+
50
+ # hand the REAL stdout to your MCP transport for JSON-RPC:
51
+ run_stdio_server(stdout=real_stdout)
52
+ ```
53
+
54
+ Now nothing — your code or your dependencies — can poison the protocol with a stray
55
+ text write.
56
+
57
+ ---
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ pip install quietmcp
63
+ ```
64
+
65
+ Zero dependencies, pure standard library.
66
+
67
+ ## The three pieces
68
+
69
+ ### 1. `safe_print` — `print()` that goes to stderr
70
+
71
+ ```python
72
+ from quietmcp import safe_print
73
+ safe_print("about to call tool", tool_name) # identical to print(), but on stderr
74
+ ```
75
+
76
+ ### 2. `get_logger` — a logger that can't reach stdout
77
+
78
+ ```python
79
+ log = quietmcp.get_logger("my-server", level=logging.DEBUG,
80
+ file="server.log", json=False, trace="trace.jsonl")
81
+ ```
82
+
83
+ It logs to **stderr** (or a file), and — critically — sets `propagate=False` so your
84
+ records never bubble up to a root logger whose handler might be pointed at stdout.
85
+ It's idempotent, so calling it again with the same name won't stack duplicate
86
+ handlers. Pass `stream=sys.stdout` and it refuses with a clear error: that's the bug
87
+ it exists to prevent.
88
+
89
+ ### 3. `install()` — guard stdout against *everything*
90
+
91
+ The piece that saves you from libraries you don't control. `install()` replaces
92
+ `sys.stdout` with a guard and returns the real stream:
93
+
94
+ ```python
95
+ real_stdout = quietmcp.install(mode="redirect") # default: stray writes -> stderr
96
+ ```
97
+
98
+ | mode | a stray `print()` to stdout… |
99
+ |------|------------------------------|
100
+ | `"redirect"` (default) | is written to stderr instead (you still see it) |
101
+ | `"drop"` | is silently discarded |
102
+ | `"strict"` | raises `StdoutPollutionError` — great for catching the offender in tests/CI |
103
+
104
+ The guard only intercepts **text** writes. The binary layer (`sys.stdout.buffer`) —
105
+ which the Python MCP SDK uses to emit JSON-RPC — passes straight through to the real
106
+ stdout. So protocol output works untouched while `print()`/logging is contained.
107
+ That's exactly the split you want, and it's why `install()` + the MCP SDK just work
108
+ together.
109
+
110
+ Use it as a context manager when you'd rather scope it (e.g. in tests):
111
+
112
+ ```python
113
+ from quietmcp import protect_stdout, StdoutPollutionError
114
+
115
+ with protect_stdout("strict") as real_stdout:
116
+ run_one_request() # any stray stdout write in here raises
117
+ ```
118
+
119
+ > Note: this guards Python-level writes (`print`, `logging`, `sys.stdout.write`). A
120
+ > subprocess or a C extension writing to fd 1 directly is out of scope — those need
121
+ > OS-level redirection.
122
+
123
+ ## Capture and view a trace
124
+
125
+ Pass `trace="…"` to `get_logger` (or attach a `TraceHandler`) to tee structured
126
+ records to a JSONL file, then read it back later — your own logs, kept, instead of
127
+ scrolling off stderr:
128
+
129
+ ```python
130
+ log = quietmcp.get_logger("my-server", trace="trace.jsonl")
131
+ log.info("tool call", extra={"tool": "search", "ms": 12})
132
+ ```
133
+
134
+ ```bash
135
+ python -m quietmcp view trace.jsonl # readable table in the terminal
136
+ python -m quietmcp view trace.jsonl --level WARNING # filter to WARNING and up
137
+ python -m quietmcp view trace.jsonl --html trace.html # a self-contained browsable page
138
+ ```
139
+
140
+ Programmatically: `read_trace`, `render_trace`, and `render_trace_html`.
141
+
142
+ ## What it does **not** do
143
+
144
+ - **It doesn't speak MCP.** It has no dependency on any MCP SDK and adds no protocol
145
+ features — it just keeps your stdout clean so the SDK you already use keeps working.
146
+ - **It doesn't redirect OS file descriptors.** Subprocesses/C libraries writing to
147
+ fd 1 directly aren't intercepted (that needs `os.dup2`-level redirection).
148
+ - **It's not an observability platform.** The trace viewer is a local convenience,
149
+ not a hosted dashboard.
150
+
151
+ ## Part of an agent-reliability suite
152
+
153
+ quietmcp is the first of a small set of framework-agnostic tools for shipping
154
+ agents and agent infrastructure you can trust. More are on the way.
155
+
156
+ ## Contributing
157
+
158
+ See `CONTRIBUTING.md` in the source distribution. The bar: zero dependencies, never write to
159
+ stdout, and ship tests + docs with every change. MIT licensed.
@@ -0,0 +1,135 @@
1
+ # quietmcp
2
+
3
+ > Keep your MCP server quiet on stdout — `print()` and log freely without
4
+ > corrupting the JSON-RPC stream.
5
+
6
+ ![Python](https://img.shields.io/badge/python-3.9%2B-blue)
7
+ ![License](https://img.shields.io/badge/license-MIT-green)
8
+
9
+ On an MCP **stdio** server, `stdout` *is* the protocol — every byte is part of the
10
+ JSON-RPC stream the client parses. So the most natural thing in the world, a
11
+ `print("got here")`, silently corrupts that stream and the client breaks with a
12
+ baffling parse error. Worse, a third-party library you don't control can `print` a
13
+ progress bar and take your server down with it.
14
+
15
+ The fix is mundane — send everything that isn't protocol to **stderr** — but easy
16
+ to get wrong and easy for a dependency to undo. **quietmcp** makes it effortless:
17
+
18
+ ```python
19
+ import quietmcp
20
+
21
+ real_stdout = quietmcp.install() # stray writes to stdout now go to stderr
22
+ log = quietmcp.get_logger("my-server") # logs to stderr, never stdout
23
+ log.info("server starting")
24
+ quietmcp.safe_print("debug: tool called") # print(), but safe
25
+
26
+ # hand the REAL stdout to your MCP transport for JSON-RPC:
27
+ run_stdio_server(stdout=real_stdout)
28
+ ```
29
+
30
+ Now nothing — your code or your dependencies — can poison the protocol with a stray
31
+ text write.
32
+
33
+ ---
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install quietmcp
39
+ ```
40
+
41
+ Zero dependencies, pure standard library.
42
+
43
+ ## The three pieces
44
+
45
+ ### 1. `safe_print` — `print()` that goes to stderr
46
+
47
+ ```python
48
+ from quietmcp import safe_print
49
+ safe_print("about to call tool", tool_name) # identical to print(), but on stderr
50
+ ```
51
+
52
+ ### 2. `get_logger` — a logger that can't reach stdout
53
+
54
+ ```python
55
+ log = quietmcp.get_logger("my-server", level=logging.DEBUG,
56
+ file="server.log", json=False, trace="trace.jsonl")
57
+ ```
58
+
59
+ It logs to **stderr** (or a file), and — critically — sets `propagate=False` so your
60
+ records never bubble up to a root logger whose handler might be pointed at stdout.
61
+ It's idempotent, so calling it again with the same name won't stack duplicate
62
+ handlers. Pass `stream=sys.stdout` and it refuses with a clear error: that's the bug
63
+ it exists to prevent.
64
+
65
+ ### 3. `install()` — guard stdout against *everything*
66
+
67
+ The piece that saves you from libraries you don't control. `install()` replaces
68
+ `sys.stdout` with a guard and returns the real stream:
69
+
70
+ ```python
71
+ real_stdout = quietmcp.install(mode="redirect") # default: stray writes -> stderr
72
+ ```
73
+
74
+ | mode | a stray `print()` to stdout… |
75
+ |------|------------------------------|
76
+ | `"redirect"` (default) | is written to stderr instead (you still see it) |
77
+ | `"drop"` | is silently discarded |
78
+ | `"strict"` | raises `StdoutPollutionError` — great for catching the offender in tests/CI |
79
+
80
+ The guard only intercepts **text** writes. The binary layer (`sys.stdout.buffer`) —
81
+ which the Python MCP SDK uses to emit JSON-RPC — passes straight through to the real
82
+ stdout. So protocol output works untouched while `print()`/logging is contained.
83
+ That's exactly the split you want, and it's why `install()` + the MCP SDK just work
84
+ together.
85
+
86
+ Use it as a context manager when you'd rather scope it (e.g. in tests):
87
+
88
+ ```python
89
+ from quietmcp import protect_stdout, StdoutPollutionError
90
+
91
+ with protect_stdout("strict") as real_stdout:
92
+ run_one_request() # any stray stdout write in here raises
93
+ ```
94
+
95
+ > Note: this guards Python-level writes (`print`, `logging`, `sys.stdout.write`). A
96
+ > subprocess or a C extension writing to fd 1 directly is out of scope — those need
97
+ > OS-level redirection.
98
+
99
+ ## Capture and view a trace
100
+
101
+ Pass `trace="…"` to `get_logger` (or attach a `TraceHandler`) to tee structured
102
+ records to a JSONL file, then read it back later — your own logs, kept, instead of
103
+ scrolling off stderr:
104
+
105
+ ```python
106
+ log = quietmcp.get_logger("my-server", trace="trace.jsonl")
107
+ log.info("tool call", extra={"tool": "search", "ms": 12})
108
+ ```
109
+
110
+ ```bash
111
+ python -m quietmcp view trace.jsonl # readable table in the terminal
112
+ python -m quietmcp view trace.jsonl --level WARNING # filter to WARNING and up
113
+ python -m quietmcp view trace.jsonl --html trace.html # a self-contained browsable page
114
+ ```
115
+
116
+ Programmatically: `read_trace`, `render_trace`, and `render_trace_html`.
117
+
118
+ ## What it does **not** do
119
+
120
+ - **It doesn't speak MCP.** It has no dependency on any MCP SDK and adds no protocol
121
+ features — it just keeps your stdout clean so the SDK you already use keeps working.
122
+ - **It doesn't redirect OS file descriptors.** Subprocesses/C libraries writing to
123
+ fd 1 directly aren't intercepted (that needs `os.dup2`-level redirection).
124
+ - **It's not an observability platform.** The trace viewer is a local convenience,
125
+ not a hosted dashboard.
126
+
127
+ ## Part of an agent-reliability suite
128
+
129
+ quietmcp is the first of a small set of framework-agnostic tools for shipping
130
+ agents and agent infrastructure you can trust. More are on the way.
131
+
132
+ ## Contributing
133
+
134
+ See `CONTRIBUTING.md` in the source distribution. The bar: zero dependencies, never write to
135
+ stdout, and ship tests + docs with every change. MIT licensed.
@@ -0,0 +1,92 @@
1
+ # quietmcp — Release Readiness (v0.1.0)
2
+
3
+ **Date:** 2026-06-13 · **Verdict: GO (publish-ready)** — pending only Jenil's
4
+ go-ahead + PyPI credentials.
5
+
6
+ This is a fresh re-assessment run on **Windows 11 / Python 3.13** (the prior pass,
7
+ 2026-06-03, ran in the Linux web container on Python 3.11). The cross-platform run
8
+ surfaced three real Windows footguns, all now fixed in place. quietmcp itself —
9
+ the library — was already solid; the issues were in the demo/launch scripts and one
10
+ file-read encoding.
11
+
12
+ quietmcp keeps an MCP **stdio** server's stdout clean: a `print()`-safe helper, a
13
+ stdout-refusing logger, a guard that contains stray text writes (yours or a
14
+ dependency's), and a tiny JSONL trace viewer. Zero-dependency core, fully typed.
15
+
16
+ ---
17
+
18
+ ## Verification results (this run, Windows / Py3.13)
19
+
20
+ | Check | Result |
21
+ |-------|--------|
22
+ | `pip install -e ".[dev]"` | ✅ clean (Python 3.13.13, Windows) |
23
+ | `ruff check .` | ✅ All checks passed |
24
+ | `python -m pytest` | ✅ **36 passed** (was 35; +1 BOM regression test) |
25
+ | `python -m build` | ✅ builds `sdist` + `wheel` in isolated env |
26
+ | Zero-dep import | ✅ `import quietmcp` with no extras; `__version__ == "0.1.0"`, 15 public exports |
27
+ | `py.typed` ships in wheel | ✅ present at `quietmcp/py.typed` |
28
+ | Console-script entry point | ✅ `quietmcp = quietmcp.cli:main` in wheel `entry_points.txt`; `quietmcp --help` works |
29
+ | `examples/quickstart.py` | ✅ runs (incl. **piped stdout** on Windows — see fix #2) |
30
+ | `launch/demo-walkthrough.py` | ✅ runs (incl. piped stdout on Windows — see fix #2) |
31
+ | CLI `view` / `--level` / `--html` / `python -m quietmcp` | ✅ all paths work end-to-end |
32
+ | CLI error paths (missing file, bad JSONL) | ✅ one-line message **to stderr**, exit code 2 |
33
+ | Name availability | ✅ free as of the prior check (**re-verify at publish time** — see below) |
34
+
35
+ ---
36
+
37
+ ## Fixes applied this session (Windows hardening, in place)
38
+
39
+ 1. **`read_trace` rejected UTF-8-with-BOM trace files.** It opened with `utf-8`,
40
+ which raises "Unexpected UTF-8 BOM" on a BOM'd file (common from Windows
41
+ editors/tooling — e.g. PowerShell `Out-File -Encoding utf8`). quietmcp's *own*
42
+ `TraceHandler` writes BOM-less UTF-8, so the self round-trip was fine, but a
43
+ trace from any BOM-writing tool was unreadable. **Fix:** read with `utf-8-sig`
44
+ (transparently strips a BOM; identical for BOM-less files). Added a regression
45
+ test (`test_read_trace_tolerates_utf8_bom`). `src/quietmcp/trace.py`.
46
+
47
+ 2. **The demo scripts crashed on Windows' cp1252 console.** `examples/quickstart.py`
48
+ and `launch/demo-walkthrough.py` print a `✓` (U+2713); when stdout is a pipe on
49
+ Windows (CI logs, recordings, this harness), Python falls back to cp1252 and
50
+ `✓` raises `UnicodeEncodeError` — *after* printing "it worked", so a Windows dev's
51
+ first run ends in a traceback. **Fix:** each demo best-effort `reconfigure`s its
52
+ own output to UTF-8 at startup (the protocol stream the library guards is never
53
+ touched). Root-cause fix, so em-dashes/`✓` render everywhere.
54
+
55
+ 3. **CI never tested Windows.** The matrix was `ubuntu-latest` only, so #1 and #2
56
+ could never be caught despite `requires-python` declaring no OS restriction.
57
+ **Fix:** added a `windows-latest` lane (Py3.12) that runs the example + launch
58
+ scripts under the default console. `.github/workflows/ci.yml`.
59
+
60
+ All three are committed as code/test/CI/CHANGELOG changes (the "perfect commit"
61
+ discipline). Tests 35 → **36**, still ruff-clean, build still green.
62
+
63
+ ---
64
+
65
+ ## Punch list (non-blocking, post-launch polish)
66
+
67
+ - **P3 — `install()` mode is sticky.** A second `install("strict")` after
68
+ `install("redirect")` silently returns the first guard and ignores the new mode
69
+ (idempotent by design, for "install once at startup"). Consider a docstring note
70
+ or a `warnings.warn` when `mode` differs from the live guard.
71
+ - **P3 — `get_logger(file=..., stream=...)`** attaches both handlers; with only
72
+ `file=` it logs to the file alone (no stderr echo). Intended — worth one README
73
+ sentence so users aren't surprised `file=` silences stderr.
74
+ - **P3 — `render_trace` to a piped console** can still hit cp1252 if a *user's* log
75
+ message contains non-ASCII (general Python-on-Windows behaviour, not quietmcp's
76
+ output). Not worth a code change; could be a one-line FAQ note.
77
+
78
+ None of these block a v0.1.0 launch.
79
+
80
+ ## Pre-publish checklist (when Jenil gives the go-ahead)
81
+
82
+ 1. **Re-verify the name** on PyPI + `github.com/thejenilsoni/quietmcp` (was free at
83
+ last check; confirm again immediately before upload).
84
+ 2. Decide whether the *Unreleased* entries ship as **v0.1.0** (fold them in) or as
85
+ **v0.1.1** — they're currently staged under *Unreleased*.
86
+ 3. Extract `projects/quietmcp/` to its own repo; rewrite sibling links
87
+ (`../agentvcr/` etc. in README) to absolute `github.com/thejenilsoni/...` URLs.
88
+ 4. `python -m build` → `twine check dist/*` → `twine upload` (twine not installed
89
+ here; needs a PyPI token — **do not upload without Jenil's go-ahead**).
90
+ 5. Record the killer demo (noisy dep `print()`s mid-request, JSON-RPC stays clean)
91
+ per `launch/recording-guide.md`. Consider co-launching with `mcplens` as an
92
+ "MCP server toolkit."
@@ -0,0 +1,82 @@
1
+ """Show that debug output can't corrupt the MCP protocol stream — offline.
2
+
3
+ Run me: python examples/quickstart.py
4
+
5
+ We simulate one MCP request handler. A rude third-party library `print()`s to
6
+ stdout (which on a real server is the JSON-RPC channel), and we also log and
7
+ safe_print debug info. With quietmcp installed, only the actual protocol response
8
+ reaches stdout; everything else is safely redirected to stderr and captured to a
9
+ trace we then render.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import io
15
+ import json
16
+ import sys
17
+ import tempfile
18
+ from pathlib import Path
19
+
20
+ import quietmcp
21
+
22
+
23
+ def noisy_library_call() -> dict:
24
+ # A dependency that rudely writes to stdout — the classic MCP stream-corrupter.
25
+ print("LIB: download progress 50%")
26
+ print("LIB: download progress 100%")
27
+ return {"temperature_c": 21}
28
+
29
+
30
+ def handle_request(real_stdout, log) -> None:
31
+ log.info("handling tools/call", extra={"tool": "get_weather"})
32
+ quietmcp.safe_print("DEBUG: validating arguments") # muscle-memory print(), now safe
33
+ result = noisy_library_call()
34
+ log.info("tool succeeded", extra={"result": result})
35
+ # The ONLY thing that should ever hit stdout: the JSON-RPC response.
36
+ response = {"jsonrpc": "2.0", "id": 1, "result": result}
37
+ real_stdout.write(json.dumps(response) + "\n")
38
+ real_stdout.flush()
39
+
40
+
41
+ def main() -> None:
42
+ # Make the demo's own narration readable on any console (e.g. Windows cp1252),
43
+ # even when stdout is a pipe. Best-effort; this is the display channel, not the
44
+ # protocol stream the library protects.
45
+ for _stream in (sys.stdout, sys.stderr):
46
+ try:
47
+ _stream.reconfigure(encoding="utf-8") # type: ignore[union-attr]
48
+ except (AttributeError, ValueError):
49
+ pass
50
+
51
+ protocol = io.StringIO() # stands in for the real stdout (the MCP channel)
52
+ stderr_cap = io.StringIO() # stands in for stderr
53
+ trace_path = Path(tempfile.gettempdir()) / "quietmcp-demo.jsonl"
54
+ trace_path.unlink(missing_ok=True)
55
+
56
+ saved_out, saved_err = sys.stdout, sys.stderr
57
+ sys.stdout, sys.stderr = protocol, stderr_cap
58
+ try:
59
+ real_stdout = quietmcp.install(to=stderr_cap) # guard stdout; stray writes -> stderr
60
+ log = quietmcp.get_logger("demo", stream=stderr_cap, trace=str(trace_path))
61
+ handle_request(real_stdout, log)
62
+ finally:
63
+ quietmcp.uninstall()
64
+ sys.stdout, sys.stderr = saved_out, saved_err
65
+
66
+ print("=== stdout — the JSON-RPC channel (must be protocol ONLY) ===")
67
+ print(protocol.getvalue().strip())
68
+ print("\n=== stderr — debug + the noisy library, safely redirected ===")
69
+ print(stderr_cap.getvalue().strip())
70
+
71
+ # Prove the protocol stream was never polluted.
72
+ assert "LIB:" not in protocol.getvalue()
73
+ assert "DEBUG:" not in protocol.getvalue()
74
+ assert json.loads(protocol.getvalue())["result"] == {"temperature_c": 21}
75
+
76
+ print("\n=== captured trace (python -m quietmcp view ...) ===")
77
+ print(quietmcp.render_trace(quietmcp.read_trace(trace_path)))
78
+ print("\nstdout stayed clean — the protocol survived a noisy dependency. ✓")
79
+
80
+
81
+ if __name__ == "__main__":
82
+ main()