You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
MCP stdio server: osw log records bypass the stdout guard #170
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 in redirect_stdout(sys.stderr) when policy.capture_stdout is set
(src/osw/service/context.py:113-125, enabled at src/osw/mcp/server.py:134).
A logging.StreamHandler stores the stream object at construction time, so
rebinding sys.stdout afterwards has no effect on it.
Reproduction of the last point, independent of osw:
importioimportloggingimportsysfromcontextlibimportredirect_stdouthandler=logging.StreamHandler(sys.stdout) # built before the redirectlogger=logging.getLogger("probe")
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.propagate=Falsebuffer=io.StringIO()
withredirect_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.
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.
Resolved on the library side in #160 (commit 329bff4): enable_logging() in src/osw/__init__.py now defaults to sys.stderr, with test_the_default_handler_writes_to_stderr in tests/test_logging_setup.py guarding 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.
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 to sys.stdout. On feat/mcp-server it defaults to sys.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() of src/osw/mcp/server.py.
There is also a second protection that was not known when this was written. The MCP SDK's stdio_server claims file descriptor 1 and diverts it, then re-wraps the original as UTF-8 text. A stray print() 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.
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.