Skip to content

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
k-yoshimi:developfrom
HengyuLi-Ozaki-lab:pr/fp-mcp-stdout-guard
Open

fix(fp_mcp): redirect Fortran WRITE(6,...) to stderr to protect JSON-RPC channel#232
HengyuLi-Ozaki-lab wants to merge 1 commit into
k-yoshimi:developfrom
HengyuLi-Ozaki-lab:pr/fp-mcp-stdout-guard

Conversation

@HengyuLi-Ozaki-lab

Copy link
Copy Markdown
Collaborator

Summary

Adds to fp_mcp 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. fp_mcp was 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

FPWRTPRF dumps the entire 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 : 211 parse errors per run
after  :   0 parse errors per run

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:

Invalid JSON: trailing characters ... input_value='  1  1  7.9500E-01  4.89...'
Invalid JSON: ...                  input_value='--------SAVE_TIME=  0.358601E-01'

Runs still reported status: ok before 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 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 during the eq/tr work.

I did not port tr_mcp's _redirect_fortran_stdout_to_stderr context 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_load trace visible at all while verifying the companion set_param_str PR (# OLD FILE (eq.bin) IS ASSIGNED FOR INPUT., XX FROPEN (EQ): FILE NOT FOUND, XX FPMESH:EQLOAD:IERR=7).

Verification

  • 45 passed, 55 subtests
  • --print-tools still prints to the terminal (the one-shot bypass works)
  • 7 live fp_mcp runs 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_str in fp_mcp — the two touch different regions of server.py and either can merge first. Python-only; no .so rebuild.

🤖 Generated with Claude Code

…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>
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.

1 participant