Skip to content

Handle stdin EOF gracefully in MCP stdio mode (#3428)#3647

Open
anushakolan wants to merge 1 commit into
mainfrom
dev/anushakolan/mcp-stdio-eof-graceful-exit
Open

Handle stdin EOF gracefully in MCP stdio mode (#3428)#3647
anushakolan wants to merge 1 commit into
mainfrom
dev/anushakolan/mcp-stdio-eof-graceful-exit

Conversation

@anushakolan
Copy link
Copy Markdown
Contributor

Why make this change?

When dab start --mcp-stdio was launched by a client (such as MCP Inspector or VS Code) and the client closed the stdin pipe (EOF), the process crashed with exit code 4294967295 instead of exiting cleanly. This caused noisy error signals in any tool or host that expected a clean 0 exit on shutdown.

What is this change?

Modified McpStdioServer.RunAsync to handle stdin EOF as a normal shutdown signal:

  • Replaced direct Console.OpenStandardInput() + StreamReader with Console.In (testable, injectable)
  • Added explicit null check on the return value of ReadLineAsyncnull means EOF, which now triggers a clean return instead of an unhandled exception
  • Blank lines continue to be skipped as before

How was this tested?

Unit Tests

Added McpStdioServerRunAsyncTests:

  • RunAsync_EofOnStdin_ExitsGracefullyWithoutOutput — feeds empty stdin, verifies clean exit with no stdout output
  • RunAsync_BlankLineThenShutdown_IgnoresBlankLineAndHandlesShutdown — feeds blank line + shutdown request, verifies single valid response and clean exit

Manual Testing

  • Reproduced the exact reported scenario: piped an initialize request then closed stdin — verified EXIT_CODE=0
    echo '{"jsonrpc":"2.0","method":"initialize",...,"id":1}' | dab start --config dab-config.json --mcp-stdio
    echo "Exit code: $?"   # was 4294967295, now 0
    
  • Verified EXIT_CODE=0 for normal shutdown request path
  • Verified successful MCP tool calls (read_records, describe_entities) continue to work after the change

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the robustness of DAB’s MCP stdio hosting path by treating stdin EOF (client closes the pipe) as a normal shutdown signal rather than an error condition that terminates the process with a noisy non-zero exit code.

Changes:

  • Updated McpStdioServer.RunAsync to read from Console.In and to exit cleanly when ReadLineAsync returns null (EOF).
  • Added unit tests validating clean exit on EOF and correct handling of blank lines followed by a shutdown request.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/Service.Tests/UnitTests/McpStdioServerRunAsyncTests.cs Adds unit coverage for EOF-on-stdin and blank-line handling in the MCP stdio loop.
src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs Treats stdin EOF as graceful shutdown by returning from the read loop when ReadLineAsync yields null.

@anushakolan anushakolan added bug Something isn't working cri Customer Reported issue assign-for-review labels Jun 2, 2026
@anushakolan anushakolan added this to the June 2026 milestone Jun 2, 2026

using Stream stdin = Console.OpenStandardInput();
using StreamReader reader = new(stdin, utf8NoBom);
// Read through Console.In so tests can inject stdin and the process
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: leaks test concerns into production code. from a production standpoint, the rationale here I think is that we want to honor the configured Console.In ie:

// Read via Console.In so the loop honors the configured Console.InputEncoding
// (set to UTF-8 by Program.Main for stdio mode) and shares the SyncTextReader.

try
{
// Empty input immediately yields EOF (ReadLineAsync returns null).
Console.SetIn(new StringReader(string.Empty));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Console.SetIn is process-global, if we ever parallelize this test it could break other tests that read Console.In. Can instead extract the read source, and then tests can pass a StringReader directly.


Assert.AreEqual(1, lines.Length,
"Expected a single response line for shutdown request.");
StringAssert.Contains(lines[0], "\"id\":1");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: parsing this line as JSON and then asserting on jsonrpc/id/result.ok would align more closely with the other mcp tests while being a bit more robust.

Copy link
Copy Markdown
Contributor

@aaronburtle aaronburtle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, just a couple nits.

using StreamReader reader = new(stdin, utf8NoBom);
// Read through Console.In so tests can inject stdin and the process
// still follows the configured console input encoding in stdio mode.
TextReader reader = Console.In;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we no longer need utf8NoBom for stdin? Why was it needed before and what changed?

Copy link
Copy Markdown
Collaborator

@Aniruddh25 Aniruddh25 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving with some questions to avoid regression around UTFNoBOM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working cri Customer Reported issue

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

[Bug] --mcp-stdio mode crashes (exit 4294967295) when stdin receives EOF

4 participants