Logging from a stdio server
In this lesson one debug line breaks a tool server for our test client, and we find out why that client can’t read the server’s reply. The fix changes where the line goes. Then we read the log of the real file-system server while an agent calls it.
The previous lesson connected the reference file-system server of the
Model Context Protocol (MCP) project, and showed a stand-in that speaks
the same stdio transport. Here we use that stand-in again. A quick way
to debug a server you write is a print, and on this transport a
print to standard output is a bug.
What a debug line does to the stream
Section titled “What a debug line does to the stream”On the stdio transport the client starts the server as a child process. The server reads requests from its standard input and writes its messages to its standard output, one message per line in JSON-RPC, the JSON format for Remote Procedure Calls (RPC) that MCP uses. The specification says a server must not write anything to standard output that isn’t a valid MCP message. A server that wants to log writes to standard error instead, and the client may show that stream, pass it on or drop it. The client also shouldn’t take output on standard error as a sign that something failed [1]. That rule is stdio logging, and the fixture below shows what happens without it.
The fixture directory for this lesson,
site/examples/customizing-agents/logging-from-a-stdio-server/, holds
server.py. It answers each request with the stand-in’s own code from
../mcp-first-server/stand_in.py, so it has the same three read-only
tools and the same scope. Before each answer it calls log, which prints
the method and the tool name:
def log(message): print(message)check.py plays the client. It starts a server and sends one
tools/call for list_directory on the handbook directory. Then it
reads one line from the server’s standard output. It prints that line,
cut at 40 characters, and whether the line parsed as JSON. Last it prints
what the server wrote to standard error. call_server.py runs it against
server.py.
Call the server
Section titled “Call the server”Run call_server.py in the fixture directory, and compare with the
output below.
python3 call_server.pystdout line: debug: tools/call list_directory parsed: no (Expecting value) stderr: (empty)
Output verified in CI from site/examples/customizing-agents/logging-from-a-stdio-server/call_server.py.
The server worked. It got the request, ran the tool and wrote a correct
reply. But print writes to standard output by default, so the debug
line went out first, on the line where the client waits for its reply.
The client can’t parse debug: ... as JSON-RPC, although the server did
its work, and the specification has no rule for what a client does with
such a line. check.py stops at the first line it can’t parse, while
another client may drop the line, report an error and read on. Either
way the server broke the rule, and you can’t count on one outcome.
Why did the client fail?
Section titled “Why did the client fail?”A small MCP server over the stdio transport prints a debug line with Python's print before it writes each reply. A test client sends one tools/call, reads one line from the server's standard output and reports that it can't parse the line as JSON. The server's standard error is empty.
The client’s report says parsed: no (Expecting value), and the line it
read is debug: tools/call list_directory. What went wrong?
Which stream did the debug line go to, and what does the client expect to read from that stream?
Move the log to standard error
Section titled “Move the log to standard error”The fix is one argument. server_fixed.py is server.py with the log
function changed to write to standard error:
def log(message): print(message, file=sys.stderr)call_fixed_server.py sends the same call to server_fixed.py.
Call the fixed server
Section titled “Call the fixed server”Run call_fixed_server.py in the fixture directory, and compare with the
output below.
python3 call_fixed_server.pystdout line: {"jsonrpc": "2.0", "id": 1, "result":...
parsed: yes, isError: False
stderr: debug: tools/call list_directoryOutput verified in CI from site/examples/customizing-agents/logging-from-a-stdio-server/call_fixed_server.py.
Now the first line on standard output is the reply, and the debug line is still there, on standard error. You kept the information and the client gets a clean stream.
MCP also has a logging feature inside the protocol, a notifications/message
the server sends to the client. Revision 2026-07-28 deprecates it, and
its registry of deprecated features tells a stdio server to log to
standard error instead [2]. So on stdio,
standard error is the place for the server’s log, whatever your client
does with it.
Fix the debug line
Section titled “Fix the debug line”The server talks to its agent over the MCP stdio transport, where standard output carries the replies.
A colleague’s stdio server stopped working with their agent after they
added a print of a debug line before each reply. They still need the
debug line for another bug, and they follow MCP revision 2026-07-28. What
do you suggest?
Which change keeps the debug line and leaves standard output with protocol messages only?
Read the log while the agent calls it
Section titled “Read the log while the agent calls it”A client may ignore the server’s standard error [1], so
to read the log with any agent, send it to a file yourself. Start the
server through sh -c with 2>> and a file name, and sh appends
everything the server writes to standard error to that file. Standard
output stays connected to the agent.
The reference file-system server replaces its allowed directories with the roots the client sends [3], and it writes its own messages to standard error. This is its standard error from a run on October 7, 2026, with version 2026.8.31 and a small test client that sent one root and then called a tool. The run logged the start on stdio and the switch to the roots, and nothing for the tool call:
Secure MCP Filesystem Server running on stdioUpdated allowed directories from MCP roots: 1 valid directoriesThe previous lesson showed that Claude Code sends the directory you
started it in as a root [4], so under Claude Code the
second line tells you the server took its scope from that directory.
The line counts the directories, so if it says anything other than 1,
remove the server before you ask the agent anything.
On that run npx also wrote two npm notice lines of its own to
standard error, so your file may hold lines like those too.
Which stream?
Section titled “Which stream?”An MCP server runs over the stdio transport as a child process of the agent, and writes to its standard output and its standard error.
Place each line where a stdio server writes it.
What does the client read from standard output, and what does it parse there?
Lines on standard error
Section titled “Lines on standard error”An agent shows the lines a stdio MCP server writes to its standard error. The server's tool calls succeed.
Your agent shows a few lines from the server’s standard error, and the calls work. What do the lines tell you?
What does the specification give standard error to, on the stdio transport?
Exercise
Fix the bug, then read a real server’s log. From the root of the course
repository, copy both fixture directories next to each other into an
empty directory, so server.py still finds the stand-in, and run the
client against the broken server in the copy:
mkdir -p /tmp/stdio-logcp -r site/examples/customizing-agents/mcp-first-server site/examples/customizing-agents/logging-from-a-stdio-server /tmp/stdio-log/cd /tmp/stdio-log/logging-from-a-stdio-serverpython3 call_server.pyChange the log function in your copy of server.py so it writes to
standard error, and run python3 call_server.py again. It now prints
what call_fixed_server.py printed above.
Then connect the reference server to the handbook copy with its standard error sent to a file, and start your agent inside the copy, as in the previous lesson. The server has write tools, so the copy is the only directory it may see. Claude Code gives the server the directory it started in as its root [4], and the command gives the same directory as the argument:
cd /tmp/stdio-log/mcp-first-server/handbookclaude mcp add handbook-log -- sh -c 'exec npx -y @modelcontextprotocol/server-filesystem@2026.8.31 /tmp/stdio-log/mcp-first-server/handbook 2>>/tmp/stdio-log/server.log'claudeIn a second terminal, run tail -F /tmp/stdio-log/server.log. Ask the
agent to list the files in the handbook and change nothing, and watch the
log while it works. A good result: the log has the two lines shown
above, the agent’s list has the four handbook files, and none of the log
lines showed up in the agent’s replies. When you’re done, remove the server
with claude mcp remove handbook-log [4]. Then ask
yourself: if the server had printed those lines to standard output
instead, how would you have found out, and would you have found out at
all?
Stretch: Add a log line to your fixed copy of server.py that prints the path of each call to standard error, and run call_server.py again to check the stream is still clean.
Recap
- On the stdio transport, standard output carries one JSON-RPC message per line and nothing else. A server logs to standard error, which the client may show, keep or drop, and which isn’t a sign of failure [1].
- A
printwrites to standard output by default, so one debug line arrives where the client expects a message. What happens next is up to the client: the test client here stops, and another may drop the line and report an error. The rule is broken in either case. - The fix is to write the line to standard error, which keeps the information. MCP revision 2026-07-28 deprecates the in-protocol logging feature and points stdio servers to standard error [2].
- To read a server’s log with any agent, start it through
sh -cand append its standard error to a file, then read the file in a second terminal.
You can now
- Adds a tool via MCP or CLI with least privilege
References
Section titled “References”- Model Context Protocol a Series of LF Projects, LLC. Model Context Protocol specification 2026-07-28. modelcontextprotocol.io. Reference.
MCP specification - Model Context Protocol a Series of LF Projects, LLC. Deprecated Features, Model Context Protocol specification 2026-07-28. modelcontextprotocol.io. Reference.
MCP deprecated features - Model Context Protocol a Series of LF Projects, LLC. Filesystem MCP Server. GitHub, modelcontextprotocol/servers, src/filesystem. Reference.
MCP filesystem server - Anthropic. Connect Claude Code to tools via MCP. Claude Code documentation. Reference.
Claude Code mcp