# MCP stdio "Unexpected Token" Errors: How stdout Breaks JSON-RPC

> Seeing Unexpected token ... is not valid JSON from a stdio MCP server? Something printed to stdout, which is reserved for JSON-RPC. I reproduce the real errors, show a one-line test that finds the culprit, and fix it in Node and Python, including npm run and dotenv.

**Source:** https://mcpplaygroundonline.com/blog/mcp-stdio-unexpected-token-json  
**Author:** Nikhil Tiwari  
**Published:** 2026-11-01  
**Category:** Troubleshooting  
**Reading time:** 9 min read  
**Topics:** mcp stdio unexpected token json, unexpected token is not valid json mcp, mcp server stdout, mcp console.log stdio, mcp stdio stderr logging, mcp error -32001 request timed out, mcp server not valid json claude desktop, python mcp print stdout, dotenv mcp server, mcp json-rpc stdout

---

📖 TL;DR

-   **A stdio MCP server may only write JSON-RPC messages to stdout.** Anything else shows up in the client as `Unexpected token 'X', "..." is not valid JSON`.
-   **The character in quotes is your clue.** `'S'` is usually "Server started", `'>'` is an `npm run` header, `'◇'` is dotenv 17.
-   **A full stray line is often just noise.** Output without a newline is worse: it glues onto the next real message, and the call ends in `MCP error -32001: Request timed out`.
-   **Find it in one command:** pipe an `initialize` request into your server with stderr hidden. Every line you see must start with `{"jsonrpc"`.
-   **Fix:** logs go to stderr. `console.error` in Node, `logging` or `print(..., file=sys.stderr)` in Python.

## Table of Contents

-   [Why stdout breaks a stdio MCP server](#why)
-   [What each "Unexpected token" message means](#messages)
-   [When is stdout output fatal?](#fatal)
-   [Find what's printing to stdout in one command](#find)
-   [Six common causes of stdout pollution](#causes)
-   [Fix it in Node.js and TypeScript](#node)
-   [Fix it in Python](#python)
-   [Keep your logs: read stderr while you test](#desktop)
-   [FAQ](#faq)

## Why stdout breaks a stdio MCP server

You add one `console.log("Server started")` to your [MCP server](/blog/what-is-model-context-protocol). Now the client log fills with **Unexpected token ... is not valid JSON**.

The server runs fine in your terminal. You can see the message printed there. So what's wrong?

With the _stdio transport_, the client starts your server as a child process. **It talks to it over stdin and stdout, one JSON-RPC message per line.**

The [MCP transport spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports) is strict about this. **The server must not write anything to stdout that isn't a valid MCP message.** Logs belong on stderr.

Your terminal shows both streams mixed together, so the stray line looks harmless. The client reads stdout as protocol. It tries to parse "Server started" as JSON and fails.

In this post I reproduce the real errors with a test server, show you **how to find the line that's printing**, and fix it for Node and Python.

## What each "Unexpected token" message means

Node's JSON parser quotes the start of the bad line. **That's usually enough to tell you exactly what printed.** I ran these through Node 22:

Error in the client log

What your server printed

`Unexpected token 'S', "Server sta"... is not valid JSON`

A startup message from `console.log` or `print`

`Unexpected token '>', "> my-serve"... is not valid JSON`

The header `npm run` prints before your script

`Unexpected token '◇', "◇ injected"... is not valid JSON`

dotenv 17 announcing it loaded your `.env`

`Unexpected token 'L', "Loading..."... is not valid JSON`

A progress message, often written without a newline

`Unexpected end of JSON input`

An empty line, for example from `console.log()`

A schema or validation error, no "Unexpected token"

Valid JSON that isn't JSON-RPC, such as a structured log line

Older Node versions word it differently: `Unexpected token S in JSON at position 0`. **Same cause, same fix.**

Not sure where your client keeps its log? Claude Desktop writes `~/Library/Logs/Claude/mcp-server-NAME.log` on a Mac and `%APPDATA%\Claude\logs\` on Windows. My [guide to servers that work in the terminal but not in Claude Desktop](/blog/mcp-server-works-in-terminal-not-claude-desktop) lists the others.

## When is stdout output fatal?

Here's what surprised me. **A stray line doesn't always break the server.** I wrote a small test server and connected to it with the official [TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) client (v1.24):

What the server printed

Client error

Did the tool call work?

`console.log("Server started on stdio")`

Unexpected token 'S'

Yes

`console.log("ping called")` inside the tool

Unexpected token 'p'

Yes

A JSON log line, `{"level":"info",...}`

Schema validation error

Yes

`process.stdout.write("Loading...")`, no newline

Unexpected token 'L'

**No:** `MCP error -32001: Request timed out`

The last row is the dangerous one. **Without a newline, "Loading..." and the server's next JSON message end up on the same line.** The whole line fails to parse, so the client never sees the reply.

Here, that lost reply was the answer to `initialize`. The connection never came up, and the client waited until it gave up. **A timeout with no other error is a strong hint of this.**

Other clients are less forgiving than the SDK. Depending on the client and version, a bad line can close the connection. Then you see "Server disconnected" instead.

So don't treat the error as harmless noise. **A server that only works because the client skips bad lines will break in the next client.**

## Find what's printing to stdout in one command

Don't hunt through files yet. **Run the server exactly as the client does, send it an `initialize` request, and hide stderr.** Whatever's left is stdout.

On macOS or Linux, use the command and args from your client config:

```
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"check","version":"0"}}}' \
  | node build/index.js 2>/dev/null
```

In PowerShell, swap the stderr redirect:

```
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"check","version":"0"}}}' |
  node build/index.js 2>$null
```

For a Python server, replace `node build/index.js` with your command, such as `uv run server.py`.

A clean server prints exactly one line, then exits when the input ends. It looks like this:

```
{"result":{"protocolVersion":"2025-11-25","capabilities":{"tools":{...}},...},"jsonrpc":"2.0","id":1}
```

My polluted test server printed this instead:

```
Server started on stdio
{"result":{"protocolVersion":"2025-11-25",...},"jsonrpc":"2.0","id":1}
```

**Every line must start with `{` and contain `"jsonrpc":"2.0"`.** Anything else is your bug.

This only checks startup. If the stray output appears when a tool runs, call that tool too. A [local stdio tester](/blog/test-local-mcp-server-stdio-inspector-ai-agents) is quicker for that than hand-typed JSON.

## Six common causes of stdout pollution

### 1\. console.log, console.info and print

The obvious one. In Node, `console.log`, `console.info` and `console.debug` all write to stdout. **Only `console.error` and `console.warn` go to stderr**, as the [Node console docs](https://nodejs.org/api/console.html) describe.

In Python, `print()` writes to stdout unless you pass `file=sys.stderr`.

### 2\. Starting the server with npm run

If your config runs `npm run start`, npm prints a blank line and a header before your script starts:

```

> my-server@1.0.0 start
> node build/index.js
```

**That's stdout, and it reaches the client.** Point the config at `node` and the built file directly. If you must use npm, add `--silent`: `npm run --silent start`. See the [npm run docs](https://docs.npmjs.com/cli/commands/npm-run-script).

### 3\. dotenv 17

[dotenv](https://github.com/motdotla/dotenv) 17 logs a line like `◇ injected env (1) from .env` to stdout by default. I confirmed it with 17 and checked that 18 is quiet again.

**Upgrade to dotenv 18, or pass `quiet: true`**: `dotenv.config({ quiet: true })`. Better still, put the variables in the client config's `env` block. The client doesn't run from your project folder, so `.env` often isn't found anyway.

### 4\. Libraries that print banners or progress

Some packages print a banner, a deprecation notice or a download progress bar when imported. **You didn't write the line, but your process still printed it.**

The one-command test above catches these. Look for text you don't recognise, then find which import produces it.

### 5\. Writing without a newline

`process.stdout.write("Loading...")` in Node, or `print("...", end="")` in Python. **This is the case that times out**, because it corrupts the next real message.

### 6\. Structured logs on stdout

Loggers like pino write JSON lines to stdout by default. **The line parses as JSON, so you get a validation error instead of "Unexpected token".** Point the logger's destination at stderr (file descriptor 2).

Hit something that isn't on this list? My [MCP server troubleshooting guide](/blog/mcp-server-troubleshooting-common-errors-fix) covers the other common errors. For servers launched through npx, see [fixing spawn npx ENOENT](/blog/fix-spawn-npx-enoent-mcp).

## Fix it in Node.js and TypeScript

Replace every `console.log` with `console.error`. Then add a guard as the **first lines of your entry file**, so a log call you missed can't reach stdout:

```
// Stdio MCP server: stdout is for JSON-RPC only.
console.log = console.error;
console.info = console.error;
console.debug = console.error;
```

This is safe. The SDK's `StdioServerTransport` writes with `process.stdout.write`, not `console.log`, so your protocol messages still go out.

It won't catch a library that calls `process.stdout.write` itself. **Run the one-command test again** after the change to be sure.

Building your first server? The [Node.js MCP server guide](/blog/build-custom-mcp-server-using-nodejs-simple-guide) starts with logging set up this way.

## Fix it in Python

Use the [logging module](https://docs.python.org/3/library/logging.html). **`logging.basicConfig()` writes to stderr by default**, so it's safe as-is:

```
import logging
import sys

logging.basicConfig(level=logging.INFO)  # stderr by default
log = logging.getLogger(__name__)

log.info("Server starting")              # safe
print("debug", file=sys.stderr)          # safe
print("debug")                           # breaks stdio
```

If an import prints something you can't turn off, wrap it before the server starts:

```
import contextlib
import sys

with contextlib.redirect_stdout(sys.stderr):
    import noisy_library
```

The same rules apply to servers built with FastMCP and the official [Python SDK](https://github.com/modelcontextprotocol/python-sdk). My [FastMCP tutorial](/blog/build-mcp-server-python-fastmcp-tutorial) walks through a full server.

## Keep your logs: read stderr while you test

Moving logs to stderr fixes the protocol. **But now your logs seem to vanish**, because most clients bury stderr in a log file.

[MCP Playground Desktop](/blog/mcp-playground-desktop-guide) runs your stdio server and shows its output in a **Server stderr** tab, next to every JSON-RPC message in both directions.

-   **Your logs and stack traces** appear in Server stderr as the server writes them.
-   **A request with no reply** in the JSON-RPC tab points to a lost message, like the no-newline case above.
-   **If the connection fails** and the server wrote to stderr, the error message tells you to check that tab.

One honest caveat: **like the TypeScript SDK client, the app skips stdout lines that aren't JSON.** A server with a stray print may connect fine there. Use the one-command test to prove stdout is clean.

[Get MCP Playground Desktop for Mac and Windows →](/local-mcp-server-testing)

Is your server already on a public HTTPS URL? None of this applies, because stdout isn't involved. [Test any MCP server free in your browser →](/mcp-test-server)

## FAQ

**What does "Unexpected token ... is not valid JSON" mean in an MCP server?+**

Your stdio server wrote something to stdout that isn't a JSON-RPC message. The client reads every stdout line as protocol, fails to parse it, and logs this error. The quoted text shows the start of the bad line.

**Can I use console.log in an MCP server?+**

Not with the stdio transport, because console.log writes to stdout. Use console.error, which writes to stderr. Over Streamable HTTP, console.log is fine because messages travel over HTTP, not stdout.

**Why does my MCP server time out with error -32001 after printing to stdout?+**

The output probably had no newline, so it joined the server's next JSON-RPC message on one line. That line fails to parse, the reply is lost, and the client waits until it times out.

**Where should a stdio MCP server write its logs?+**

To stderr. The MCP spec allows servers to write UTF-8 logs to stderr, and clients may capture or ignore it. In Python, the logging module writes to stderr by default.

**Why does dotenv break my MCP server?+**

dotenv 17 prints a line about the variables it loaded to stdout by default. Upgrade to dotenv 18, which is quiet again, or call dotenv.config({ quiet: true }).

## Conclusion

On a stdio MCP server, stdout carries JSON-RPC and nothing else. **The quoted characters in the error tell you what printed**, and the one-command test shows you every stray line.

Move logs to stderr, run the test again, and keep reading those logs while you test. [Get MCP Playground Desktop for Mac and Windows →](/local-mcp-server-testing)

## Frequently asked questions

### What does "Unexpected token ... is not valid JSON" mean in an MCP server?

Your stdio server wrote something to stdout that isn't a JSON-RPC message. The client reads every stdout line as protocol, fails to parse it, and logs this error. The quoted text shows the start of the bad line.

### Can I use console.log in an MCP server?

Not with the stdio transport, because console.log writes to stdout. Use console.error, which writes to stderr. Over Streamable HTTP, console.log is fine because messages travel over HTTP, not stdout.

### Why does my MCP server time out with error -32001 after printing to stdout?

The output probably had no newline, so it joined the server's next JSON-RPC message on one line. That line fails to parse, the reply is lost, and the client waits until it times out.

### Where should a stdio MCP server write its logs?

To stderr. The MCP spec allows servers to write UTF-8 logs to stderr, and clients may capture or ignore it. In Python, the logging module writes to stderr by default.

### Why does dotenv break my MCP server?

dotenv 17 prints a line about the variables it loaded to stdout by default. Upgrade to dotenv 18, which is quiet again, or call dotenv.config({ quiet: true }).


---

_Canonical page: https://mcpplaygroundonline.com/blog/mcp-stdio-unexpected-token-json — MCP Playground (mcpplaygroundonline.com), the free browser-based tool for testing MCP servers and building AI agents._
