MCP Logging: What I Learned Adding Proper Logging to My Production MCP Server
Honestly, I used to think logging was one of those "boring" parts of backend development. You know the drill — add a few logger.info() calls, call it a day, and forget about it until something breaks.
But after building and running a production MCP (Model Context Protocol) server for my 1,800-hour knowledge base project for a few months, I've learned that logging in MCP servers is actually different. If you get it wrong, debugging MCP issues becomes a nightmare. If you get it right, you can actually see what your AI clients are asking, what your server is returning, and catch issues before users even report them.
So here's the thing: I've spent the last week redoing logging from scratch for my MCP server, and I want to share what worked, what didn't, and what every MCP server developer should know.
Why MCP Logging Is Different
Let me start with a story. Three weeks ago, I got a bug report from a user: "Sometimes your MCP server just returns empty results. It works the second time I try, though."
Great. Intermittent bugs are every developer's favorite, right? I tried reproducing it for a day — nothing. I checked my existing logs... and guess what? I was only logging requests, not responses. I could see that a tools/call came in, but I had no idea what actually went out.
That's when I realized: MCP is a protocol between two AI systems. You've got an AI client (like Claude Desktop, Continue, Cursor, whatever) making requests to your MCP server, and your server sends back JSON responses. If something goes wrong with the JSON formatting or the content, the client just gets an empty response or a parse error. Without proper logging, you're flying blind.
Unlike regular REST APIs where a human is clicking around and telling you exactly what they did, in MCP-land:
- The client is AI — it can make requests you never expected, with parameters that look weird to humans
- The user doesn't understand what's happening — they just say "it doesn't work" and have no way to give you details
-
JSON serialization is easy to break — one unescaped quote, one wrong
null, and the entire response is garbage - Debugging happens after the fact — you need the full conversation to figure out what went wrong
In my case, the bug ended up being a note with a " character in the title that I wasn't escaping properly when manually building JSON (don't judge — I learned that lesson the hard way in the error handling article). Without logging the response, I would've been hunting for that bug forever.
What I Changed: The New Logging Approach
After that frustrating debugging session, I completely rewrote how logging works in my MCP server. Here's what I ended up with, using Java/Spring Boot as an example (but the principles apply to any language/framework):
1. Log Both Request AND Response (Everything)
This seems obvious, but I skipped it at first. I was logging incoming requests, but not outgoing responses. Don't do that. Log everything.
Here's my request/response logging filter now:
@Component
public class McpLoggingFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(McpLoggingFilter.class);
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
if (!isMcpEndpoint(request)) {
filterChain.doFilter(request, response);
return;
}
// Wrap request to read body multiple times
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
long startTime = System.currentTimeMillis();
try {
filterChain.doFilter(requestWrapper, responseWrapper);
} finally {
long duration = System.currentTimeMillis() - startTime;
String requestBody = new String(requestWrapper.getContentAsByteArray(), StandardCharsets.UTF_8);
String responseBody = new String(responseWrapper.getContentAsByteArray(), StandardCharsets.UTF_8);
log.info("\n=== MCP Request/Response ===" +
"\nEndpoint: {}" +
"\nMethod: {}" +
"\nDuration: {}ms" +
"\nRequest: {}" +
"\nResponse: {}" +
"\n============================",
request.getRequestURI(),
request.getMethod(),
duration,
truncateIfNeeded(requestBody),
truncateIfNeeded(responseBody)
);
responseWrapper.copyBodyToResponse();
}
}
private boolean isMcpEndpoint(HttpServletRequest request) {
return request.getRequestURI().startsWith("/mcp/");
}
private String truncateIfNeeded(String content) {
// Don't log 100KB of search results — that's just noise
if (content.length() > 10_000) {
return content.substring(0, 10_000) + "... [truncated, total " + content.length() + " chars]";
}
return content;
}
}
A few things I like about this:
- It's a filter, so it catches all MCP requests no matter what controller handles them
- It logs duration, which helps you spot slow queries immediately
- It truncates large responses to keep your log files from exploding (I learned this the hard way too — one full knowledge base search can be 100KB+ of text)
- It keeps request and response together in the same log entry, so you don't have to go searching through thousands of lines to match them up
2. Structured Logging for Important Fields
I still use plain text logging for development (I know, I know — everyone says structured JSON logging is the future, but I'm old school). But for important fields that you might want to query later, I add them as structured fields if your logging framework supports it.
In SLF4J that looks like this:
log.atInfo()
.addArgument(requestId)
.addArgument(method)
.addArgument(durationMs)
.addArgument(responseStatus)
.log("mcp.request method={} request_id={} duration={}ms status={}");
Why do this? Because when you get 1000 requests a day (okay, I get like 10 requests a day — it's a side project), and you need to find the one that failed, being able to grep by status=500 is way faster than scrolling through everything.
Even if you don't have fancy logging infrastructure, this habit pays off when you finally need to debug production issues.
3. Don't Log Secrets — But Do Log Everything Else
This should go without saying, but I'll say it anyway: never log API keys, auth tokens, or user PII. But everything else is fair game.
In my MCP server, the API key can come through several places (remember the authentication article where I said you need to support multiple locations because different clients do different things?). I make sure to redact it before logging:
private String redactApiKey(String body) {
// Replace api_key value with *** REDACTED ***
return body.replaceAll("(?i)(\"api[_-]key\"\\s*:\\s*\")[^\"]+\"", "$1*** REDACTED ***\"");
}
Simple, effective, and keeps secrets out of your logs. Everything else — tool names, parameter values, result content — I log it all. The cost of disk space is way lower than the cost of debugging without logs.
4. Separate MCP Logs From Your Regular App Logs
This was a game-changer for me. I used to mix MCP logs in with all my other application logs. Now I configure my logging framework to send all MCP-related logs to a separate file.
In logback-spring.xml that looks like this:
<appender name="MCP_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/mcp.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/mcp.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.kevinten.papers.mcp" level="info" additivity="false">
<appender-ref ref="MCP_FILE"/>
<appender-ref ref="CONSOLE"/>
</logger>
Why is this so great? Because when I need to debug an MCP issue, I just tail -f logs/mcp.log instead of grepping through thousands of lines of unrelated application logs. It's a simple change that saves me so much time.
The Good, The Bad, and The Unexpected: Lessons I Learned
After running this new logging setup for a few weeks, here's what surprised me:
Good: I Caught a Bug Before Anyone Reported It
Last week, I checked my logs and noticed this:
Request: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search_knowledge","arguments":{"query":"MCP authentication"}}}
Response: {"jsonrpc":"2.0","id":1,"result":{}}
Empty result! Why? Turns out my search query matched some notes that were marked as private in my knowledge base, so the service returned an empty list. That's not a bug per se, but it's a terrible user experience. The AI client gets nothing, doesn't know what happened, and the user thinks the server is broken.
I fixed it within five minutes of seeing it in logs:
if (results.isEmpty()) {
return List.of(
new TextContent("No results found for your query: " + query +
"\n\nTry broadening your search terms or check if the topic exists in my knowledge base.")
);
}
Now the AI gets a human-readable message it can show to the user, instead of just nothing. I would've never found this if I wasn't logging responses. The user probably would've gotten frustrated and not even bothered reporting it — they would've just assumed it doesn't work.
Bad: Logging Can Add Overhead (But It's Negligible for Most Cases)
I did notice a small performance hit after adding full request/response logging. Nothing dramatic — we're talking a few extra milliseconds per request. For my side project with 10-20 requests a day, it's totally irrelevant.
But if you're running a high-traffic MCP server, you might want to think about sampling. Log every request in development, log 10% or 1% in production. Or just log errors by default and enable full logging when you need to debug.
For me, though — and I suspect for most side project and even small production MCP servers — the overhead is totally worth it. The debuggability gain is enormous.
Unexpected: I Learned How People Actually Use My MCP Server
This was a huge surprise. I built my MCP server so people can query my knowledge base. I expected people to search for technical topics, get answers, done.
But looking through my logs, I've discovered people are using it in ways I never expected:
- Some people ask it to write entire articles based on my notes
- Some people use it to help debug their own MCP servers by asking how I solved specific problems
- One person even used it to generate interview questions based on my experience building MCP servers
Reading through the actual requests has given me so many ideas for new tools and improvements. I'm currently working on a generate_blog_post tool based on what I've seen people doing. I never would've thought of that if I wasn't logging.
Now, obviously this is my own personal knowledge base, and all users know I'm logging — I mention it in the README. If you're handling user data, you need to be careful about this and be transparent. But for my open source side project, it's been incredibly valuable to see how people actually use it.
Pros & Cons of My Current Setup
Let me be honest — this setup isn't perfect. Here's what works and what doesn't:
Pros ✅
- Full visibility — When something goes wrong, I have the full request and response in one place. No more "what actually happened?" guesswork.
- Separation of concerns — MCP logs are in their own file, so they don't get lost in other application noise.
- Truncation keeps things manageable — I don't fill up my disk with multi-megabyne log files from large search results.
- Secrets are redacted — I don't have to worry about accidental API key leaks in logs.
- Catch bugs early — I've caught three issues before users even reported them. That's a huge win for user experience.
Cons ❌
- Still plain text — If I want to do fancy queries across all my logs, I need to parse them manually. Structured JSON logging would help here, but I haven't bothered yet for a side project.
- No request correlation — If a request goes through multiple services, I don't have a request ID that follows it all the way through. Again, not a big deal for a single-server side project, but would be needed for distributed systems.
- Truncation can hide bugs — Once in a blue moon the bug is in the truncated part. I've increased my limit to 10KB, which has solved this for me, but it's still a tradeoff.
- Privacy considerations — If you're serving multiple users, you need to think about whether you should be logging user requests at all. For my single-user + public guest access setup, it's fine, but your mileage may vary.
Recommendations for Your MCP Server
Based on my experience, here's what I recommend every MCP server developer do:
- Always log both request AND response — I can't stress this enough. If you only take one thing away from this article, let it be this.
- Keep them together — Put request and response in the same log entry. Don't log request at the start and response at the end in different places. You'll thank me when you need to debug.
- Redact secrets — Never log API keys, passwords, or PII. It's not worth the risk.
- Put MCP logs in their own file — It makes debugging so much easier when you don't have to sift through unrelated logs.
- Truncate large payloads — You don't need 100KB of search results in your logs. 10KB is enough for 99% of debugging cases.
- Log duration — It helps you spot performance issues before users notice them.
- Sample if you need to — If you're running high traffic, don't log everything. Log all errors, sample a percentage of successful requests.
Who Is This For?
If you're building an MCP server for production, even a small one — do this. If you're just playing around locally — do this anyway. You'll need it when something breaks, and it's easier to set it up at the beginning than to add it later when you're panicking because your production server is broken and you have no logs.
Honestly, I built my first version of the MCP server without proper logging because I thought "it's just a simple protocol, how hard can it be?" I learned the hard way. Don't be me. Well, actually — do be me, but learn from my mistakes instead of making them yourself.
Final Thoughts
After going through this process, I've realized that good logging isn't just about debugging when something breaks. It's about having visibility into how your system is actually being used. In MCP-land, where you have two AI systems talking to each other and humans are one step removed, that visibility is even more important than usual.
I've been building side projects for over ten years, and I still forget the basics sometimes. Logging seems boring, it seems like it doesn't matter, but when you need it, it's worth more than any fancy framework or optimization.
So what's your experience with MCP logging? Have you had similar horror stories where lack of logging made debugging a nightmare? Do you have any tricks I didn't mention here? Drop a comment below and let me know — I'm always looking for ways to improve.
And if you want to see the full code for my MCP server, check it out on GitHub — it's all open source: https://github.com/kevinten10/Papers
Top comments (0)