Skip to content

StdioTransport: Tool handler not invoked, client times out (Node.js subprocess on Linux) #576

Open
@Postmanplayx

Description

@Postmanplayx

Describe the bug

When an McpServer using StdioServerTransport is run as a Node.js subprocess (spawned by an McpClient using StdioClientTransport), the server successfully initializes, registers tools, and the client connects. However, when the client calls a registered tool using client.callTool(), the server-side tool handler is never invoked (no logs from the handler appear), and the client eventually times out with an McpError -32001: Request timed out.

Interestingly, if the client sends a JSON-RPC request for a non-existent method (e.g., trying various "getCapabilities" methods), the server does respond correctly with a "Method not found" JSON-RPC error. This indicates that basic STDIN/STDOUT communication for request/response is established. The issue seems specific to the tool call dispatching mechanism for callTool requests, as the server does not enter the registered tool handler.

To Reproduce

Steps to reproduce the behavior:

  1. Create a minimal MCP server (my-mcp-server.js) as provided in the "Code Snippets" section below.
  2. Create a minimal MCP client (client-test.js) as provided in the "Code Snippets" section below, which spawns the server script as a subprocess.
  3. Place both files in the same directory with a package.json (details also in "Code Snippets").
  4. Run npm install to install dependencies.
  5. Run node client-test.js.
  6. Observe the logs from both client and server.

Expected behavior

The server-side tool handler (async (params) => { ... } for "simple-ping" in my-mcp-server.js) should be invoked, and its console.error logs (e.g., [Server-SIMPLIFIED-HANDLER] "simple-ping" called...) should appear. The client should receive a successful JSON-RPC response from the tool, not a timeout.

Logs

The observed log output when running node client-test.js is as follows:

[Client] Spawning server: node /home/slavek/Projects/context7-mcp-server/my-mcp-server.js
[Client] Connecting to server...
-----------------------------------------------------
[Server-SIMPLIFIED] Initializing simplified server (tool with no args)...
[Server-SIMPLIFIED] McpServer instance created.
[Server-SIMPLIFIED] Tool "simple-ping" registered.
[Server-SIMPLIFIED] StdioServerTransport instance created.
[Server-SIMPLIFIED] Simplified MCP Server connected and ready.
[Client] Client connected.
[Client] Calling tool 'simple-ping' (no arguments)...
// <-- Hangs here for 60 seconds, then client times out -->
[Client] Main error during client execution: McpError: MCP error -32001: Request timed out
    at Timeout.timeoutHandler (file:///home/slavek/Projects/context7-mcp-server/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js:295:49)
    at listOnTimeout (node:internal/timers:588:17)
    at process.processTimers (node:internal/timers:523:7)
[Client] Error data: { timeout: 60000 }
[Client] Closing connection...
[Client] Client disconnected.

Code Snippets

my-mcp-server.js (Simplified Server):

// my-mcp-server.js (simplified, tool with no args)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

async function initializeSimplifiedServer() {
  console.error("-----------------------------------------------------");
  console.error("[Server-SIMPLIFIED] Initializing simplified server (tool with no args)...");

  const server = new McpServer({
    name: "SimplifiedNoArgsMcpServer",
    version: "1.0.0",
    capabilities: { tools: { "simple-ping": { description: "A simple ping tool." } } }
  });
  console.error("[Server-SIMPLIFIED] McpServer instance created.");

  server.tool(
    "simple-ping",
    z.undefined(),
    async (params) => {
      // This log is crucial and is never reached
      console.error(`[Server-SIMPLIFIED-HANDLER] "simple-ping" called. Params received:`, JSON.stringify(params)); 
      return { content: [{ type: 'text', text: "Pong!" }] };
    }
  );
  console.error('[Server-SIMPLIFIED] Tool "simple-ping" registered.');

  const transport = new StdioServerTransport();
  console.error("[Server-SIMPLIFIED] StdioServerTransport instance created.");
  try {
    await server.connect(transport);
    console.error("[Server-SIMPLIFIED] Simplified MCP Server connected and ready.");
  } catch (error) {
    console.error("[Server-SIMPLIFIED-ERROR] Failed to connect server to transport:", error);
    process.exit(1);
  }
}
initializeSimplifiedServer().catch(err => {
  console.error("[Server-SIMPLIFIED-FATAL] Failed to initialize simplified server:", err);
  process.exit(1);
});

client-test.js (Simplified Client):

// client-test.js (calling "simple-ping" with no args)
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

async function runClient() {
  const serverScriptPath = path.resolve(__dirname, 'my-mcp-server.js'); 
  console.log(`[Client] Spawning server: node ${serverScriptPath}`);
  const transport = new StdioClientTransport({ command: "node", args: [serverScriptPath] });
  const client = new Client({ name: "my-test-client-noargs", version: "1.0.0" });

  try {
    console.log("[Client] Connecting to server...");
    await client.connect(transport);
    console.log("[Client] Client connected.");
    
    console.log("[Client] Calling tool 'simple-ping' (no arguments)...");
    const result = await client.callTool("simple-ping"); 
    console.log("[Client] Result from 'simple-ping':", JSON.stringify(result, null, 2));
  } catch (error) {
    console.error("[Client] Main error during client execution:", error);
    if (error.data) console.error("[Client] Error data:", error.data);
  } finally {
    if (client.isConnected) {
      console.log("[Client] Closing connection...");
      await client.close(); 
      console.log("[Client] Client disconnected.");
    }
  }
}
runClient();

package.json (relevant parts):

{
  "type": "module",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.9.0", 
    "zod": "^3.23.8" 
  }
}

Note: The issue was also reproduced with @modelcontextprotocol/sdk@^1.12.1 yielding the same timeout result.

Environment:

  • @modelcontextprotocol/sdk version(s): 1.12.1, 1.9.0 (behavior is identical for both)
  • Node.js version: v22.15.0
  • OS: Linux Ubuntu 24.04.2 LTS (Noble Numbat)
  • NPM version: 10.9.2

Additional context

This issue appears highly related to, or an instance of, what might be described in Issue #509 ("Tool Handlers Not Invoked with StdioServerTransport on macOS"), although this is observed on Linux. The core problem is that the registered tool handler on the server is not being called when a callTool request is made by the client via StdioClientTransport managing a subprocess. The server does respond to requests for non-existent JSON-RPC methods (e.g., various "getCapabilities" attempts) with a "Method not found" error, suggesting basic STDIN/STDOUT communication for request/response is established at some level. However, the specific dispatching to the tool handler for callTool seems to fail silently from the server's perspective, leading to a client timeout.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions