MCP Server Logging: What I Learned Adding Proper Observability to My 1,800-Hour MCP Knowledge Base
Honestly, I didn't think I needed logging at first.
Like many side project developers, I thought "It works on my machine, why would I need fancy logging?" I had System.out.println() here, e.printStackTrace() there, and that was good enough for me. I was wrong. So wrong.
After building 10+ MCP servers and 1,800 hours of development time on my personal knowledge base Papers, I finally added proper structured logging. And let me tell you — it solved 80% of my "why isn't this working?" problems in the first week alone.
If you're building an MCP server right now and skipping on observability — this one's for you.
Why MCP Servers Need Different Logging
So here's the thing about MCP: it's a proxy protocol. Your server doesn't actually talk directly to the end-user. It talks to an MCP client, which talks to an LLM, which talks to the user. That's three layers between your server and whoever's actually using it.
When something goes wrong — and trust me, something will go wrong — where do you look?
- Did the client even send the request?
- Did my server receive it?
- Did the tool call parse correctly?
- Did I return the wrong format?
- Did the LLM hallucinate a tool name that doesn't exist?
Before proper logging, I'd be staring at my editor, guessing, putting more printlns everywhere, restarting the server, trying to reproduce. It's exhausting. I spent three hours last month debugging why Claude Desktop couldn't connect to my server — turns out I was logging to stdout after I started the HTTP server, and the buffering was killing the connection. Three hours. For a logging issue.
That's when I finally did what I should've done from the start: add proper structured logging.
What I Built: Structured Logging for MCP
I'm using Java Spring Boot for my MCP server, so I went with SLF4J/Logback because that's what Spring Boot gives you out of the box. But the principles here apply to any language or framework.
The key insight I missed at first: MCP has well-defined endpoints, you should log every request and response automatically.
Here's what my logging filter looks like:
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
@Component
public class McpLoggingFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(McpLoggingFilter.class);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpReq = (HttpServletRequest) request;
HttpServletResponse httpRes = (HttpServletResponse) response;
Instant start = Instant.now();
String method = httpReq.getMethod();
String path = httpReq.getRequestURI();
log.info("MCP_REQUEST_START: method={}, path={}, remoteAddr={}",
method, path, httpReq.getRemoteAddr());
try {
chain.doFilter(request, response);
Duration duration = Duration.between(start, Instant.now());
log.info("MCP_REQUEST_COMPLETE: method={}, path={}, status={}, durationMs={}",
method, path, httpRes.getStatus(), duration.toMillis());
} catch (Exception e) {
Duration duration = Duration.between(start, Instant.now());
log.error("MCP_REQUEST_FAILED: method={}, path={}, durationMs={}, error={}",
method, path, duration.toMillis(), e.getMessage(), e);
throw e;
}
}
}
That's it — 40 lines of code, and suddenly I see every request coming in. When Claude Desktop says "connection failed," I can check if the request ever hit my server. If it didn't — the problem is with Claude's config, not my code. If it did — I can see what status code I returned.
But wait, it gets better. MCP is all about tool calls. So I added specific logging for the two most important endpoints: tools/list and tools/call.
Tool Call Logging: Because LLM's Get It Wrong All The Time
Here's the dirty secret about working with LLMs and MCP: the LLM will call the wrong tool name. It will mess up the parameters. It will do things you never expected. And without logging, you'll never figure out why.
I added this to my MCPServerController:
@RestController
@RequestMapping("/mcp")
public class McpServerController {
private static final Logger log = LoggerFactory.getLogger(McpServerController.class);
private final KnowledgeBaseMcpService mcpService;
// constructor omitted
@PostMapping("/call")
public ResponseEntity<McpCallResponse> callTool(@RequestBody McpCallRequest request) {
// Log EVERY tool call before processing
log.info("MCP_TOOL_CALL: name={}, sessionId={}, paramsSize={}",
request.getName(), request.getSessionId(),
request.getParams() != null ? request.getParams().size() : 0);
try {
McpCallResponse response = mcpService.callTool(request);
// Log the result size (don't log the full content, it's too big)
if (response.getResult() != null && response.getResult().getContent() != null) {
log.info("MCP_TOOL_SUCCESS: name={}, contentLength={}",
request.getName(), response.getResult().getContent().size());
} else if (response.getError() != null) {
log.warn("MCP_TOOL_ERROR: name={}, error={}",
request.getName(), response.getError().getMessage());
}
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("MCP_TOOL_EXCEPTION: name={}, error={}",
request.getName(), e.getMessage(), e);
throw e;
}
}
}
Why don't I log the full parameters or full response? Two reasons:
- Privacy: My knowledge base has personal notes. I don't want the full content in logs.
- Size: Tool responses can be huge — kilobytes, even megabytes. Your log files will bloat overnight.
Just log the metadata: what tool was called, how big the response was, whether it succeeded. That's 90% of what you need for debugging.
The other important endpoint is tools/list. Every client calls this on startup. If this fails, nothing works.
@PostMapping("/list")
public ResponseEntity<McpListToolsResponse> listTools() {
log.info("MCP_TOOLS_LIST_REQUEST");
try {
List<ToolInfo> tools = mcpService.listTools();
log.info("MCP_TOOLS_LIST_RESPONSE: toolCount={}", tools.size());
return ResponseEntity.ok(new McpListToolsResponse(tools));
} catch (Exception e) {
log.error("MCP_TOOLS_LIST_FAILED: error={}", e.getMessage(), e);
throw e;
}
}
I can't tell you how many times this has saved me. I once had an issue where the JSON serialization of my tool list was failing because one tool had a null description. The LLM client would just close the connection with no error message. But my logs showed it failed at tools/list with a JSON mapping exception — 2 minutes debugging instead of 2 hours.
Authentication Logging: Because Everyone Puts the API Key in the Wrong Place
Remember that article I wrote about MCP authentication? I support four different locations for API keys because different clients expect different things.
Guess what? People still put the key in the wrong place. With logging, I can see exactly what happened:
@Component
public class McpAuthFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(McpAuthFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String apiKey = extractApiKey(request);
if (apiKey == null) {
log.warn("MCP_AUTH_MISSING: path={}, no API key found in any location",
request.getRequestURI());
response.sendError(HttpStatus.UNAUTHORIZED.value(), "API key required");
return;
}
if (!apiKeyService.isValid(apiKey)) {
log.warn("MCP_AUTH_INVALID: apiKeyPrefix={}",
apiKey.length() > 4 ? apiKey.substring(0, 4) + "..." : "too-short");
response.sendError(HttpStatus.UNAUTHORIZED.value(), "Invalid API key");
return;
}
log.debug("MCP_AUTH_SUCCESS: apiKeyPrefix={}",
apiKey.length() > 4 ? apiKey.substring(0, 4) + "..." : "too-short");
filterChain.doFilter(request, response);
}
}
Never log the full API key. That's how secrets leak into logs. Just log the first 4 characters so you can tell which key it was, that's all you need.
I had this issue last week where someone was trying to connect with my server and it kept saying 401. They swore they put the key in the right place. I checked the logs — "MCP_AUTH_MISSING". So the key wasn't coming through at all. Turns out they were putting it in the wrong header name. Fixed in 5 minutes. Without logging? We'd still be going back and forth guessing.
The CORS Logging That Saved My Weekend
If you expose your MCP server over the internet (which I do, so I can use it from multiple clients), you need CORS. And if you've done any web development, you know CORS is just one gotcha after another.
The worst gotcha with MCP and CORS? The preflight OPTIONS request doesn't include authentication, and if you reject it, the whole connection fails silently.
I wrote about this in my error handling article — but the key point is: log your CORS preflight checks.
@Configuration
public class CorsConfig implements WebMvcConfigurer {
private static final Logger log = LoggerFactory.getLogger(CorsConfig.class);
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
log.info("CORS configured: allowing all origins with credentials");
}
}
That's it. And in my auth filter, I explicitly skip auth for OPTIONS:
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
log.debug("CORS_PREFLIGHT: skipping auth for OPTIONS");
filterChain.doFilter(request, response);
return;
}
Without that log line, I would've spent hours figuring out why preflight was failing. With it — I see immediately that we're skipping auth, everything's working as expected.
Pros & Cons of My Current Setup
Let's be honest — nothing's perfect. Here's what works, what doesn't, and what I'd do differently if I was starting over.
What Works (Pros)
✅ Low overhead: This is just a filter and some log statements. No extra services, no expensive operations. In production, the performance impact is negligible. I'm running on a $5/month VPS and I don't even notice it.
✅ Privacy by design: I don't log personal content, just metadata. My users' notes stay private, I still get all the debugging info I need.
✅ Works with everything: SLF4J works with every logging framework. You can use this with Logback, Log4j2, whatever. You can send logs to a file, to Elasticsearch, to Datadog — doesn't matter.
✅ Catches 90% of issues early: Most MCP problems are connection problems, bad tool calls, auth issues. This catches all of them immediately.
✅ Easy to add incrementally: You don't have to rewrite your whole app. Just add the filter to your MCP endpoints and you're 80% there.
What Doesn't Work (Cons)
❌ No request body logging: I don't log the full request body, which means sometimes I miss weird parsing issues. If I had it to do over, I'd add an option to log the full body when debugging locally only, never in production.
❌ No correlation across requests: I don't have request IDs yet. If you have concurrent requests, the logs from different requests can get interleaved. Easy fix — add a request ID to the MDC, I just haven't done it yet.
❌ No aggregation: Right now I just log to a file. If I had more traffic, I'd want to aggregate these logs in something like ELK or Grafana Loki so I can query by tool name, status code, etc. For my side project, file is fine. For production at scale, you'll want more.
❌ Still learning what to log: I'm still discovering what's useful. For example, I recently added logging for when the LLM asks for a tool that doesn't exist — that's been super helpful for seeing what the LLM is hallucinating.
if (!toolExists(request.getName())) {
log.warn("MCP_TOOL_NOT_FOUND: requestedTool={}, availableTools={}",
request.getName(), availableToolNames);
return errorResponse("Tool not found: " + request.getName());
}
That one line has already taught me that LLMs really like to hallucinate tool names that sound right but aren't actually what I defined.
The Hard Lesson I Learned the Expensive Way
Honestly? I avoided adding proper logging because it felt like overkill for a side project. "It's just me using this," I thought. "Why do I need all this enterprise stuff?"
I learned the hard way that even side projects need good observability. Because:
- You will forget how your own code works in six months: I guarantee it. When something breaks in six months, you'll thank me for the logs.
- Other people will use your server eventually: Even if it's just one friend, when they have a problem, you need to debug it.
- Different clients behave differently: What works for Claude Desktop might not work for your custom client, might not work for Cursor, might not work for whatever new MCP client comes out next month.
I spent more time debugging without logs than I spent adding proper logging. That's the irony. The 4 hours I put into adding structured logging saved me more than 40 hours of debugging already.
Your Action Items: What to Add Today
You don't need to boil the ocean. If you're building an MCP server right now, just add these three things today:
- A request/response filter that logs every MCP request with method, path, duration, status.
- Specific logging for tools/list and tools/call — log the tool name, whether it succeeded.
- Authentication logging — log when API key is missing or invalid, never log the full key.
That's it. That's 100 lines of code. That's an hour of work. And it will save you so much pain later.
If you want to see the full implementation, check out my Papers repository on GitHub — it's all open source, you can browse the code and copy whatever you need.
What's Your Experience?
I've been building MCP servers for a few months now, and I'm still learning what good observability looks like for this new pattern. MCP is different from regular REST APIs — it's a different flow, different failure modes, different debugging needs.
- Are you building an MCP server? What are you doing for logging?
- What's the weirdest debugging issue you've had with MCP that logging would've caught?
- Am I missing something obvious? Should I be logging more? Less? Something different entirely?
Drop a comment below and let me know — I'm always looking to learn from what other people are doing. And if you found this helpful, feel free to give the repo a star — it helps other people find it.
Happy building — and happy logging!
Top comments (0)