Skip to content

MCP stdio server: osw log records bypass the stdout guard #170

Description

@LukasGold

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 in src/osw/__init__.py. Its
    default stream is sys.stdout and its default level is INFO.
  • 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:

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions