spring-projects / spring-projects/spring-ai

StdIO transport doesn't detect MCP server process exit and conceals exit code

Open
#5,982 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

status: waiting-for-triage
Dominant language
Java
Stars
9.5k
Forks
2.9k
Avg merge
1d 7h
Merged PRs (30d)
6

Description

Bug description
If MCP server process exits - this is not detected by an StdioClientTransport that launches said process, or any other code. The initialization of MCP server just times out waiting for stdio response from a process that's no longer running

Environment
Spring AI 2.0.0-M5, mcp-core-2.0.0-M2.jar, macOS Tahoe 26.3.1

Steps to reproduce
Configured chrome-devtools MCP server

application.yaml:

spring:
  ai:
    mcp:
      client:
        stdio:
          servers-configuration: classpath:mcp-servers.json

mcp-servers.json:

{
	"mcpServers": {
		"chrome-devtools": {
			"command": "/opt/homebrew/bin/npx",
			"args": [
				"chrome-devtools-mcp@latest",
				"--autoConnect"
			],
			"env":{
				"PATH": "/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin"
			}
		}
	}
}

Without the PATH variable explicitly listing paths, the PATH provided by Spring AI is not sufficient for Node/NPX to launch, the command exits with exit code 127 - which is not detected by Spring AI at all, instead initialize command is sent and response is awaited for default 20 seconds

Expected behavior
In case process exits, there should be no wait for response from it, instead exception should be thrown logging exit code of the process

Minimal Complete Reproducible example
Use above files with this main method in @SpringBootApplication

    public static void main(String[] args) {
        List<McpSyncClient> clients = SpringApplication.run(AiagorcApplication.class, args)
                .getBean("mcpSyncClients", List.class);
        System.out.println("Clients: " + clients.get(0).listTools());
    }

Note: if process exits immediately or quickly, supposedly this can be detected right away in the StdioClientTransport:


			ProcessBuilder processBuilder = this.getProcessBuilder();
			processBuilder.command(fullCommand);
			processBuilder.environment().putAll(params.getEnv());

			// Start the process
			try {
				this.process = processBuilder.start();
			}
			catch (IOException e) {
				throw new RuntimeException("Failed to start process with command: " + fullCommand, e);
			}

			// Validate process streams
			if (this.process.getInputStream() == null || process.getOutputStream() == null) {
				this.process.destroy();
				throw new RuntimeException("Process input or output stream is null");
			}

Debug has shown here the this.process has PID and exit code, but no IOException is thrown - because process ran, it just exited. Alternatively, the input/output streams of exited process are probably closed - and this can be detected in Lambdas created in startInboundProcessing/startOutboundProcessing methods of StdioClientTransport

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with StdioClientTransport, focusing on its startInboundProcessing and startOutboundProcessing methods and the process started by getProcessBuilder(). Reproduce the failing configuration with the documented PATH and observe the process exit. Done means an early exception reports the process exit code instead of waiting for the initialization timeout.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
65/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.