MCP stdio "Unexpected Token" Errors: How stdout Breaks JSON-RPC
Nikhil Tiwari
MCP Playground
π 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 annpm runheader,'β'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
initializerequest into your server with stderr hidden. Every line you see must start with{"jsonrpc". - Fix: logs go to stderr.
console.errorin Node,loggingorprint(..., file=sys.stderr)in Python.
Table of Contents
- Why stdout breaks a stdio MCP server
- What each "Unexpected token" message means
- When is stdout output fatal?
- Find what's printing to stdout in one command
- Six common causes of stdout pollution
- Fix it in Node.js and TypeScript
- Fix it in Python
- Keep your logs: read stderr while you test
- FAQ
Why stdout breaks a stdio MCP server
You add one console.log("Server started") to your MCP server. 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 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 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 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 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 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.
3. dotenv 17
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 covers the other common errors. For servers launched through npx, see fixing spawn npx ENOENT.
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 starts with logging set up this way.
Fix it in Python
Use the logging module. 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. My 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 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 β
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 β
FAQ
What does "Unexpected token ... is not valid JSON" mean in an MCP server?+
Can I use console.log in an MCP server?+
Why does my MCP server time out with error -32001 after printing to stdout?+
Where should a stdio MCP server write its logs?+
Why does dotenv break my MCP server?+
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 β
Written by Nikhil Tiwari
15+ years in product development. AI enthusiast building developer tools that make complex technologies accessible to everyone.
Free MCP Tools (no install)
Build, compare & ship MCP agents
Connect any MCP server, run evals on it, compare 80+ models side-by-side, deploy hosted servers, and save reusable agents you can export as an API β all in your browser.