8Keep the stdio transport's protocol channel clean while the ``oct-mcp``
9server starts. Under the ``stdio`` transport, standard output (file
10descriptor 1) carries JSON-RPC messages, and the MCP specification says
11a stdio server must not write anything else there. While
12:func:`mcp.server.stdio.stdio_server` serves, the MCP Python SDK already
13points fd 1 at standard error and writes the protocol through a private
14duplicate. Before that, from process start until
15:meth:`mcp.server.MCPServer.run` enters the transport, nothing protected
16fd 1, so a ``_dbg`` record from startup (a lifecycle line with the MCP
17domain at level 2 or higher, any L1 startup error, the production-mode
18downgrade warning, or the FS-25 fallback warning) was written onto the
19wire ahead of the ``initialize`` response. This module covers that
20startup window with the same mechanism the SDK uses while serving.
24- :func:`divert_stdout`: keep a private duplicate of fd 1 (the wire),
25 always above the standard descriptors, and point fd 1 at standard
26 error, or at the null device when standard error is not open. Every
27 write to fd 1 then misses the wire, whoever makes it: ``_dbg`` in any
28 output mode, ``print()``, a third-party library or a child process
29 that inherits standard output.
30- :func:`restore_stdout`: flush Python's ``sys.stdout`` while fd 1 is
31 still diverted, discarding into the null device whatever standard
32 error rejects, then point fd 1 back at the wire and close the private
33 duplicate. :func:`oct.mcp.server.start_server` calls it immediately
34 before ``MCPServer.run(transport="stdio")``, whose ``stdio_server()``
35 claims fd 1 again at once.
36- :func:`stdout_is_diverted`: report whether a diversion is still
37 recorded, so :func:`oct.mcp.server.start_server` can refuse to serve
38 when the hand-back failed.
44 L1 - errors: fd 1 could not be pointed back at the wire
45 L2 - lifecycle: diversion started, fd 1 handed back to the transport;
46 EXTERNAL_ERROR when standard error rejected buffered output
47 L3 - (none): the lifecycle records are the whole trace
48 L4 - (none): no deep trace is emitted
50Every record is written while fd 1 is diverted, so none of them can
51reach the wire: the hand-back record is emitted before fd 1 moves.
55- None of the three public functions raises. A failed diversion leaves
56 fd 1 carrying the wire. A failed hand-back leaves the diversion
57 recorded, with fd 1 still off the wire (on standard error or the null
58 device, or closed when a failing Windows ``dup2`` closed it first);
59 :func:`stdout_is_diverted` then returns ``True`` and
60 :func:`oct.mcp.server.start_server` exits with code 1 instead of
61 letting the SDK serve the protocol on standard error.
62- :func:`divert_stdout` returns ``True`` only when it started a
63 diversion. It returns ``False``, with fd 1 carrying the wire, when a
64 diversion is already active, when fd 1 is not an open descriptor, or
65 when fd 1 can be pointed neither at standard error nor at the null
67- The private duplicate never lands on descriptor 0 or 2. When one of
68 them is closed and the duplicate lands there, that duplicate is held
69 while another is taken and then closed again, so the standard
70 descriptor stays closed. The SDK reaches the same result with
71 ``F_DUPFD_CLOEXEC`` on POSIX; this form works on every platform.
72- :func:`restore_stdout` returns ``True`` only when fd 1 carries the
73 wire again after a diversion. Without an active diversion it does
74 nothing and returns ``False``, so callers such as an in-process test
75 of ``start_server`` can call it unconditionally.
76- Bytes written into ``sys.stdout`` during the diversion are flushed
77 before fd 1 moves back. When standard error rejects that flush (a
78 broken or closed pipe), fd 1 is pointed at the null device and the
79 flush repeated, so the rejected bytes are discarded instead of
80 reaching the wire when the interpreter flushes at exit. Only when the
81 null device cannot be opened either can such bytes stay buffered.
82- ``sys.stdout`` itself is never replaced or wrapped: only the
83 descriptor moves, so ``sys.stdout.buffer.fileno()`` stays 1 and the
84 SDK's own claim of fd 1 still applies once the server runs.
85- Bytes buffered in ``sys.stdout`` before the diversion starts are not
86 flushed first, so they are diverted too; nothing legitimate is
87 written to standard output before the transport starts.
88- On Windows a child process inherits the Win32 standard-output handle,
89 not fd 1. The C runtime of a console interpreter (``python.exe``)
90 updates that handle whenever fd 1 changes, so a child started during
91 the diversion writes to standard error too. The GUI-subsystem
92 ``pythonw.exe`` does not; no child process is started during startup,
93 and the SDK rebinds the handle itself while serving (through pywin32,
94 when it is installed).
95- Module state is not locked: the functions are called from the main
96 thread during startup, before any tool call can run.
100- oct.core.diagnostics (_dbg structured logger)
103from __future__
import annotations
107from contextlib
import suppress
109from oct.core.diagnostics
import _dbg
119_wire_fd: int |
None =
None
123 """Duplicate *fd* onto a descriptor above the standard range.
125 A duplicate that lands on 0 or 2 (a closed standard descriptor) is
126 held while the next one is taken and closed afterwards, so the wire
127 never sits behind a standard descriptor and a closed one stays
128 closed. With *fd* (fd 1) itself open, at most descriptors 0 and 2
129 can be free below 3, so the third duplicate is always above the
135 When *fd* cannot be duplicated, or when three duplicates in a
136 row land in the standard range.
140 for _attempt
in range(_STDERR_FD + 1):
141 duplicate = os.dup(fd)
142 if duplicate > _STDERR_FD:
144 held.append(duplicate)
145 raise OSError(f
"no duplicate of fd {fd} above the standard range")
148 with suppress(OSError):
153 """Make fd 1 a duplicate of *target_fd*; return whether that worked."""
155 os.dup2(target_fd, _STDOUT_FD)
162 """Make fd 1 a duplicate of the null device; return whether that worked."""
164 null_fd = os.open(os.devnull, os.O_WRONLY)
170 with suppress(OSError):
175 """Point fd 1 at standard error, or at the null device as a fallback.
177 Returns the name of the target, or ``None`` when neither worked.
180 return "standard error"
182 return "the null device"
187 """Flush ``sys.stdout`` and ``sys.__stdout__``.
189 Returns ``False`` when a flush was rejected with :class:`OSError`,
190 which leaves the bytes buffered. A closed stream holds no bytes, so
191 its :class:`ValueError` counts as flushed.
194 for stream
in (sys.stdout, sys.__stdout__):
207 """Point fd 1 at standard error and keep the wire in a private descriptor.
209 Called by :func:`oct.mcp.__main__.main` under the ``stdio`` transport,
210 right after argument parsing, so ``--help`` still prints to standard
211 output but nothing written during startup reaches the protocol
212 channel. :func:`restore_stdout` ends the diversion.
216 ``True`` when this call started a diversion. ``False`` when a
217 diversion was already active or fd 1 could not be diverted; fd 1
218 then still carries the wire.
221 func =
"divert_stdout"
222 if _wire_fd
is not None:
235 with suppress(OSError):
241 f
"standard output diverted to {target} until the stdio transport starts",
248 """End a diversion started by :func:`divert_stdout`.
250 Emits its lifecycle record and flushes ``sys.stdout`` while fd 1 is
251 still diverted, then points fd 1 back at the wire and closes the
252 private duplicate. Called by :func:`oct.mcp.server.start_server`
253 immediately before ``MCPServer.run(transport="stdio")``, and again
254 by :func:`oct.mcp.__main__.main` on the way out, where it is a
255 no-op unless startup failed.
259 ``True`` when fd 1 carries the wire again. ``False`` when no
260 diversion was active, or when fd 1 could not be pointed back; in the
261 second case an L1 record says so and the diversion stays recorded,
262 which :func:`stdout_is_diverted` reports.
265 func =
"restore_stdout"
269 _dbg(
"MCP", func,
"handing standard output back to the stdio transport", 2)
277 "EXTERNAL_ERROR: standard error rejected buffered output; "
278 "discarding it into the null device",
286 "SYSTEM_ERROR: could not point standard output back at the stdio wire",
290 with suppress(OSError):
297 """Return whether a diversion started by :func:`divert_stdout` is still recorded.
299 ``True`` after a failed hand-back: fd 1 does not carry the wire, so
300 the stdio transport must not start.
302 return _wire_fd
is not None
bool stdout_is_diverted()
bool _flush_python_stdout()
bool _point_fd1_at(int target_fd)
int _dup_above_std(int fd)
bool _point_fd1_at_null_device()