McpError: MCP error -32000: Connection closed

Node.js · updated 2026-11-11

Thrown by the MCP TypeScript SDK's Client when the transport closes while a request is still waiting: with stdio, that means the server process exited. Claude Code, Claude Desktop, Cursor and your own agents built on the SDK all report it the same way. SDK 1.x throws McpError with code -32000; the v2 client package (@modelcontextprotocol/client) throws SdkError: Connection closed with code CONNECTION_CLOSED. The error says nothing about why the server died. The cause is in the server: an exception at startup (a missing env var, a bad import) or an uncaught exception or rejection while a tool runs. The server printed it to its stderr before exiting.

McpError: MCP error -32000: Connection closed
    at McpError.fromError (node_modules/@modelcontextprotocol/sdk/dist/esm/types.js:2048:16)
    at Client._onclose (node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js:267:32)
    at _transport.onclose (node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js:227:18)
    at ChildProcess.<anonymous> (node_modules/@modelcontextprotocol/sdk/dist/esm/client/stdio.js:85:31)
    at ChildProcess.emit (node:events:519:28)
  code: -32000

Quick fix

Find out why the server process exited. Run the exact command from your MCP config in a terminal and watch stderr: a startup crash shows up at once. If it only dies during a tool call, the server has an uncaught exception or unhandled rejection; catch it in the tool, or report it before the process exits so you get the stack trace instead of "Connection closed".

  1. Copy the command, args and env from the client's MCP config and run them yourself; a server that exits right away prints the reason.
  2. Check the client's MCP logs for the server's stderr (Claude Code: run claude --debug=mcp, or check /mcp; Claude Desktop: the mcp*.log files in ~/Library/Logs/Claude on macOS).
  3. Look for env vars the client doesn't pass: a stdio server gets only the env in its config, not your shell's.
  4. If it dies mid-call, wrap the tool's work in try/catch and return an error result instead of letting a timer or callback throw.
  5. Never write logs to stdout in a stdio server; use console.error, which goes to stderr.
typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

// Fail with a clear message on stderr instead of an unexplained exit.
if (!process.env.DATABASE_URL) {
  console.error('DATABASE_URL is not set. Add it to this server\'s "env" in the MCP config.');
  process.exit(1);
}

const server = new McpServer({ name: 'orders', version: '1.0.0' });

server.registerTool('order_total', { description: 'Total for an order', inputSchema: {} }, async () => {
  try {
    const total = await loadTotal();
    return { content: [{ type: 'text', text: String(total) }] };
  } catch (err) {
    // An error result keeps the server alive; the client shows the message.
    return { isError: true, content: [{ type: 'text', text: String(err) }] };
  }
});

await server.connect(new StdioServerTransport());
Fix it with your coding agent

Paste this into Claude Code, Cursor or Codex in the project that throws it. The agent reads this guide in plain text and applies the fix that matches your stack.

prompttext
Read https://runheed.com/errors/mcp-error-32000-connection-closed.md and fix this error in this project.

Why it happens

The server crashes at startup

A missing env var, a failed import or a top-level await that rejects. The client connects, the process exits, and every request fails with -32000.

An uncaught exception during a tool call

A throw inside a setTimeout, an event handler or an un-awaited promise escapes the SDK, and Node exits. A throw directly inside the tool handler does not: the SDK turns that into a result with isError: true.

Missing env in the client config

Clients start stdio servers with only the env listed in the config, so a key that works in your terminal is undefined there.

The process is killed

Out of memory, a process.exit() in a dependency, or the server finishing its main module without connecting a transport.

Other fixes

  • A wrong command fails differently: spawn npx ENOENT means the client could not start the process at all. Use the full path to node or npx.
  • console.log on stdout in a stdio server puts non-JSON text in the protocol stream. With the current SDK the client logs a JSON parse error and keeps going, but it is still the first thing to remove.
  • For remote (Streamable HTTP) servers the same code means the HTTP connection dropped; check the server's logs and any proxy timeouts.

Same error, other wording

  • MCP error -32000: Connection closed
  • McpError: MCP error -32000: Connection closed (while calling tools/call)
  • SdkError: Connection closed (code CONNECTION_CLOSED, SDK v2)
  • Error: spawn npx ENOENT

Let your coding agent catch the next one

Runheed records this error in production with the stack trace, the breadcrumbs before it and, when you set it, the agent session that shipped the code, and serves it to Claude Code, Cursor or any MCP client. With it connected, you can ask:

> Find the crash in my MCP server that makes clients report "MCP error -32000: Connection closed", show me the stack trace, and fix it so the tool returns an error result instead of killing the process.

Setup for Node.js and Express or Vercel Functions, then have your agent fix and resolve it.

Building an MCP server? Track the errors your tools raise, in TypeScript or Python, so a failing tool shows up with its line instead of only in your users' client logs.

Related errors

More in MCP servers and clients

References: MCP TypeScript SDK: stdio client transport source, MCP docs: debugging servers (logging to stderr, client logs), Claude Code docs: MCP servers, Claude Code docs: CLI reference (--debug)

Spotted a mistake? Email hello@greatwork.company. About Runheed.