Commit Graph

28 Commits

Author SHA1 Message Date
Max Isbey 116a95fc37 Re-point fd at the wire when a divert dup2 fails
A Windows dup2 (UCRT before Windows 11) closes its target before
duplicating, so an OSError from the divert can leave the standard
descriptor closed rather than still carrying the wire. Run the same
dup2(private_fd, fd) restore the exit path uses before serving in
place: an idempotent no-op when fd is already on the wire, and a
re-materialization from the private duplicate when the target was
closed. Adds a regression variant that closes the target as UCRT does.

Also scopes the real-host logging note to the default stderr handler.
2026-07-24 16:19:49 +00:00
Max Isbey 57271c28bf Scope the stderr-closed isolation test to POSIX
The wire duplicate is allocated above the standard range atomically only
via F_DUPFD; Windows has no atomic minfd dup, so with fd 2 closed the
duplicate can land in the hole and the transport degrades to serving in
place. That degradation is safe, but it leaves fd 0 on the planted pipe,
and the test's blocking read of it then never returns on Windows.

No-Verification-Needed: test-only platform scoping
2026-07-24 16:19:49 +00:00
Max Isbey 0f9695fe2c Never close a buffer the transport does not own
The transport's text layers now detach instead of closing on garbage
collection. In the in-place paths the wrapped buffer is the sys
stream's own, and closing it destroyed sys.stdout for the rest of the
process (the issue #1933 class, which the claimed path had already
fixed incidentally via the private descriptor).

Also strengthens the fd 0 watchdog migration note (the null device
reports permanently readable, so any-event watchers misfire at startup
rather than merely never firing) and restores the note about child
output volume flowing into the client's stderr channel.
2026-07-24 16:19:48 +00:00
Max Isbey a45283b735 Reshape the stream claim into an explicit state machine
The claim registry maps each standard descriptor to at most one owning
transport; the wire duplicate is allocated where it cannot land in the
standard range (F_DUPFD_CLOEXEC above fd 2 on POSIX) and recorded on
the claim before the descriptor is moved; release restores with a
single dup2 and deregisters only on success. Every failure, modeled or
not, lands on the safe side: the claim is retained and later
transports are refused rather than handed a diverted descriptor.

Deleted by the same design: the roll-forward path (its premise, a
reliably reportable dup2 outcome, does not exist on Windows), and the
restore-time flush (user flush() side effects can destroy the wire;
unflushed stray output now drains after the session instead). The
design was validated through three adversarial verification passes;
the write-up with invariants and accepted residues is on the PR.
2026-07-24 16:19:48 +00:00
Max Isbey 8a076d0006 Roll forward when a failed claim cannot be rolled back
If a mid-claim OSError is followed by a rollback failure, fd is stuck
on the diversion but the private duplicate still holds the wire, so
the transport now keeps the claim and serves from the duplicate like a
completed claim; the restore at exit gets another chance. Previously
the claim was released while fd stayed diverted, letting a later
stdio_server() claim the diversion as its wire.
2026-07-24 16:19:48 +00:00
Max Isbey 6a6c544fec Keep a still-diverted fd claimed when its restore fails
A failed exit-time restore is still swallowed so it never masks what
ended the transport, but the fd now stays in the claimed set: a later
stdio_server() in that process is refused instead of claiming the
diversion and serving the null device as its wire.
2026-07-24 16:19:48 +00:00
Max Isbey 0ba79d4fc2 Hold the stream claim for in-place fallbacks too
A transport that falls back to serving the real stdin or stdout in
place (incomplete descriptor table, or a claim that failed with
OSError) now keeps its fd in the claimed set for its lifetime, so a
second concurrent stdio_server() is refused in every shape rather than
only after a successful diversion. The sentinel now means a transport
owns the stream, not that the fd is currently diverted.
2026-07-24 16:19:48 +00:00
Max Isbey 2e609e18a3 Refuse abnormal processes instead of repairing them in stdio isolation
The stdin/stdout claim now engages only in a normal process: the sys
stream backed by its real descriptor and fds 0-2 all open, which makes
it impossible for the private wire duplicates to land in the standard
range. Anything else - replaced streams, an incomplete descriptor
table, a failed dup - is served in place exactly as v1 was, and a
second concurrent stdio_server() raises RuntimeError instead of
contending for the streams.

This replaces the previous hardening (the dup-above-standard-range
loop, stderr-merge detection, and nested transports serving into the
diversion) with guards, and removes the migration entry: no working
code changes behavior, so there is nothing to migrate. Comments and
docstrings trimmed throughout the diff.
2026-07-24 16:19:48 +00:00
Max Isbey 2b0f3ace9f Isolate the stdio server's stdout from handler code and subprocesses
While serving on the process's real stdout, stdio_server now moves the
protocol pipe to a private descriptor and points fd 1 - and, on
Windows, the standard output handle - at stderr, restoring it when the
transport exits. A stray print() in handler code or a child process
writing to its inherited stdout lands in the client's log instead of
corrupting the JSON-RPC stream. The null device stands in when stderr
is unusable or, on POSIX, is detected as merged into stdout (2>&1).

The stdin claim generalizes into the shared _claim_fd mechanism: one
lock-guarded sentinel table covers both descriptors, private wire
duplicates are forced above the standard descriptor range so a process
started with a standard descriptor closed cannot hand the wire out as
its "stderr", and a failed claim degrades to serving the sys stream's
buffer in place exactly as v1 did.

Docs now describe the guarded behavior with its remaining gaps (output
flushed before serving begins, injected streams, merged stderr on
Windows), and the transport:stdio:stream-purity divergence narrows
accordingly.
2026-07-24 16:19:48 +00:00
Max Isbey fbe9841788 Isolate the stdio server's stdin from handler subprocesses
While serving on the process's real stdin, stdio_server() now reads the
protocol from a private duplicate of fd 0 and points fd 0 (and, on
Windows, the standard input handle) at the null device, restoring both
on exit. Children spawned by handler code then inherit the null device
instead of the protocol pipe.

A child that inherited the pipe could consume protocol bytes on any
platform, and on Windows a Python child hangs inside interpreter
startup behind the transport's pending read (CPython gh-78961) until
the next request arrives, so any tool that ran a subprocess without
stdin=DEVNULL appeared to hang until timeout.

Isolation engages only when sys.stdin is backed by the real fd 0, at
most once per process, and degrades to reading stdin in place when the
descriptor table cannot be rearranged.

Fixes #671.
2026-07-24 16:19:48 +00:00
Max 837ef904f8 Align with spec #3002: optional clientInfo, serverInfo in result _meta (#3143)
Deploy Docs / deploy-docs (push) Has been cancelled
CI / checks (push) Failing after 24m23s
CI / all-green (push) Has been cancelled
Conformance Tests / server-conformance (push) Has been cancelled
Conformance Tests / client-conformance (push) Has been cancelled
GitHub Actions Security Analysis / zizmor (push) Has been cancelled
2026-07-23 12:00:36 +01:00
Max 080f2a869d Harden the dual-era stream loop's era-lock and rejection semantics (#3040) 2026-07-01 17:07:12 +01:00
Max e50fb5be19 Serve the 2026-07-28 era over stdio and other stream-pair transports (#3038) 2026-07-01 00:11:56 +01:00
Marcelo Trylesinski 0ee7f1b293 Split protocol types into a standalone mcp-types package (#2973) 2026-06-25 19:18:38 +02:00
Max bdc48e98b1 Fix stdio client shutdown bugs and rebuild the stdio test suite (#2773) 2026-06-05 16:15:43 +01:00
Max Isbey 1a2244f402 fix: handle non-UTF-8 bytes in stdio server stdin (#2302) 2026-03-17 18:40:39 +00:00
Marcelo Trylesinski f4672c5084 Drop RootModel from JSONRPCMessage (#1908) 2026-01-19 14:04:15 +01:00
Max Isbey 89e9c43acf Get baseline 100% clean coverage (#1553) 2025-11-11 14:09:32 +01:00
Marcelo Trylesinski c7671e470c Add pyright strict mode on the whole project (#1254) 2025-08-11 18:56:37 +01:00
Marcelo Trylesinski 543961968c Use 120 characters instead of 88 (#856) 2025-06-11 11:45:50 +02:00
ihrpr 9d99aee014 Revert "Add message queue for SSE messages POST endpoint (#459)" (#649) 2025-05-07 16:35:20 +01:00
Akash D 3b1b213a96 Add message queue for SSE messages POST endpoint (#459) 2025-05-06 17:10:43 -07:00
ihrpr da0cf22355 Wrap JSONRPC messages with SessionMessage for metadata support (#590) 2025-05-02 14:29:00 +01:00
Marcelo Trylesinski 7196604468 Revert "refactor: reorganize message handling for better type safety and clar…" (#282)
Main branch checks / checks (push) Failing after 0s
Check uv.lock / check-lock (push) Has been cancelled
This reverts commit 9d0f2daddb.
2025-03-14 09:50:46 +00:00
David Soria Parra 9d0f2daddb refactor: reorganize message handling for better type safety and clarity (#239)
Main branch checks / checks (push) Failing after 0s
Check uv.lock / check-lock (push) Has been cancelled
* refactor: improve typing with memory stream type aliases

Move memory stream type definitions to models.py and use them throughout
the codebase for better type safety and maintainability.

GitHub-Issue:#201

* refactor: move streams to ParsedMessage

* refactor: update test files to use ParsedMessage

Updates test files to work with the ParsedMessage stream type aliases
and fixes a line length issue in test_201_client_hangs_on_logging.py.

Github-Issue:#201

* refactor: rename ParsedMessage to MessageFrame for clarity

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: move MessageFrame class to types.py for better code organization

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>

* fix pyright

* refactor: update websocket client to use MessageFrame

Modified the websocket client to work with the new MessageFrame type,
preserving raw message text and properly extracting the root JSON-RPC
message when sending.

Github-Issue:#204

* fix: use NoneType instead of None for type parameters in MessageFrame

🤖 Generated with [Claude Code](https://claude.ai/code)

* refactor: rename root to message
2025-03-13 13:44:55 +00:00
David Soria Parra ed87ae9f06 rename mcp_python to mcp 2024-11-11 12:40:32 +00:00
Justin Spahr-Summers 4040945cdf Exclude Nones 2024-10-02 21:58:33 +01:00
David Soria Parra 4cbf815430 Initial import 2024-09-24 22:04:19 +01:00