Skip to content

[Server] Fix stdio server ending by itself on an idle Windows pipe - #671

Open
dg wants to merge 1 commit into
modelcontextprotocol:mainfrom
dg:fix-stdio-idle-windows-pipe
Open

dg wants to merge 1 commit into
modelcontextprotocol:mainfrom
dg:fix-stdio-idle-windows-pipe

Conversation

@dg

@dg dg commented Nov 5, 2026

Copy link
Copy Markdown

On Windows, fgets() on an idle pipe blocks even in non-blocking mode and after about 13.5 hours reports a false EOF, so a stdio server ends by itself (php/php-src#25204). StdioTransport now reads only once stream_select() reports input, which also keeps the run loop going while idle. This needs PHP 8.5, older versions behave as before.

On Windows, PHP waits for data on a pipe in a loop of 3,200,000 usleep(10) calls, also in non-blocking
mode. One call takes about 15 ms there, so fgets() on an idle pipe blocks for about 13.5 hours and then
reports a false EOF, which ends listen(). Reading only after stream_select() reports input stays out of
that loop. It needs PHP 8.5, where stream_select() stopped reporting idle pipes as readable
(php/php-src#16889).

stream_select() throws a ValueError on a stream it cannot select, like php://memory, instead of
returning false.
dg added a commit to dg/pohoda-mcp that referenced this pull request Nov 5, 2026
PHP on Windows waits for data on a pipe in a loop of 3,200,000 usleep(10) calls, also in non-blocking
mode. One call takes about 15 ms there, so fgets() on an idle pipe blocks for about 13.5 hours and then
reports a false EOF, which ends the server with exit code 0 and no error. mcp/sdk calls fgets() on an
idle pipe, so the transport now reads only after stream_select() reports input. It needs PHP 8.5, as
stream_select() on Windows reports an idle pipe as readable before that.

See php/php-src#25204 and modelcontextprotocol/php-sdk#671.
dg added a commit to dg/pohoda-mcp that referenced this pull request Nov 5, 2026
PHP on Windows waits for data on a pipe in a loop of 3,200,000 usleep(10) calls, also in non-blocking
mode. One call takes about 15 ms there, so fgets() on an idle pipe blocks for about 13.5 hours and then
reports a false EOF, which ends the server with exit code 0 and no error. mcp/sdk calls fgets() on an
idle pipe, so the transport now reads only after stream_select() reports input. It needs PHP 8.5, as
stream_select() on Windows reports an idle pipe as readable before that.

See php/php-src#25204 and modelcontextprotocol/php-sdk#671.
dg added a commit to dg/pohoda-mcp that referenced this pull request Nov 5, 2026
PHP on Windows waits for data on a pipe in a loop of 3,200,000 usleep(10) calls, also in non-blocking
mode. One call takes about 15 ms there, so fgets() on an idle pipe blocks for about 13.5 hours and then
reports a false EOF, which ends the server with exit code 0 and no error. mcp/sdk calls fgets() on an
idle pipe, so the transport now reads only after stream_select() reports input. It needs PHP 8.5, as
stream_select() on Windows reports an idle pipe as readable before that.

See php/php-src#25204 and modelcontextprotocol/php-sdk#671.
dg added a commit to dg/pohoda-mcp that referenced this pull request Nov 5, 2026
PHP on Windows waits for data on a pipe in a loop of 3,200,000 usleep(10) calls, also in non-blocking
mode. One call takes about 15 ms there, so fgets() on an idle pipe blocks for about 13.5 hours and then
reports a false EOF, which ends the server with exit code 0 and no error. mcp/sdk calls fgets() on an
idle pipe, so the transport now reads only after stream_select() reports input. It needs PHP 8.5, as
stream_select() on Windows reports an idle pipe as readable before that.

See php/php-src#25204 and modelcontextprotocol/php-sdk#671.
dg added a commit to dg/pohoda-mcp that referenced this pull request Nov 5, 2026
PHP on Windows waits for data on a pipe in a loop of 3,200,000 usleep(10) calls, also in non-blocking
mode. One call takes about 15 ms there, so fgets() on an idle pipe blocks for about 13.5 hours and then
reports a false EOF, which ends the server with exit code 0 and no error. mcp/sdk calls fgets() on an
idle pipe, so the transport now reads only after stream_select() reports input. It needs PHP 8.5, as
stream_select() on Windows reports an idle pipe as readable before that.

The run loop then also runs on an idle connection, and mcp/sdk 0.9 only reads the session there, so the
first read past its TTL of an hour deletes it and every later request gets "Session not found or has
expired" without an id. A stdio session lasts as long as the process, so the server gives it no TTL.

See php/php-src#25204, modelcontextprotocol/php-sdk#671 and modelcontextprotocol/php-sdk#672.
dg added a commit to dg/pohoda-mcp that referenced this pull request Nov 5, 2026
PHP on Windows waits for data on a pipe in a loop of 3,200,000 usleep(10) calls, also in non-blocking
mode. One call takes about 15 ms there, so fgets() on an idle pipe blocks for about 13.5 hours and then
reports a false EOF, which ends the server with exit code 0 and no error. mcp/sdk calls fgets() on an
idle pipe, so the transport now reads only after stream_select() reports input. It needs PHP 8.5, as
stream_select() on Windows reports an idle pipe as readable before that.

The run loop then also runs on an idle connection, and mcp/sdk 0.9 only reads the session there, so the
first read past its TTL of an hour deletes it and every later request gets "Session not found or has
expired" without an id. A stdio session lasts as long as the process, so the server gives it no TTL.

See php/php-src#25204, modelcontextprotocol/php-sdk#671 and modelcontextprotocol/php-sdk#672.
dg added a commit to nette/mcp-inspector that referenced this pull request Nov 6, 2026
PHP on Windows waits for data on a pipe in a loop of 3,200,000 usleep(10) calls, also in non-blocking
mode. One call takes about 15 ms there, so fgets() on an idle pipe blocks for about 13.5 hours and then
reports a false EOF, which ends the server with exit code 0 and no error. mcp/sdk calls fgets() on an
idle pipe, so the transport now reads only after stream_select() reports input. It needs PHP 8.5, as
stream_select() on Windows reports an idle pipe as readable before that.

The run loop then also runs on an idle connection, and mcp/sdk 0.9 only reads the session there, so the
first read past its TTL of an hour deletes it and every later request gets "Session not found or has
expired" without an id. A stdio session lasts as long as the process, so the server gives it no TTL.

See php/php-src#25204, modelcontextprotocol/php-sdk#671 and modelcontextprotocol/php-sdk#672.
dg added a commit to dg/google-services that referenced this pull request Nov 6, 2026
PHP on Windows waits for data on a pipe in a loop of 3,200,000 usleep(10) calls, also in non-blocking
mode. One call takes about 15 ms there, so fgets() on an idle pipe blocks for about 13.5 hours and then
reports a false EOF, which ends the server with exit code 0 and no error. mcp/sdk calls fgets() on an
idle pipe, so the transport now reads only after stream_select() reports input. It needs PHP 8.5, as
stream_select() on Windows reports an idle pipe as readable before that.

The run loop then also runs on an idle connection, and mcp/sdk 0.9 only reads the session there, so the
first read past its TTL of an hour deletes it and every later request gets "Session not found or has
expired" without an id. A stdio session lasts as long as the process, so the server gives it no TTL.

See php/php-src#25204, modelcontextprotocol/php-sdk#671 and modelcontextprotocol/php-sdk#672.
dg added a commit to dg/fio-mcp that referenced this pull request Nov 6, 2026
PHP on Windows waits for data on a pipe in a loop of 3,200,000 usleep(10) calls, also in non-blocking
mode. One call takes about 15 ms there, so fgets() on an idle pipe blocks for about 13.5 hours and then
reports a false EOF, which ends the server with exit code 0 and no error. mcp/sdk calls fgets() on an
idle pipe, so the transport now reads only after stream_select() reports input. It needs PHP 8.5, as
stream_select() on Windows reports an idle pipe as readable before that.

The run loop then also runs on an idle connection, and mcp/sdk 0.9 only reads the session there, so the
first read past its TTL of an hour deletes it and every later request gets "Session not found or has
expired" without an id. A stdio session lasts as long as the process, so the server gives it no TTL.

See php/php-src#25204, modelcontextprotocol/php-sdk#671 and modelcontextprotocol/php-sdk#672.

This branch has not been deployed

No deployments
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