MCP -32700 Parse error, stdout contamination, stdio and Tool list failed troubleshooting - XBSTACK

How to Fix MCP -32700 Parse Errors: Troubleshooting stdout, stdio, and "Tool list failed"

Release Date
2026-06-04
Reading Time
16分钟
Content Size
23,333 chars
MCP 协议
mcp-server
json-rpc
claude
cursor
troubleshooting
Laboratory Note

This article documents my real-world experiments in the lab. I believe that building your own digital assets with AI is the ultimate moat for developers.

Quick Answer

  • Diagnose MCP -32700 Parse error, Unexpected non-JSON line, Tool list failed, and spawn ENOENT with a 30-second stdout/stderr check for macOS, Linux, and Windows PowerShell, then verify JSON-RPC lifecycle and protocol negotiation.

Who Should Read This

  • Developers evaluating mcp / mcp-server / json-rpc / claude for production use.
  • Indie builders who need a practical implementation path instead of another generic concept article.
  • Readers comparing architecture trade-offs, risks, tooling boundaries and next actions.

Problem Solved

  • Why does it show 'Tool list failed' when refreshing the tool list in Cursor or Claude Desktop?
  • What is the physical root cause when the MCP Server issues a JSON-RPC parse error?
  • How to achieve physical separation of stdout and stderr in Node.js and Python code?
  • How to locate the local running logs of Cursor and Claude on systems like macOS?
  • How to quickly diagnose connection issues using command-line bare execution and manually constructing JSON-RPC messages?
  • What do MCP Unexpected non-JSON line, -32700, and -32600 mean respectively?
  • Why should HTTP 415, 401, or 502 errors be investigated at the HTTP layer instead of stdout?

Search performance review, updated July 22, 2026: Search Console currently has data through July 19. This page recorded 23 impressions and an average position of 23.3 in the current 28-day window. Only 26 of 28 metric dates have returned, so this update does not infer a new CTR. The page is not yet in the first two result pages; rather than splitting stdout pollution, Tool list failed, and spawn ENOENT into competing URLs, this page keeps the complete troubleshooting task together and strengthens the five-step order and 30-second validation command.

First, follow these 5 steps to troubleshoot

If the -32700 Parse error, Tool list failed, or MCP connection indicator turns red, do not change the business logic yet; check in the following order:

  1. Check stdout: In stdio mode, stdout may contain only protocol messages. Send all ordinary logs to stderr.
  2. Start with absolute paths: Specify node, python3, the script path, and the working directory as absolute paths so you can rule out spawn ENOENT first.
  3. Run the server directly: Start it once in a terminal and confirm that initialization produces no banners, warnings, or third-party library output.
  4. Check JSON-RPC messages: Make sure the jsonrpc, id, method, params, result, and error structures are valid and the strings have been correctly escaped.
  5. Check protocol version and capability negotiation: If the connection can start but the tool list fails, continue to verify the initialize response, capabilities, and client-supported protocol versions.

30-second diagnostic: separate stdout and stderr first

macOS / Linux:

/usr/local/bin/node /absolute/path/server.js \
  1>/tmp/mcp-stdout.log \
  2>/tmp/mcp-stderr.log

python3 - <<'PY'
import json
from pathlib import Path
for index, raw in enumerate(Path('/tmp/mcp-stdout.log').read_text(encoding='utf-8').splitlines(), 1):
    if not raw.strip():
        continue
    try:
        message = json.loads(raw)
    except json.JSONDecodeError as exc:
        raise SystemExit(f'line {index} is not JSON: {exc}: {raw[:160]!r}')
    if message.get('jsonrpc') != '2.0':
        raise SystemExit(f'line {index} is not JSON-RPC 2.0: {raw[:160]!r}')
print('stdout contains only JSON-RPC 2.0 messages')
PY

Windows PowerShell:

& "C:\Program Files\nodejs\node.exe" "C:\absolute\path\server.js" `
  1> "$env:TEMP\mcp-stdout.log" `
  2> "$env:TEMP\mcp-stderr.log"

Get-Content "$env:TEMP\mcp-stdout.log" | ForEach-Object {
  if ($_ -and -not ($_ | Test-Json -ErrorAction SilentlyContinue)) {
    throw "stdout contains a non-JSON line: $_"
  }
}

Interpret the result this way:

  • a banner, ordinary log, warning, or stack trace in stdout means transport contamination;
  • valid JSON without jsonrpc: "2.0" means a JSON-RPC structure problem;
  • empty stdout while the process keeps running may simply mean the server is waiting for initialize, so inspect the client log and startup path next.

Do not mix the three types of error reports together

PhenomenonPriority inspection
-32700 Parse errorstdout contamination, message truncation, illegal JSON, encoding, and line breaks
Tool list failedWhether the server has completed initialization, whether it declares tools capability, and whether tools/list returns a valid structure
spawn ENOENTExecutable file path, script path, PATH, working directory, and file permissions

Symptoms, verification commands, and passing criteria

SymptomsWhat to do firstPassed the standard
The server reports -32700 as soon as it startsStart it directly and capture stdout and stderrstdout contains only complete JSON-RPC messages; ordinary logs appear only in stderr
Tool list failedFirst complete initialize and notifications/initialized, then send tools/listThe initialization response includes the negotiated protocol version and tools capability
spawn ENOENTUse which node / which python3, then put the resulting absolute path in the client configurationThe client can start the process, and the log no longer reports that the executable was not found
Disconnect after a few seconds of connectionCheck the earliest stderr and client logs, not just the last EPIPEFind the first parsing error, failure to capture exception, or process exit cause
Only the long-text tool failsCheck the serialized single-line JSON, encoding, and message sizeEach stdio message is newline-delimited, with no unescaped line breaks inside a message

This article addresses only JSON-RPC parsing over stdio and local startup issues. For remote deployment, see MCP Streamable HTTP in Practice; for public-network authentication, see MCP OAuth Authentication in Practice; for permissions and auditing, see MCP Security Best Practices.

Use MCP Inspector and staged imports to isolate hidden output

If a direct terminal run produces no obvious logs but the client still reports a parsing failure, take these additional steps:

  1. Use MCP Inspector to start the same server and observe whether non-protocol text is mixed during initialize, tools/list, and tool calls.
  2. Delay third-party imports one by one. Some Python or Node.js dependencies print banners, deprecation warnings, or version prompts during import, require, or initialization.
  3. Don’t just replace print and console.log; also check the default transport of sys.stdout.write, process.stdout.write, and third-party logging frameworks.

The old stdio contamination page has been merged into this page, and subsequent local connections, stdout contamination, and -32700 errors are all maintained here.

Practical Review Checklist

When troubleshooting a parse error, do not start by blaming the model. Check the following in order:

  • Run the MCP Server directly and confirm that stdout contains only valid JSON-RPC messages.
  • Send every diagnostic log to stderr or a dedicated log file.
  • Check whether Cursor or Claude starts the child process with an incomplete PATH.
  • Test stdin/stdout with the smallest valid JSON-RPC initialization sequence.
  • Check whether long text, embedded newlines, or large Resources are causing truncation or buffering problems.

Define the Problem Boundary with the Official Specifications First

The MCP stdio transport rules are explicit: the server reads JSON-RPC messages from stdin and returns JSON-RPC messages through stdout; messages use UTF-8, are newline-delimited, and must not contain unescaped newlines inside a message. Logs may be written to stderr, but stdout must not contain any non-MCP output. After the connection starts, the client must complete initialize, negotiate the protocol version and capabilities, send notifications/initialized, and only then proceed to normal operations such as tools/list.

The relevant primary sources are the MCP Transports specification, the MCP Lifecycle specification, and the JSON-RPC 2.0 error object specification.

Error Symptoms, Protocol Layer, and First Check

Error symptomLayerCommon causeFirst check
Unexpected non-JSON linestdio framingconsole.log, print, a banner, or a dependency warning entered stdoutCapture stdout and stderr separately and locate the first non-JSON line
-32700 Parse errorJSON parsingThe received text is not valid JSON, or a message was truncated or contains control charactersParse every stdout line as JSON
-32600 Invalid RequestJSON-RPC structureThe JSON parses, but required fields such as jsonrpc or method are missingCompare the payload with the JSON-RPC Request and Response structures
Tool list failedMCP lifecycle or tool capabilityInitialization is incomplete, the tools capability is missing, or the tools/list response is invalidVerify initialize and notifications/initialized first
spawn ENOENTProcess startupThe client PATH is incomplete, or the command or script path is wrongUse absolute paths for Node.js, Python, and the server script
HTTP 401, 403, 415, or 502Streamable HTTPAuthentication, Content-Type, proxy, route, or upstream-service failureInspect HTTP headers, gateway logs, and server logs instead of stdout

These errors belong to different layers. -32700 means the receiver cannot parse the incoming text as JSON. -32600 means the text is valid JSON but not a valid JSON-RPC Request. Tool list failed is only a higher-level client symptom whose root cause may be process startup, initialization, capability negotiation, or the tool-list response.

Why console.log and print Can Break an MCP Handshake Instantly

Writing any non-JSON-RPC content to stdout breaks message framing and causes the client to read incomplete or contaminated packets. Full-stack developers new to MCP often carry over habits from traditional Web APIs and command-line tools. In conventional web development, console.log or Python’s print is a convenient way to report the connected database, the number of loaded configuration items, or current API usage. In an HTTP service, those logs go to the server console while the business response travels to the client through a network socket as an HTTP response body. Logs and application data occupy separate channels by default.

That separation disappears in MCP stdio mode. The command-line process’s stdout is effectively the protocol socket. In other words, your console.log becomes a packet sent to the client.

When you write console.log("Database connected successfully"), Node.js ultimately calls process.stdout.write. The success message enters the stdout pipe while the client is waiting for a JSON-RPC 2.0 message, so the client receives plain text instead.

The client parser effectively attempts something like JSON.parse("Database connected successfully"), which immediately throws a syntax error. Because this parsing happens in the client’s communication path, an uncaught failure tells the client that the subprocess channel is corrupted. To guard against malformed output or buffer overflow, the client may terminate the subprocess with SIGKILL.

Worse, contamination often comes not from your own log statements but from third-party dependencies. Some Node.js or Python libraries print messages to stdout as soon as they are required or imported. A database driver might emit a deprecation warning while inspecting the environment, an ORM might report a runtime performance warning, or a configuration loader might print a warning when it cannot find its default file. Even one such byte on stdout can break the current MCP handshake.

Under stdio, any unstructured text written to stdout is transport contamination. All non-protocol data—including logs, warnings, and stack traces—must instead go to stderr. stderr is also an output channel, but it is intended for diagnostics. A parent process such as Cursor or Claude treats stderr differently from stdout: it records the content as plain text in local logs or discards it, rather than feeding it to the JSON-RPC parser. Using stderr as the sole destination for application logs is therefore a hard requirement for a stable MCP process.

Minimal Reproduction Code: How to Explicitly Direct Application Logs to stderr

The stable approach is not to globally override process.stdout.write, console.log, or Python’s sys.stdout, but to have your application logs explicitly written to stderr from the start, and to disable banners or Console Handlers of third-party dependencies. Global hijacking may simultaneously intercept legitimate protocol output from the SDK, and should ultimately not be used as a patch.

The following Node.js example uses only console.error for diagnostic information and does not modify any global output stream:

// safe-mcp-server.js
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";

// 初始化 Server
const server = new Server(
  {
    name: "safe-demo-server",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// 注册工具列表
server.setRequestHandler(ListToolsRequestSchema, async () => {
  console.error("收到客户端的 list tools 请求"); // 使用 console.error 打印日志
  return {
    tools: [
      {
        name: "calculate_future_value",
        description: "计算复利未来价值",
        inputSchema: {
          type: "object",
          properties: {
            principal: { type: "number", description: "本金" },
            rate: { type: "number", description: "年化收益率" },
            years: { type: "number", description: "投资年限" },
          },
          required: ["principal", "rate", "years"],
        },
      },
    ],
  };
});

// 注册工具执行
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;
  console.error(`开始执行工具 ${name},参数为:`, args);

  if (name === "calculate_future_value") {
    const { principal, rate, years } = args;
    const result = principal * Math.pow(1 + rate, years);
    return {
      content: [
        {
          type: "text",
          text: `经过 ${years} 年的复利增值,本金 ${principal} 将增长至 ${result.toFixed(2)}`,
        },
      ],
    };
  }

  throw new Error(`未知的工具方法: ${name}`);
});

// 启动服务,绑定到 stdio transport
async function run() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP Server 已成功通过安全 Stdio 通道启动并监听");
}

run().catch((error) => {
  console.error("Server 发生严重崩溃:", error);
  process.exit(1);
});

Do not execute sys.stdout = sys.stderr on the Python side after SDK initialization. FastMCP’s stdio transport must retain stdout for legitimate responses. A safer approach is to send application logs explicitly through logging.StreamHandler(sys.stderr) and disable third-party Console Handlers.

# safe_mcp_server.py
import logging
import sys
from mcp.server.fastmcp import FastMCP

logger = logging.getLogger("safe-mcp-server")
logger.setLevel(logging.INFO)
logger.handlers.clear()
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
logger.addHandler(handler)
logger.propagate = False

mcp = FastMCP("Safe Python MCP Server")

@mcp.tool()
def calculate_dca_returns(monthly_investment: float, annual_rate: float, years: int) -> str:
    logger.info(
        "calculate_dca_returns monthly=%s rate=%s years=%s",
        monthly_investment,
        annual_rate,
        years,
    )
    monthly_rate = annual_rate / 12
    months = years * 12
    total_value = 0.0
    for _ in range(months):
        total_value = (total_value + monthly_investment) * (1 + monthly_rate)
    return f"期末总资产: {total_value:.2f}"

if __name__ == "__main__":
    mcp.run(transport="stdio")

If a dependency forcibly prints to stdout during the import phase, prioritize disabling its banner or isolating that dependency into a separate subprocess. Do not override process.stdout.write or Python’s global stdout as a “catch-all,” because this could also truncate legitimate SDK protocol outputs.

Copyable stdout / stderr Verification Commands

Capture the two streams separately. A server that is waiting for initialize and leaves stdout empty is behaving normally.

# Node.js
/usr/local/bin/node /absolute/path/server.js \
  1>/tmp/mcp-stdout.log \
  2>/tmp/mcp-stderr.log

# Python
/usr/bin/python3 /absolute/path/server.py \
  1>/tmp/mcp-stdout.log \
  2>/tmp/mcp-stderr.log

Then validate every non-empty stdout line as JSON-RPC 2.0:

# validate_mcp_stdout.py
import json
from pathlib import Path

path = Path("/tmp/mcp-stdout.log")
for line_no, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
    if not raw.strip():
        continue
    try:
        message = json.loads(raw)
    except json.JSONDecodeError as exc:
        raise SystemExit(f"line {line_no} is not JSON: {exc}: {raw[:160]!r}")
    if message.get("jsonrpc") != "2.0":
        raise SystemExit(f"line {line_no} is not JSON-RPC 2.0: {raw[:160]!r}")

print("stdout contains only JSON-RPC 2.0 messages")

Run it with:

python3 validate_mcp_stdout.py

If the first failing line is a banner, database connection notice, or DeprecationWarning, fix stdout contamination first. If every line parses, continue with lifecycle, capability, and tool-response checks.

Troubleshooting Workflow: How to Find and Capture Low-Level Errors in Claude and Cursor

When an IDE client such as Claude Desktop or Cursor cannot connect, the only reliable diagnostic path is to run the server directly in a terminal, capture stderr, and inspect the client’s local logs. MCP integrations in these graphical clients run in the background, so their process and pipe interactions are largely opaque. When the tool list fails, many developers see only a red warning indicator and cannot inspect the underlying error. The following standard workflow provides a comprehensive local audit and captures the relevant client logs.

Step 1: Isolate the server in a terminal. Do not rush to add your Server in the configuration file; first, run it locally in isolation in your terminal. Open your iTerm or terminal and execute your startup command. Normally, the service is waiting for the client’s initialize handshake request, and the console should have no normal text output and remain running. If your program prints any initialization text to the console immediately on startup, promptly locate the code line producing this output and either remove it or redirect it to stderr.

Step 2: Test the minimum handshake in MCP lifecycle order. Do not send tools/list immediately. Initialization must be the first interaction between the client and Server; after the Server returns the protocol version and capabilities, the client then sends notifications/initialized, and only then can normal operations begin.

First, send the initialization request:

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"manual-debug-client","version":"1.0.0"}}}

After confirming that the server returns valid result.protocolVersion, serverInfo, and capabilities, send these messages in order:

{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}

Each must be an independent, single-line JSON-RPC message. A healthy server will return a list of valid tools after initialization is complete; If banners, warnings, or regular logs are mixed in during the initialization phase, stdout contamination should be addressed first, rather than continuing to adjust business tools. For actual debugging, MCP Inspector is preferred because it correctly handles lifecycle and message order.

Step 3: Search the client’s local log files comprehensively. If the terminal tests go smoothly but errors still occur after mounting to the client, you must retrieve the runtime logs written on the client’s local disk.

For Claude Desktop: On macOS, open the terminal and run the following command to view the logs: cat ~/Library/Logs/Claude/mcp.log On Windows, this log is usually stored at: %APPDATA%\Claude\logs\mcp.log On Linux, this log file is usually stored at: ~/.config/Claude/logs/mcp.log Claude records the command-line arguments for each MCP process-start attempt and every line emitted by the child process in this file. If child-process output violates JSON-RPC, the log reports explicit messages such as Unexpected non-JSON line.

For Cursor: Cursor is developed based on the Electron architecture, which means it runs an instance of the Chromium browser internally. We can fully debug Cursor’s backend communication just like we would a web page. Open Cursor, click Help in the top menu, and find Toggle Developer Tools. This will pop up a Chrome DevTools panel. Switch to the Console tab. When you refresh the MCP Server in the Cursor options, all pipe read/write errors and process exception logs will be printed in the console as a red Error. You can filter the keyword MCP in the console to see if there is an error stack where the child process exited with code or the stdio connection is closed.

Additionally, Cursor’s extended host processes also log the work. On macOS, log paths are usually as follows: ~/Library/Application Support/Cursor/logs On Windows, the log path is: %APPDATA%\Cursor\logs You can use the terminal’s lookup tool to search these log directories for the latest log files, which usually contain stdio capture data from when Cursor background subprocesses start.

Step 4: Capture messages with protocol-aware tools. Do not plug the tee directly behind the stdout of the MCP Server. Ordinary shell pipes do not understand the MCP lifecycle, request cancellation, and bidirectional message boundaries, and incorrect proxy scripts may further pollute stdout due to additional outputs. Prioritize using MCP Inspector; If you really want to perform byte-level scraping, the proxy process must meet three conditions:

  1. Stdin and stdout forward bidirectionally as is, without modifying or buffering merged messages.
  2. All diagnostic information should only be written as stderr or in separate files.
  3. First, verify in automated testing that every message before and after the proxy is still valid single-line JSON-RPC.

A simpler approach is to write the server’s business logs to a separate file while saving the client’s MCP logs. Aligning the timestamps on both sides is usually sufficient to pinpoint the first polluted byte or the earliest process anomaly.

Repair Plan: Fix the Root Cause Instead of Hijacking stdout Globally

  1. Preserve the first evidence: Save stdout, stderr, and client logs separately, then align timestamps and find the earliest failure. EPIPE is often only a downstream effect of an earlier disconnect.
  2. Clean up the logging channel: Send application logs through console.error, logging.StreamHandler(sys.stderr), or a file transport. Disable third-party banners and default Console Handlers.
  3. Let the SDK own protocol output: Do not manually concatenate, batch, or globally redirect MCP messages. Keep UTF-8 encoding, single-line JSON-RPC messages, and newline framing intact.
  4. Test the lifecycle automatically: Cover initialize, notifications/initialized, and tools/list in order, and assert the negotiated protocolVersion, serverInfo, and tools capability.
  5. Make the startup environment explicit: Use absolute executable paths, absolute script paths, and a defined working directory. When the project depends on nvm, pyenv, or a virtual environment, configure PATH and required environment variables explicitly.

The goal is not to promise that a Parse error can never happen. The goal is to classify every failure reproducibly into process startup, stdio framing, JSON parsing, JSON-RPC structure, MCP lifecycle, or tool execution.


Common Pitfalls / Common Errors (Error Logs)

Understanding standard JSON-RPC error codes and the corresponding stack traces in stderr helps pinpoint the failing component quickly. The table below lists five common low-level errors and their root causes.

Error Code (Code)Error messageProtocol Definition (Specification)Physical manifestations and common root causesTroubleshooting physical operations
-32700Parse errorParsing error: The server receives an invalid JSON packetstdout is contaminated by banner/warning messages from console.log, print, or third-party dependencies, or message encoding, framing, and line breaks are invalidDelete the standard stdout output and explicitly write stderr to the application log; Use the SDK to handle protocol serialization, not override global stdout
-32600Invalid RequestInvalid request: the JSON structure does not comply with JSON-RPC 2.0Missing jsonrpc: "2.0", a missing method field, or a response containing both result and errorUse a Schema Validator to validate message structure and allowlist filtering before serializing the payload
-32601Method not foundMethod not found: the method is not declared or supported by the serverThe client misspelled the tool name, or the server did not declare the tools capability during initializationVerify the name mapping between the client and server tool lists, and inspect the schema returned by ListToolsRequestSchema
-32602Invalid paramsInvalid parameters: the method-call parameters do not match the declarationThe client argument types, such as string versus number, or required fields do not match the tool’s inputSchemaCompare the arguments strictly with the properties in inputSchema; if schema validation fails, record the payload and stack trace explicitly to stderr
-32603Internal errorInternal error: the MCP server crashed while executing the toolBusiness code throws an uncaught exception, such as a database connection failure or insufficient file permissions, causing the Node.js/Python process to errorWrap the top level of handlers in try...catch, write the stack with console.error, and return a compliant error payload
  1. Client outputs an Unexpected non-JSON line error:
[json-rpc] Unexpected non-JSON line: "DB Connection established..."
[json-rpc] Unexpected non-JSON line: "DeprecationWarning: Big-endian support is deprecated"

The root cause is that the server prints a message such as a database-connection notice, or a third-party library emits a deprecation warning, before the server writes a valid message to stdout. The client expects JSON but receives plain text, so its parser fails immediately.

  1. JSON-RPC -32700 Parse error:
{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}

This error is returned to you by the server from the client (such as Claude), or by the server to the client. It indicates that during the streaming process, one side received the data but failed when trying to parse the data using JSON.parse. This is usually because the JSON structure was truncated (the buffer was not fully flushed), or the transmitted content contained unrecognized control characters, garbled text, or non-UTF-8 characters.

  1. The IDE client reports a spawn ENOENT error:
Failed to run command: spawn node ENOENT
Failed to run command: spawn python3 ENOENT

This error indicates that Cursor or Claude tried to start your MCP process in the background, but since the executable files for node or python3 could not be found in its PATH environment variable, the process did not start at all. The IDE throws this physical exception, usually accompanied by the connection status turning red immediately.

  1. Broken-pipe write EPIPE error:
Error: write EPIPE at AfterWriteReq.oncomplete (node:internal/stream_base_commons:90:16)

This occurs when your Node.js process tries to write a message to process.stdout and finds that the reading process on the other end (Claude / Cursor) has already exited, or has proactively closed the standard input stream due to a previous parse error. This is a typical cascading error, indicating that the root cause lies in an earlier communication anomaly.

  1. A noncompliant message format causes -32600 Invalid Request:
{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request: missing jsonrpc version"}, "id": 1}

This indicates that the received message can be successfully parsed as JSON, but the JSON object lacks a key protocol identifier internally. For example, you misspelled jsonrpc (written as json-rpc), or omitted the method field in the request message, or the response contains both result and error fields.


Stdio and Streamable HTTP: error detection boundaries are different

Currently, MCP mainly uses two types of transmission: local stdio and remote Streamable HTTP. The stdout contamination issue discussed in this article only directly affects STDIO; Remote HTTP services are more common with authentication, Content-Type, session, proxy, and network layer errors.

Inspection dimensionStdioStreamable HTTP
Connection MethodThe host starts the local subprocess, communicating via stdin/stdoutThe client sends messages via HTTP POST, with optional SSE streaming return
The most sensitive issuestdout mixes in regular logs, PATH, working directory, process exitURL, authentication, request header, reverse proxy, session, and timeout
Log locationstderr or independent log fileApplication logs, gateway logs, and request tracking
Common error reports-32700, spawn ENOENT, EPIPE401, 403, 404, 415, 502, timeout
Applicable scenariosLocal desktop client and development toolsRemote sharing, team services, and cloud access

If the local stdio server is stable but the goal is cross-machine or multi-user sharing, stop patching around stdout and move on to MCP Streamable HTTP deployment and MCP OAuth authentication.


Frequently Asked Questions

The following answers cover common transport- and protocol-layer pitfalls encountered during daily development and edge-case failures.

Why does the MCP Server run without errors in my terminal while Cursor keeps reporting “Tool list failed”?

This is mainly due to differences in environmental variables in the execution environment. When you run in the terminal, you use the complete environment variables in your current shell. For example, your Node.js is installed via nvm, and your PATH variable contains the complete node executable path. As a desktop client, Cursor’s PATH variable when forking child processes in the background may be the system’s default minimalist PATH, causing it to fail to find your node executable and thus throwing spawn ENOENT. Additionally, some servers do not print any errors when they do not receive standard input, but once the Cursor shakes hands, they crash during initialization due to receiving incorrect packets. You should first write all executable command paths as absolute paths in the Cursor configuration file.

First, check whether the dependency supports closing banners, silent mode, or custom loggers, and clearly point its log handler to stderr. Do not override process.stdout.write, console.log, or execute sys.stdout = sys.stderr, because the MCP SDK also requires stdout to send legitimate protocol messages, and global redirection may cause the server to lose its response channel entirely. If the dependency cannot close the output, it should be isolated to another child process, which the MCP Server calls via controlled IPC and only receives structured results.

Why does the client still report a -32700 Parse error after I move every log to console.error?

This is usually because your message content was truncated, or your JSON-RPC message contains invalid characters. For example, if your output JSON string contains unescaped line breaks (for example, placing a long text containing a newline as a string in params), the stdio transport framework will mistakenly treat this line break as a message separator when reading line by line, splitting a complete message into two lines. Parsing the first line will trigger a parse error due to incomplete JSON structure. You should ensure that all text you put into JSON messages has been properly escaped (for example, using JSON.stringify will automatically handle line break escaping).

How can I record MCP Server production logs safely for later troubleshooting?

The safest approach is to use a logging system such as Winston (Node.js) or Loguru (Python), configure a File Transport, and append every log entry to a dedicated file on local disk. This fully separates logs from protocol data. Do not write logs directly to the console for convenience. You can also send them to stderr because Claude Desktop and Cursor capture child-process stderr in their internal logs. Because client logs can be overwritten quickly, however, a separate local log file remains more reliable.

Keep Reading

Explore more advanced Model Context Protocol techniques and architecture patterns for building a more resilient local AI agent network.

Topic path / MCP

Continue from protocol details to production MCP governance

The MCP hub connects protocol fundamentals, transports, authentication, security, JSON-RPC debugging and production deployment without splitting the search intent across isolated guides.

Next Reading

View Hub →
Xiaobai

Xiaobai

Full-Stack AI Engineer

Xiaobai, a full-stack AI engineer building production Agent systems, product tools and independent software assets.

About Xiaobai & XBSTACK →

Liked this article?
Join the newsletter

Every issue condenses production AI engineering changes, real failures, reproducible experiments, useful tools and new XBSTACK assets. No generic news digest and no filler.

Comments