fix(fp_mcp): redirect Fortran WRITE(6,...) to stderr to protect JSON-RPC channel - #232
Open
HengyuLi-Ozaki-lab wants to merge 1 commit into
Open
Conversation
…RPC channel
fp_mcp was the only MCP server without the fd-isolation guard that
eq_mcp/server.py and tr_mcp/server.py have carried since the
`fix(mcp): redirect Fortran WRITE(6,...) to stderr` patch. Fortran
WRITE(6,...) targets OS fd 1, which for an MCP stdio server IS the
JSON-RPC write pipe, so every Fortran diagnostic line lands in the
protocol stream.
FP is by far the worst offender of the three: FPWRTPRF dumps the whole
radial profile table to unit 6 on every time step, so the damage grows
linearly with NTMAX.
Measured client-side on the fp_iter01 fixture (NRMAX=40, NPMAX=50,
NTHMAX=50, NSAMAX=1, NSMAX=3) at NTMAX=2, counting
"Failed to parse JSONRPC message from server" records emitted by
mcp/client/stdio/__init__.py:157:
before this commit : 211 parse errors per run
after this commit : 0 parse errors per run
(~61 junk lines per extra time step, i.e. ~6000 at NTMAX=100. An
earlier measurement put NTMAX=1 at 150.) Runs still reported
status: ok before the fix — the client logs each unparseable line and
continues — so this was silent protocol corruption, not a hard failure.
Fix is the same two-step dance as eq_mcp/tr_mcp, applied at module load
before any mcp/logging import touches sys.stdout:
1. dup fd 1 (the JSON-RPC pipe) to a fresh fd, then dup2(2, 1) so
Fortran WRITE(6,...) goes to the subprocess stderr;
2. rebuild sys.stdout around the saved fd so the MCP stdio transport
still writes to the real pipe.
--print-tools / --help / --version keep the untouched stdout via the
same _ONESHOT_FLAGS guard the other two servers use.
fd 0 is deliberately NOT redirected — the Fortran library uses stdin
internally and redirecting it raised crash rates in the eq/tr work.
Side benefit confirmed while verifying: Fortran diagnostics are now
readable in the per-attempt stderr log instead of being shredded into
the protocol stream. That is what made the EQ->FP eq_load trace
("# OLD FILE (eq.bin) IS ASSIGNED FOR INPUT.", "XX FROPEN (EQ): FILE
NOT FOUND", "XX FPMESH:EQLOAD:IERR=7") visible at all.
Verified: 45 passed, 55 subtests passed; --print-tools still prints to
the terminal; 7 live fp_mcp runs at 0 parse errors each.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds to
fp_mcpthe fd-isolation guard thateq_mcp/server.pyandtr_mcp/server.pyhave carried since thefix(mcp): redirect Fortran WRITE(6,...) to stderrpatch.fp_mcpwas the only MCP server still missing it.Fortran
WRITE(6,...)targets OS fd 1, which for an MCP stdio server is the JSON-RPC write pipe, so every Fortran diagnostic line lands in the protocol stream.Why FP is the worst case of the three
FPWRTPRFdumps the entire radial profile table to unit 6 on every time step, so the damage grows linearly withNTMAX.Measured client-side on the
fp_iter01fixture (NRMAX=40, NPMAX=50, NTHMAX=50, NSAMAX=1, NSMAX=3) at NTMAX=2, countingFailed to parse JSONRPC message from serverrecords emitted bymcp/client/stdio/__init__.py:157:That is ~61 junk lines per additional time step — roughly 6000 at NTMAX=100. An earlier measurement put NTMAX=1 at 150.
Sample of what was reaching the protocol stream:
Runs still reported
status: okbefore the fix — the MCP client logs each unparseable line and continues — so this was silent protocol corruption, not a hard failure.Change
The same two-step dance as
eq_mcp/tr_mcp, applied at module load before anymcp/loggingimport touchessys.stdout:dupfd 1 (the JSON-RPC pipe) to a fresh fd, thendup2(2, 1)so FortranWRITE(6,...)goes to the subprocess stderr;sys.stdoutaround the saved fd so the MCP stdio transport still writes to the real pipe.--print-tools/--help/--versionkeep the untouched stdout via the same_ONESHOT_FLAGSguard the other two servers use.fd 0 is deliberately not redirected — the Fortran library uses stdin internally, and redirecting it raised crash rates during the eq/tr work.
I did not port
tr_mcp's_redirect_fortran_stdout_to_stderrcontext manager: its own docstring documents it as a no-op after the permanent redirect, and its libgfortran flush is noted there as SIGABRT-prone.Side benefit
Fortran diagnostics become readable in the subprocess stderr instead of being shredded into the protocol stream. That is what made an
eq_loadtrace visible at all while verifying the companionset_param_strPR (# OLD FILE (eq.bin) IS ASSIGNED FOR INPUT.,XX FROPEN (EQ): FILE NOT FOUND,XX FPMESH:EQLOAD:IERR=7).Verification
--print-toolsstill prints to the terminal (the one-shot bypass works)fp_mcpruns at 0 parse errors each; before-state re-measured by temporarily stashing the guard (211 / 212)Note for reviewers
Independent of the companion PR that exposes
set_param_strinfp_mcp— the two touch different regions ofserver.pyand either can merge first. Python-only; no.sorebuild.🤖 Generated with Claude Code