Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/).
terminal shows that line and no longer the library's output. Where
`fstat` gives a descriptor no identity (Windows for a pipe or a console)
the merged layout is not recognised and fd 1 goes to stderr as usual.
- **MCP servers `ti_mcp`, `wr_mcp`, `wrx_mcp`: the same fd isolation** (#262).
The three servers had the layout of the other three and the same exposure:
`main()` now calls `_fd_isolation.install()` immediately before the stdio
server is built, importing a server module leaves the importing process's
fd 1 and `sys.stdout` alone, and `--help` / `--print-tools` / the
missing-SDK path return before anything is redirected. Each server's
`tests/test_fd_isolation.py` runs the shared checks
(`python/mcp-servers/fd_isolation_cases.py`), and
`python/tests/test_fd_isolation.py` checks that all six servers run every
one of them. `fp_mcp` follows in #232.

## TR Phase L — library-ization (2026-04-18)

Expand Down
24 changes: 24 additions & 0 deletions python/mcp-servers/ti_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@
The server is intentionally small — the real heavy lifting is in
:mod:`tilib`. This file mirrors ``tr_mcp/server.py`` and swaps the
backing library, registry, and state schema to match TI.

fd-isolation (Fortran WRITE(6) vs MCP JSON-RPC)
-----------------------------------------------
Fortran WRITE(6,...) targets OS fd 1, which is also the JSON-RPC write
pipe to the MCP client parent. Any Fortran diagnostic line corrupts the
pipe: the client logs "Failed to parse JSONRPC message from server" for
each junk line, and in the worst case the session drops with
"Connection closed".

main() therefore calls ``_fd_isolation.install()`` (the module shared by
the stdio servers, ``python/_fd_isolation.py``) immediately before the
stdio server is built -- NOT at import time, see #227 item 1.
It moves the pipe to a descriptor of its own, rebuilds ``sys.stdout`` on
it and gives fd 1 to stderr, so that Fortran WRITE(6,...) lands in the
server's log.
"""
from __future__ import annotations

Expand Down Expand Up @@ -76,6 +91,10 @@
TilibNotImplementedError,
)

# The fd isolation shared by the stdio servers; it lives beside the wrapper,
# in ``<repo>/python/``.
import _fd_isolation # noqa: E402


# =====================================================================
# Parameter & state schema metadata.
Expand Down Expand Up @@ -690,6 +709,11 @@ def main(argv: Optional[List[str]] = None) -> int:
)
return 2

# Install the fd isolation now -- NOT at import time (#227 item 1).
# Everything above this point (--help, --print-tools, the MCP-missing
# error path) returns before we touch the host process's fds.
_fd_isolation.install()

server = build_server()
# FastMCP >=0.9 exposes .run() for stdio transport by default.
server.run()
Expand Down
43 changes: 43 additions & 0 deletions python/mcp-servers/ti_mcp/tests/test_fd_isolation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""ti_mcp's share of the fd-isolation checks (fd_isolation_cases): its
main() installs the shared isolation, and nothing else of it does."""
import sys
from pathlib import Path

import pytest

MCP_ROOT = Path(__file__).resolve().parents[2] # <repo>/python/mcp-servers
if str(MCP_ROOT) not in sys.path:
sys.path.insert(0, str(MCP_ROOT))

pytest.register_assert_rewrite("fd_isolation_cases")
import fd_isolation_cases as cases # noqa: E402

SERVER = "ti_mcp"


def test_import_leaves_fd1_and_sys_stdout_alone():
cases.import_leaves_fd1_and_sys_stdout_alone(SERVER)


def test_main_installs_the_isolation_before_the_stdio_server_is_built():
cases.main_installs_the_isolation_before_the_stdio_server_is_built(SERVER)


@pytest.mark.parametrize("flag", ["--help", "--print-tools"])
def test_help_and_print_tools_install_nothing(flag):
cases.flag_installs_nothing(SERVER, flag)


def test_missing_sdk_installs_nothing():
cases.missing_sdk_installs_nothing(SERVER)


@pytest.mark.skipif(sys.platform == "win32",
reason="fstat gives a pipe no identity there: a merged stream is not recognised")
def test_stderr_merged_into_stdout_keeps_fortran_output_off_the_stream():
cases.stderr_merged_into_stdout_keeps_fortran_output_off_the_stream(SERVER)


@pytest.mark.skipif(sys.platform == "win32", reason="preexec_fn is POSIX only")
def test_closed_fd2_keeps_everything_but_json_rpc_off_the_pipe():
cases.closed_fd2_keeps_everything_but_json_rpc_off_the_pipe(SERVER)
24 changes: 24 additions & 0 deletions python/mcp-servers/wr_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@
:mod:`wrlib`. This file mirrors ``tr_mcp/server.py`` with only the
module-specific names swapped so that future ti / wrx / fp servers
can follow the same pattern.

fd-isolation (Fortran WRITE(6) vs MCP JSON-RPC)
-----------------------------------------------
Fortran WRITE(6,...) targets OS fd 1, which is also the JSON-RPC write
pipe to the MCP client parent. Any Fortran diagnostic line corrupts the
pipe: the client logs "Failed to parse JSONRPC message from server" for
each junk line, and in the worst case the session drops with
"Connection closed".

main() therefore calls ``_fd_isolation.install()`` (the module shared by
the stdio servers, ``python/_fd_isolation.py``) immediately before the
stdio server is built -- NOT at import time, see #227 item 1.
It moves the pipe to a descriptor of its own, rebuilds ``sys.stdout`` on
it and gives fd 1 to stderr, so that Fortran WRITE(6,...) lands in the
server's log.
"""
from __future__ import annotations

Expand Down Expand Up @@ -84,6 +99,10 @@
WrlibNotImplementedError,
)

# The fd isolation shared by the stdio servers; it lives beside the wrapper,
# in ``<repo>/python/``.
import _fd_isolation # noqa: E402


# =====================================================================
# Parameter & state schema metadata.
Expand Down Expand Up @@ -724,6 +743,11 @@ def main(argv: Optional[List[str]] = None) -> int:
)
return 2

# Install the fd isolation now -- NOT at import time (#227 item 1).
# Everything above this point (--help, --print-tools, the MCP-missing
# error path) returns before we touch the host process's fds.
_fd_isolation.install()

server = build_server()
# FastMCP >=0.9 exposes .run() for stdio transport by default.
server.run()
Expand Down
43 changes: 43 additions & 0 deletions python/mcp-servers/wr_mcp/tests/test_fd_isolation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""wr_mcp's share of the fd-isolation checks (fd_isolation_cases): its
main() installs the shared isolation, and nothing else of it does."""
import sys
from pathlib import Path

import pytest

MCP_ROOT = Path(__file__).resolve().parents[2] # <repo>/python/mcp-servers
if str(MCP_ROOT) not in sys.path:
sys.path.insert(0, str(MCP_ROOT))

pytest.register_assert_rewrite("fd_isolation_cases")
import fd_isolation_cases as cases # noqa: E402

SERVER = "wr_mcp"


def test_import_leaves_fd1_and_sys_stdout_alone():
cases.import_leaves_fd1_and_sys_stdout_alone(SERVER)


def test_main_installs_the_isolation_before_the_stdio_server_is_built():
cases.main_installs_the_isolation_before_the_stdio_server_is_built(SERVER)


@pytest.mark.parametrize("flag", ["--help", "--print-tools"])
def test_help_and_print_tools_install_nothing(flag):
cases.flag_installs_nothing(SERVER, flag)


def test_missing_sdk_installs_nothing():
cases.missing_sdk_installs_nothing(SERVER)


@pytest.mark.skipif(sys.platform == "win32",
reason="fstat gives a pipe no identity there: a merged stream is not recognised")
def test_stderr_merged_into_stdout_keeps_fortran_output_off_the_stream():
cases.stderr_merged_into_stdout_keeps_fortran_output_off_the_stream(SERVER)


@pytest.mark.skipif(sys.platform == "win32", reason="preexec_fn is POSIX only")
def test_closed_fd2_keeps_everything_but_json_rpc_off_the_pipe():
cases.closed_fd2_keeps_everything_but_json_rpc_off_the_pipe(SERVER)
24 changes: 24 additions & 0 deletions python/mcp-servers/wrx_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@
This mirrors the ``tr_mcp`` reference implementation as closely as
possible; the main wrx-specific additions are the WRX_RUN_OK gate and
the different parameter registry / state schema.

fd-isolation (Fortran WRITE(6) vs MCP JSON-RPC)
-----------------------------------------------
Fortran WRITE(6,...) targets OS fd 1, which is also the JSON-RPC write
pipe to the MCP client parent. Any Fortran diagnostic line corrupts the
pipe: the client logs "Failed to parse JSONRPC message from server" for
each junk line, and in the worst case the session drops with
"Connection closed".

main() therefore calls ``_fd_isolation.install()`` (the module shared by
the stdio servers, ``python/_fd_isolation.py``) immediately before the
stdio server is built -- NOT at import time, see #227 item 1.
It moves the pipe to a descriptor of its own, rebuilds ``sys.stdout`` on
it and gives fd 1 to stderr, so that Fortran WRITE(6,...) lands in the
server's log.
"""
from __future__ import annotations

Expand Down Expand Up @@ -83,6 +98,10 @@
WrxlibNotImplementedError,
)

# The fd isolation shared by the stdio servers; it lives beside the wrapper,
# in ``<repo>/python/``.
import _fd_isolation # noqa: E402


# =====================================================================
# Parameter & state schema metadata.
Expand Down Expand Up @@ -718,6 +737,11 @@ def main(argv: Optional[List[str]] = None) -> int:
)
return 2

# Install the fd isolation now -- NOT at import time (#227 item 1).
# Everything above this point (--help, --print-tools, the MCP-missing
# error path) returns before we touch the host process's fds.
_fd_isolation.install()

server = build_server()
# FastMCP >=0.9 exposes .run() for stdio transport by default.
server.run()
Expand Down
43 changes: 43 additions & 0 deletions python/mcp-servers/wrx_mcp/tests/test_fd_isolation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""wrx_mcp's share of the fd-isolation checks (fd_isolation_cases): its
main() installs the shared isolation, and nothing else of it does."""
import sys
from pathlib import Path

import pytest

MCP_ROOT = Path(__file__).resolve().parents[2] # <repo>/python/mcp-servers
if str(MCP_ROOT) not in sys.path:
sys.path.insert(0, str(MCP_ROOT))

pytest.register_assert_rewrite("fd_isolation_cases")
import fd_isolation_cases as cases # noqa: E402

SERVER = "wrx_mcp"


def test_import_leaves_fd1_and_sys_stdout_alone():
cases.import_leaves_fd1_and_sys_stdout_alone(SERVER)


def test_main_installs_the_isolation_before_the_stdio_server_is_built():
cases.main_installs_the_isolation_before_the_stdio_server_is_built(SERVER)


@pytest.mark.parametrize("flag", ["--help", "--print-tools"])
def test_help_and_print_tools_install_nothing(flag):
cases.flag_installs_nothing(SERVER, flag)


def test_missing_sdk_installs_nothing():
cases.missing_sdk_installs_nothing(SERVER)


@pytest.mark.skipif(sys.platform == "win32",
reason="fstat gives a pipe no identity there: a merged stream is not recognised")
def test_stderr_merged_into_stdout_keeps_fortran_output_off_the_stream():
cases.stderr_merged_into_stdout_keeps_fortran_output_off_the_stream(SERVER)


@pytest.mark.skipif(sys.platform == "win32", reason="preexec_fn is POSIX only")
def test_closed_fd2_keeps_everything_but_json_rpc_off_the_pipe():
cases.closed_fd2_keeps_everything_but_json_rpc_off_the_pipe(SERVER)
2 changes: 1 addition & 1 deletion python/tests/test_fd_isolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import fd_isolation_cases as cases # noqa: E402
from fd_isolation_cases import run # noqa: E402

SERVERS = ("eq_mcp", "tr_mcp", "tot_mcp")
SERVERS = ("eq_mcp", "tr_mcp", "tot_mcp", "ti_mcp", "wr_mcp", "wrx_mcp")


# ----- every server runs every shared check -----
Expand Down
Loading