Purpose
-------
Keep the stdio transport's protocol channel clean while the ``oct-mcp``
server starts. Under the ``stdio`` transport, standard output (file
descriptor 1) carries JSON-RPC messages, and the MCP specification says
a stdio server must not write anything else there. While
:func:`mcp.server.stdio.stdio_server` serves, the MCP Python SDK already
points fd 1 at standard error and writes the protocol through a private
duplicate. Before that, from process start until
:meth:`mcp.server.MCPServer.run` enters the transport, nothing protected
fd 1, so a ``_dbg`` record from startup (a lifecycle line with the MCP
domain at level 2 or higher, any L1 startup error, the production-mode
downgrade warning, or the FS-25 fallback warning) was written onto the
wire ahead of the ``initialize`` response. This module covers that
startup window with the same mechanism the SDK uses while serving.
Responsibilities
----------------
- :func:`divert_stdout`: keep a private duplicate of fd 1 (the wire),
always above the standard descriptors, and point fd 1 at standard
error, or at the null device when standard error is not open. Every
write to fd 1 then misses the wire, whoever makes it: ``_dbg`` in any
output mode, ``print()``, a third-party library or a child process
that inherits standard output.
- :func:`restore_stdout`: flush Python's ``sys.stdout`` while fd 1 is
still diverted, discarding into the null device whatever standard
error rejects, then point fd 1 back at the wire and close the private
duplicate. :func:`oct.mcp.server.start_server` calls it immediately
before ``MCPServer.run(transport="stdio")``, whose ``stdio_server()``
claims fd 1 again at once.
- :func:`stdout_is_diverted`: report whether a diversion is still
recorded, so :func:`oct.mcp.server.start_server` can refuse to serve
when the hand-back failed.
Diagnostics
-----------
Domain: MCP
Levels:
L1 - errors: fd 1 could not be pointed back at the wire
L2 - lifecycle: diversion started, fd 1 handed back to the transport;
EXTERNAL_ERROR when standard error rejected buffered output
L3 - (none): the lifecycle records are the whole trace
L4 - (none): no deep trace is emitted
Every record is written while fd 1 is diverted, so none of them can
reach the wire: the hand-back record is emitted before fd 1 moves.
Contracts
---------
- None of the three public functions raises. A failed diversion leaves
fd 1 carrying the wire. A failed hand-back leaves the diversion
recorded, with fd 1 still off the wire (on standard error or the null
device, or closed when a failing Windows ``dup2`` closed it first);
:func:`stdout_is_diverted` then returns ``True`` and
:func:`oct.mcp.server.start_server` exits with code 1 instead of
letting the SDK serve the protocol on standard error.
- :func:`divert_stdout` returns ``True`` only when it started a
diversion. It returns ``False``, with fd 1 carrying the wire, when a
diversion is already active, when fd 1 is not an open descriptor, or
when fd 1 can be pointed neither at standard error nor at the null
device.
- The private duplicate never lands on descriptor 0 or 2. When one of
them is closed and the duplicate lands there, that duplicate is held
while another is taken and then closed again, so the standard
descriptor stays closed. The SDK reaches the same result with
``F_DUPFD_CLOEXEC`` on POSIX; this form works on every platform.
- :func:`restore_stdout` returns ``True`` only when fd 1 carries the
wire again after a diversion. Without an active diversion it does
nothing and returns ``False``, so callers such as an in-process test
of ``start_server`` can call it unconditionally.
- Bytes written into ``sys.stdout`` during the diversion are flushed
before fd 1 moves back. When standard error rejects that flush (a
broken or closed pipe), fd 1 is pointed at the null device and the
flush repeated, so the rejected bytes are discarded instead of
reaching the wire when the interpreter flushes at exit. Only when the
null device cannot be opened either can such bytes stay buffered.
- ``sys.stdout`` itself is never replaced or wrapped: only the
descriptor moves, so ``sys.stdout.buffer.fileno()`` stays 1 and the
SDK's own claim of fd 1 still applies once the server runs.
- Bytes buffered in ``sys.stdout`` before the diversion starts are not
flushed first, so they are diverted too; nothing legitimate is
written to standard output before the transport starts.
- On Windows a child process inherits the Win32 standard-output handle,
not fd 1. The C runtime of a console interpreter (``python.exe``)
updates that handle whenever fd 1 changes, so a child started during
the diversion writes to standard error too. The GUI-subsystem
``pythonw.exe`` does not; no child process is started during startup,
and the SDK rebinds the handle itself while serving (through pywin32,
when it is installed).
- Module state is not locked: the functions are called from the main
thread during startup, before any tool call can run.
Dependencies
------------
- oct.core.diagnostics (_dbg structured logger)