MCP Logging: What I Wish I Knew Before Deploying My Production MCP Server (3 Weeks of Production Pain) A developer who built over 10 MCP servers over three months discovered that production MCP servers require different logging than regular REST APIs after spending 8 hours debugging random failures. The engineer found that standard SLF4J logging missed issues like reverse proxy size limits dropping connections before requests reached the application. The solution was a custom logging filter that captures every request before processing, logs request IDs, API key presence, and warns on slow requests exceeding 10 seconds. Honestly, I built 10+ MCP servers over the past 3 months, and I thought I had everything figured out. Authentication? Check. Error handling? Check. Rate limiting? Check. Deployment? Check. Then my production server started failing randomly. Users would report "empty responses" or "timeout" but when I checked locally, everything worked fine. No errors in the console, nothing in the logs—just… nothing. I spent 8 hours debugging over three days, and I learned the hard way: MCP servers need different logging than your regular REST API. In this post, I want to share what I got wrong, what fixed it, and the exact logging setup I use now that actually catches those weird MCP-specific failures. Let me start with a confession: I used the same logging setup I use for regular Spring Boot REST APIs. SLF4J, INFO level for requests, errors go to error log. That works fine for most APIs. But MCP is different. With MCP: What was happening to me? Some requests were hitting size limits in my reverse proxy, the proxy would drop the connection, and I'd never see it in my logs because the request never reached my app. The client retries, same thing happens again. User gets frustrated, I sit there scratching my head because "it works on my machine". So here's what I changed—everything. First, I added a filter that logs every single request before it hits my controller. Even if the request never makes it through, I have a log entry. That sounds obvious, but I used to log after authentication. Wrong—if authentication fails silently or the filter chain breaks, you get nothing. Here's the code I use 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 { long startTime = System.currentTimeMillis ; String requestId = UUID.randomUUID .toString ; String apiKey = extractApiKey request ; // Log BEFORE processing—even if everything fails, we have this log.info "MCP REQUEST START | requestId={} | method={} | path={} | apiKeyPresent={} | remoteAddr={}", requestId, request.getMethod , request.getRequestURI , apiKey = null, request.getRemoteAddr ; try { wrapRequestLogging request, response, filterChain, requestId, apiKey, startTime ; } catch Exception e { log.error "MCP REQUEST ERROR | requestId={} | error={}", requestId, e.getMessage , e ; throw e; } } private void wrapRequestLogging HttpServletRequest request, HttpServletResponse response, FilterChain filterChain, String requestId, String apiKey, long startTime throws IOException, ServletException { ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper request ; ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper response ; filterChain.doFilter wrappedRequest, wrappedResponse ; long duration = System.currentTimeMillis - startTime; int status = wrappedResponse.getStatus ; int responseSize = wrappedResponse.getContentSize ; // Log after processing—everything completed, capture size and status log.info "MCP REQUEST COMPLETE | requestId={} | status={} | durationMs={} | responseSize={} | apiKey={}", requestId, status, duration, responseSize, maskApiKey apiKey ; // Log WARN for slow requests—helps catch timeout issues before users report if duration 10000 { log.warn "MCP REQUEST SLOW | requestId={} | durationMs={} | this might cause client retry", requestId, duration ; } wrappedResponse.copyBodyToResponse ; } private String extractApiKey HttpServletRequest request { // Check all four locations I learned this the hard way in my auth article String apiKey = request.getHeader "X-API-Key" ; if apiKey = null return apiKey; String auth = request.getHeader "Authorization" ; if auth = null && auth.startsWith "Bearer " { return auth.substring 7 ; } apiKey = request.getParameter "api key" ; if apiKey = null return apiKey; return request.getParameter "apiKey" ; } private String maskApiKey String apiKey { if apiKey == null || apiKey.length < 8 return null; // Show first 4 chars, mask the rest—good enough for debugging, still secure return apiKey.substring 0, 4 + " "; } } Key things I do differently here: MCP really only has two endpoints you need to care about: tools/list and tools/call . Each has different failure modes, so I added structured logging specifically for them. For tools/list , I log how many tools are being returned: @RestController @RequestMapping "/mcp" public class McpController { private static final Logger log = LoggerFactory.getLogger McpController.class ; private final List