Skip to content

fix: route AsyncLogger output to stderr by default (fixes #1968)#1969

Open
cgseyhan wants to merge 1 commit into
unclecode:developfrom
cgseyhan:fix/async-logger-stderr-mcp-1968
Open

fix: route AsyncLogger output to stderr by default (fixes #1968)#1969
cgseyhan wants to merge 1 commit into
unclecode:developfrom
cgseyhan:fix/async-logger-stderr-mcp-1968

Conversation

@cgseyhan
Copy link
Copy Markdown

@cgseyhan cgseyhan commented May 14, 2026

Problem

AsyncLogger writes its Rich progress output to stdout via the default Console() constructor. When crawl4ai is used as an MCP server over stdio (e.g. with mcp-crawl4ai, Claude Desktop, or mcp-inspector), the log lines are injected into the JSON-RPC stream. Clients fail to parse the messages and report:

Failed to parse JSONRPC message from server

This was reported in #1968.

async_logger.py – before

self.console = Console() # writes to stdout by default

ich.Console() without arguments writes to stdout. Library log output should go to stderr — this is the POSIX convention and what Python's own logging.StreamHandler does.

async_logger.py – after

self.console = console if console is not None else Console(stderr=True)

Two changes:

  1. Default to stderr : Console(stderr=True) writes to sys.stderr, keeping stdout pristine for protocols that use it as a transport.
  2. Injectable console parameter : callers that want stdout back (or need a custom target) can pass their own Console instance without monkey-patching.

Testing

Added ests/test_async_logger_stderr.py with 7 tests:

Test What it checks
est_default_console_writes_to_stderr_not_stdout capsys.out is empty after logging
est_default_console_is_stderr logger.console.file is sys.stderr
est_custom_console_is_respected Injected StringIO console receives output
est_stdout_console_can_be_injected Legacy stdout behaviour can be restored
est_no_output_when_verbose_false erbose=False still suppresses everything
est_file_logging_still_works File logging path unaffected
est_stdout_clean_across_all_log_levels End-to-end MCP scenario: stdout has only JSON

All 7 pass ✅

Backwards Compatibility

  • Users who read console.file directly and expect stdout will see stderr instead. This is intentional and correct.
  • Users who need the old behaviour can pass console=Console() (or Console(file=sys.stdout)) to the constructor.
  • No other public API changes.

MCP stdio transport uses stdout for JSON-RPC messages. AsyncLogger
was writing Rich progress output to stdout (the default Console()
target), which caused clients to receive garbled JSON and log lines
interleaved in the same stream.

Changes:
- Pass stderr=True to Console() so all log output goes to stderr,
  which is the correct channel for library diagnostics and aligns
  with the behaviour of Python's own logging.StreamHandler.
- Add an injectable console parameter so downstream wrappers
  (e.g. mcp-crawl4ai, FastMCP integrations) can override the target
  stream without monkey-patching.
- Add import sys (used in docstring example).
- Add tests/test_async_logger_stderr.py with 7 tests covering the
  default-to-stderr behaviour, custom console injection, verbose=False
  suppression, file logging, and an end-to-end MCP scenario.

Fixes unclecode#1968
@cgseyhan cgseyhan changed the title fix(logger): route AsyncLogger output to stderr by default (fixes #1968) fix: route AsyncLogger output to stderr by default (fixes #1968) May 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants