Logging MCP Protocol When Using stdio, Part I
Learn different ways to log the MCP JSON protocol over stdio, with an intro to MCP and a deep dive into its key components and their nuances.
Join the DZone community and get the full member experience.
Join For FreeLogging MCP Protocol When Using stdio
If you haven’t heard of MCP — the Model Context Protocol — you’ve probably been living under a rock. The Model Context Protocol (MCP) is becoming widely recognized, standardizing how applications provide context to LLMs. It barely needs an introduction anymore. Still, for the sake of completeness, let me borrow selectively from the official MCP site. Do take a moment to explore the well-explained pages if you're new to MCP.
MCP is an open protocol that standardizes how applications provide context to LLMs. It’s designed to help developers build agents and complex workflows on top of LLMs. Since LLMs often need to interact with external data and tools, MCP offers:
- A growing set of prebuilt integrations that LLMs can plug into directly
- The flexibility to switch between different LLM providers and vendors
- Best practices for securing data within your own infrastructure
Why This Article?
When working with stdio-based MCP clients and servers — like GitHub Copilot and MCP servers — it becomes surprisingly difficult to observe or debug protocol messages. The typical go-to, MCP Inspector, often doesn’t help when the client launches the MCP server directly over stdio, with no proxy, and no HTTP or SSE transport to hook into.
This article shows how to break through that limitation.
We’ll explore simple, effective techniques for logging MCP protocol communication over stdio, making otherwise hidden interactions observable and explainable, especially during local testing and debugging. The goal is to enable deeper visibility into real-time LLM completion flows.
Before diving into logging, though, we’ll get a basic MCP setup running with a custom server and client.
While it's true that I started this write-up primarily to tackle the problem of logging MCP’s stdio JSON protocol communication, I also cover several core MCP concepts that are relevant beyond just logging.
Everything discussed in this article can be done at no cost, using free tools and open-source components.
Choosing an MCP Client
Lately, I’ve been exploring GitHub Copilot, so we’ll start with that — specifically, the GitHub Copilot for Eclipse plugin (officially from GitHub). It gives us a ready-made MCP client, so we can focus on building and observing the MCP server side.
That said, the same approach works with any MCP-compatible client, such as:
- GitHub Copilot for VSCode
- Claude Desktop
- Any other tool that speaks the MCP protocol and allows configuring an external server
What Comes Next
Once we have the basic setup running, we’ll take it further and:
- Write a minimal custom MCP client.
- Connect it to the same MCP server.
- Use the same logging techniques to inspect `stdio`-based messages in both directions.
- We will demonstrate using this MCP client what we were unable to demonstrate using Copilot. Namely, using resources, prompts, and completions. This is because standard Copilot integrations often focus primarily on tool invocation, making direct observation of other protocol features challenging. We will also discuss why this is so.
We will not be demonstrating LLMs invoking tools in this custom MCP client. That has already been discussed with Copilot.
We’ll also explore how the MCP Inspector can be used alongside our custom server. When the transport is `stdio`, the Inspector essentially behaves like another MCP client — one that doesn’t include an LLM. Think of it as an “MCP browser” or a Postman-like tool for MCP: it lets you inspect messages, invoke tools, and debug interactions directly. (Let's ignore agentic Postman, which is a different topic.)
Why Start With stdio?
You might ask — why start with `stdio` at all? Why not just use HTTP or SSE?
Well, that’s where MCP begins. `stdio` is the default and often the most direct way for LLM clients to communicate with custom agents, especially in plugin-style environments like Copilot. Even Claude Desktop only recently added support for Remote MCP, and that too after a long-standing community request. It's anticipated that SSE will eventually become a more widely embraced transport option.
So for now, we’re sticking with `stdio` — and figuring out how to peek under the hood to see what's really going on, which should be useful for beginners.
In a Hurry? Here Are the Three Tricks Right Upfront
If you're already familiar with MCP or have a working setup and just need the logging shortcut, here's the core idea, demonstrated in Java. This approach is adaptable to virtually any language.
Method 1: Direct SDK/Library Logging (The Cleanest Route)
Depending on the MCP SDK or library you're using, you might be able to tap into the protocol output directly by configuring its built-in logger. This is generally the cleanest and most performant route when it works.
Method 2: Extending the SDK Logging
If the SDK/library does not provide adequate logging for the MCP protocol, you can identify and utilize its extension points to implement custom logging. This document demonstrates this approach also specifically for enabling outgoing MCP JSON logging, which was initially missing from the default setup.
Method 3: Standard I/O (stdio) Interception With "Tee" (The Robust Alternative When Nothing Else Works)
However, if you encounter limitations — for instance, the library isn't logging the full MCP JSON (as we'll see later with outgoing MCP protocol JSON) — then a more direct approach to intercepting System.in and System.out is necessary. This method relies on how the SDK handles internal stream wiring and is not always the "best" approach in terms of pure performance, but it offers maximum visibility with minimal intrusion.
That’s why, when nothing else works — or when you want a reliable way to capture every byte flowing through standard I/O:
Tee is your friend.
The Apache Commons IO TeeOutputStream and TeeInputStream classes allow you to duplicate a stream's data. As data is written to or read from the original stream, a copy is simultaneously sent to a second, "branched" stream. This is inspired by the Unix tee command, which splits a stream into two, sending one to standard output and another to a file.
You don't have to write to a file as shown below for simplicity. Your "tee" target can be a logger, a socket, a GUI component, an in-memory buffer, or even a queue — anything that helps you observe or capture the data flow without breaking it. For performance, especially in production, consider streaming to a more efficient target than direct file writes.
FileOutputStream fos = new FileOutputStream(new File(dir, "out.txt")); // For outgoing messages
FileOutputStream fis = new FileOutputStream(new File(dir, "in.txt")); // For incoming messages
FileOutputStream fcs = new FileOutputStream(new File(dir, "combined.txt")); // For combined messages
PrintStream originalOutputStream = System.out;
InputStream originalInputStream = System.in;
System.setOut(new PrintStream(
new TeeOutputStream(originalOutputStream, new TeeOutputStream(fos, fcs))
));
System.setIn(new TeeInputStream(
originalInputStream, new TeeOutputStream(fis, fcs)
));
We're using Spring AI as an MCP SDK purely for demonstration purposes here. The same logging concepts and inspection strategies should work just as well with other MCP SDKs, regardless of language or framework. These are protocol-level techniques, not specific to Spring or Java.
We've now covered the core concepts and techniques for logging MCP over stdio. In the next part of this article, we'll move from theory to practice by building a small conversational agentic application from scratch, exploring various MCP nuances along the way. If you're ready for a deeper dive, let's begin the step-by-step implementation.
Continue to Logging MCP Protocol When Using stdio, Part II for the step-by-step implementation.
Opinions expressed by DZone contributors are their own.
Comments