XBSTACK XBSTACK
Xiaobai

Xiaobai

Developer · Builder

Building AI engineering systems, developer tools and long-term digital assets at XBSTACK.

About Xiaobai & XBSTACK →
How to Fix MCP -32700 Parse Errors: Troubleshooting stdout, stdio, and "Tool list failed": MODEL CONTEXT PROTOCOL article cover

MCP -32700 Parse Error: stdout Pollution, Tool List Failed, and Version Checks

Fix MCP -32700 Parse Error by separating stdout/stderr, malformed JSON, startup failures, SDK v2 migration, and legacy 2025 versus stateless 2026-07-28 lifecycle issues.

Published · 2026-06-0416 min readXBSTACK
#mcp#mcp-server#json-rpc#claude#cursor#troubleshooting

Direct answer: MCP -32700 Parse Error means the received content could not be parsed as JSON. In stdio mode, separate stdout and stderr first because logs, banners, damaged JSON or encoding problems can contaminate the protocol stream—but do not assume every -32700 is stdout logging. TypeScript SDK v2 now skips non-JSON stdout lines, while malformed JSON, JSON-RPC schema failures, SDK/client version differences and startup errors can still fail.

Version boundary — 2026-08-14: MCP 2026-07-28 is published; Python SDK v2 is now the current stable line and implements this stateless core. The new lifecycle no longer depends on the legacy initialize / initialized session handshake. This article keeps the 2025-06-18 manual initialize example only for clients and SDKs that explicitly remain on the older protocol. If your client uses 2026-07-28, debug with the current lifecycle and matching SDK/Inspector instead of copying the legacy handshake.

If the connection works and you only need to organize Tools, Resources, and extension capabilities, read MCP Resources vs Tools vs Prompts vs Roots.

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 there is no ordinary stdout banner/warning and no import or dependency crash in stderr.
  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. Identify the protocol version before testing lifecycle: legacy 2025 clients should validate initialize/capabilities; 2026-07-28 clients should validate the modern protocol metadata, capability-discovery path, and actual method responses instead of forcing the old handshake.

30-second diagnostic: separate stdout and stderr first

Reusable assets: MCP -32700 Parse Error 30-second checklist and stdout JSON-RPC validator. For a runnable validator plus clean / polluted / invalid JSON-RPC fixtures, use xbstack/mcp-stdio-diagnostics.

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 can be normal: a legacy 2025 server may be waiting for initialize, while a 2026-07-28 server may simply be waiting for the next stateless request; inspect the client log, protocol version, 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 failedStartup plus the lifecycle for the negotiated protocol: legacy initialize/tools capability or the 2026-07-28 modern capability-discovery/method path; then verify the tool-list response shape
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 failedMatch the test to the negotiated protocol: legacy 2025 uses initialize / notifications/initialized; 2026-07-28 uses the current stateless capability-discovery/method flowThe target lifecycle succeeds and the tool response is valid for that protocol version
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 the current MCP Inspector to start the same server and observe whether non-protocol text is mixed during the lifecycle that matches your negotiated version—legacy initialize/tools calls or the 2026-07-28 stateless request flow.
  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 request flow for the negotiated protocol: legacy initialization for 2025 clients, or the current stateless flow for 2026-07-28.
  • 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 boundary remains explicit: the server reads protocol messages from stdin and returns protocol messages through stdout; application diagnostics belong on stderr or in files. What changed is the lifecycle. A client that still negotiates 2025-06-18 uses the legacy initializenotifications/initialized path before normal methods such as tools/list. MCP 2026-07-28 moved the core to a stateless model and no longer relies on that session handshake, so modern troubleshooting must match the actual negotiated protocol rather than treating the 2025 sequence as universal.

The relevant primary sources include the legacy MCP 2025-06-18 Transports specification, the MCP 2025-06-18 Lifecycle specification, the official MCP 2026-07-28 release note, the current SDK migration documentation, and the JSON-RPC 2.0 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 capabilityStartup/lifecycle does not match the negotiated protocol, capability discovery is incomplete, or the tool-list response is invalidVerify the target protocol flow first: legacy initialize for 2025 or the current stateless lifecycle for 2026-07-28
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 Are Still Unsafe on the MCP Protocol Stream

In MCP stdio mode, stdout is the protocol channel, so application logs should not be treated like ordinary terminal output. Full-stack developers often carry over habits from HTTP services, where console.log or Python print goes to the server console while the business response travels over a separate socket. Stdio does not give you that separation automatically.

The exact failure behavior now depends on the host and SDK. The TypeScript SDK v2 migration guide says ReadBuffer.readMessage() skips non-JSON stdout lines, which makes that reader more tolerant than older implementations. But that does not make stdout a safe logging channel: another host or proxy can still parse the stream strictly, JSON-looking log data can reach schema validation, and malformed/truncated messages can still produce -32700 or related failures.

When console.log("Database connected successfully") writes to stdout, the text enters the same pipe as protocol traffic. A strict client may attempt to parse it as JSON and fail; a tolerant v2 reader may skip it. Either way, mixing diagnostics with protocol data makes behavior host-dependent and makes incident evidence much harder to reason about.

Contamination also often comes from third-party dependencies that print during import or startup. A driver may emit a warning, an ORM may print a runtime notice, or a configuration library may output a banner before your server code has even reached the protocol loop.

The production rule therefore stays simple: use stdout only for protocol data generated by the SDK, and send diagnostics to stderr or a dedicated file. That remains the portable choice across Claude, Cursor, Inspector, SDK versions, wrappers, and future hosts.

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";

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

server.setRequestHandler(ListToolsRequestSchema, async () => {
  console.error("list tools");
  return {
    tools: [
      {
        name: "calculate_future_value",
        description: "Calculate the future value of a lump-sum investment",
        inputSchema: {
          type: "object",
          properties: {
            principal: { type: "number", description: "Initial investment amount" },
            rate: { type: "number", description: "Annual return rate as a decimal" },
            years: { type: "number", description: "Investment duration in years" },
          },
          required: ["principal", "rate", "years"],
        },
      },
    ],
  };
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;
  console.error(`Execute the tool ${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: `After ${years} years of compound growth, principal ${principal} will grow to ${result.toFixed(2)}`,
        },
      ],
    };
  }

  throw new Error(`unknowntool: ${name}`);
});

async function run() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP Server successapprovedsecurity 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. For a legacy 2025 client, a server can legitimately wait for initialize with empty stdout. Under MCP 2026-07-28, do not use “did it receive initialize?” as the health test; inspect the modern request metadata, capability-discovery/method flow, and whether protocol messages remain valid.

# 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 the server to the client configuration. Run the startup command directly and check three things first: whether the process stays alive, whether stderr contains an import/dependency failure, and whether stdout contains ordinary text. A legacy 2025 client may leave the process waiting for initialize; a 2026-07-28 client no longer uses that old handshake. In either case, ordinary startup text should not be mixed into the protocol stream.

Step 2: Confirm the protocol version before testing the lifecycle.

For MCP 2026-07-28, do not copy an old tutorial that manually sends initialize and notifications/initialized. The core is stateless, request metadata carries protocol/client context, and capability discovery follows the modern flow. Use the current MCP Inspector or official SDK to generate the correct request sequence, then inspect the protocol messages and first stderr failure.

Only when the client explicitly uses a legacy version such as 2025-06-18 should you use the following minimum handshake:

{"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":{}}

Both generations share the same diagnostic requirement: protocol messages must have clean JSON-RPC boundaries, and ordinary diagnostics belong on stderr. For actual debugging, prefer the current MCP Inspector because it can follow the lifecycle for the protocol version you are testing.

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 for the target protocol: legacy 2025-06-18 covers initialize, notifications/initialized, and tools/list; 2026-07-28 should exercise the current stateless metadata/capability-discovery/method flow. Do not let one legacy handshake test stand in for both protocol generations.
  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.

More to Explore

Topic hub →
MCP Filesystem Server in Practice: Enabling Claude / Cursor to Securely Read Local FilesBuild a secure MCP Filesystem Server for Claude or Cursor with Roots, path allowlists, read-only tool scope, symlink/path checks, Prompt Injection defenses, and audit boundaries.MCP Tool Call Result Truncated: Causes, Pagination, Cursors, and Size LimitsMCP Tool Call Result Truncated is not a universal 64KB limit. Diagnose client, SDK, context and timeout limits, then return bounded results with totals, cursors and pagination.MCP OAuth Authentication in Practice: Why Remote MCP Servers Can't Go UnprotectedMCP OAuth Authentication in Practice: A practical guide to designing OAuth authentication and authorization for remote MCP servers, covering Protected Resource Metadata.MCP Streamable HTTP in Practice: From Local stdio to a Remote MCP ServerDeploy MCP Streamable HTTP with the 2026-07-28 protocol and Python SDK, covering stateless requests, proxies, auth, Origin checks, timeouts, and legacy compatibility.

AI Engineering Weekly

Production changes, real failures, experiments and new XBSTACK assets.

Comments & evidence

DISCUSSION

Questions, verification and corrections

Sign in to comment. Every new comment is reviewed before publication; while pending, it is visible only to you and the administrator.

Sign-in required Reviewed before public
Loading the discussion…