Repository navigation
MCP stdio server: osw log records bypass the stdout guard #170
Description
Once #160 and
#133 are both merged, osw log
records will be written to the JSON-RPC channel of the MCP stdio server.
Cause
- PR feat(logging): replace print statements with the logging module #160 calls
enable_logging()at import time insrc/osw/__init__.py. Its
default stream issys.stdoutand its default level isINFO. - The MCP server serves JSON-RPC over stdout
(src/osw/mcp/server.py:170,mcp.run(transport="stdio")). - The existing protection does not cover logging.
Context.guard()wraps each
osw call inredirect_stdout(sys.stderr)whenpolicy.capture_stdoutis set
(src/osw/service/context.py:113-125, enabled atsrc/osw/mcp/server.py:134).
Alogging.StreamHandlerstores the stream object at construction time, so
rebindingsys.stdoutafterwards has no effect on it.
Reproduction of the last point, independent of osw:
import io
import logging
import sys
from contextlib import redirect_stdout
handler = logging.StreamHandler(sys.stdout) # built before the redirect
logger = logging.getLogger("probe")
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.propagate = False
buffer = io.StringIO()
with redirect_stdout(buffer):
logger.info("does redirect_stdout capture this?")
print("captured by redirect:", repr(buffer.getvalue()))The record is written to the real stdout and buffer stays empty.
Effect
- The INFO banner that feat(logging): replace print statements with the logging module #160 emits when
OSW_LOG_LEVELis unset is written to
stdout at import, before the protocol starts. - Any osw log record emitted during a tool call is written to stdout while the
JSON-RPC stream is live. This is the case the guard was written to prevent.
Proposed fix
Redirect the osw logger to stderr in main() in src/osw/mcp/server.py, before
mcp.run():
osw.enable_logging(stream=sys.stderr)osw.disable_logging() is the alternative, if the server should stay silent
unless the client configures logging itself.
Neither PR can carry this alone: enable_logging only exists in #160, and
src/osw/mcp/server.py only exists in #133. Whichever merges second should add
the call, plus a test asserting that a tool call writes nothing to stdout.
Related
The osw[wikitext] hint in src/osw/wiki_tools.py is the current instance of
the same pattern. It reaches stdout through print() today, but at import time
only, so it does not corrupt the stream. #160 converts it to a logger call,
which does not change its destination as long as the default handler writes to
stdout.
Activity
fix(logging): default the osw handler to stderr, not stdout
LukasGold commentedon Sep 7, 2026
Resolved on the library side in #160 (commit 329bff4):
enable_logging()insrc/osw/__init__.pynow defaults tosys.stderr, withtest_the_default_handler_writes_to_stderrintests/test_logging_setup.pyguarding the default.This removes the conflict for every stdio consumer, so #133 needs no change and the "whichever merges second carries the fix" note above no longer applies.
Ready to close once #160 is merged.
LukasGold commentedon Sep 21, 2026
Closing as out of date. The diagnosis in this issue no longer matches the code.
The issue states that
enable_logging()defaults its handler stream tosys.stdout. Onfeat/mcp-serverit defaults tosys.stderr(src/osw/__init__.py:149):Its docstring names the MCP stdio server as the reason for that default, so the fix this issue proposed is in place, applied in the library rather than in
main()ofsrc/osw/mcp/server.py.There is also a second protection that was not known when this was written. The MCP SDK's
stdio_serverclaims file descriptor 1 and diverts it, then re-wraps the original as UTF-8 text. A strayprint()therefore cannot tear a JSON-RPC frame.The one item from this issue that is still outstanding is the test it asked for. No test asserts that a tool call writes nothing to stdout. That is tracked in #192.