Skip to content

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

@mvmn

Description

@mvmn

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions