DEV Community

KevinTen
KevinTen

Posted on

17 MCP Hard Lessons

17 MCP Hard Lessons

Honestly, I didn't expect to be here. Six months ago, I'd barely heard of the Model Context Protocol. Today? I've built 76 different MCP servers across four open source projects, spent weeks debugging weird issues, and learned more about MCP than I ever wanted to know.

So here's the thing — most MCP content you see online is either "hello world" tutorials or marketing hype. Nobody tells you about the weird edge cases, the subtle compatibility issues, or the mistakes that'll cost you three days of debugging.

I've made every mistake you can make. Let me save you the pain. These are the 17 hardest lessons I learned building MCP in production.


1. Different clients put API keys in different places

This is the mistake that cost me three full days of debugging. I implemented X-API-Key header authentication like any sensible person — and half the MCP clients I tested couldn't connect.

Turns out:

  • Some clients use X-API-Key header
  • Some use Authorization: Bearer <key>
  • Some put it in query param as api_key
  • Some put it in query param as apiKey

Lesson: Support all four. It's just a few lines of code, and compatibility will save you so much headache.

@Component
public class McpAuthFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;

        // Try all common locations
        String apiKey = httpRequest.getHeader("X-API-Key");
        if (apiKey == null || apiKey.isEmpty()) {
            apiKey = httpRequest.getHeader("Authorization");
            if (apiKey != null && apiKey.startsWith("Bearer ")) {
                apiKey = apiKey.substring(7);
            }
        }
        if (apiKey == null || apiKey.isEmpty()) {
            apiKey = httpRequest.getParameter("api_key");
        }
        if (apiKey == null || apiKey.isEmpty()) {
            apiKey = httpRequest.getParameter("apiKey");
        }

        if (!validateApiKey(apiKey)) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid API key");
            return;
        }

        chain.doFilter(request, response);
    }
}
Enter fullscreen mode Exit fullscreen mode

The catch: CORS preflight OPTIONS requests don't include authentication. Make sure you skip auth for OPTIONS:

if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) {
    chain.doFilter(request, response);
    return;
}
Enter fullscreen mode Exit fullscreen mode

2. Never return an empty response

I learned this the hard way when my MCP server returned nothing for an empty search result. The client just hung forever. No error message, no timeout, just... nothing.

Lesson: Always return human-readable text even when there are no results.

//  Bad
{
  "content": []
}

//  Good
{
  "content": [
    {
      "type": "text",
      "text": "No results found for your search query. Try different keywords."
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Clients expect something. Empty content confuses them. A friendly message is better than nothing.

3. Never manually build JSON — let your framework do it

One single unescaped double quote in my text content broke the entire JSON response. The client got a parse error and disconnected. I spent four hours trying to figure out why it was failing.

I was manually building JSON because "it's just a simple response." Stupid. So stupid.

Lesson: Always use your framework's JSON serializer. Always. Even for simple responses. The 30 seconds you save aren't worth the four hours of debugging.

// ❌ Bad - don't do this
String json = "{\"content\": [{\"type\": \"text\", \"text\": \"" + userInput + "\"}]}";

// ✅ Good - use your framework's ObjectMapper
String json = objectMapper.writeValueAsString(response);
Enter fullscreen mode Exit fullscreen mode

4. Set explicit Content-Length for compatibility

Some MCP clients can't handle chunked encoding properly. They cut off the response halfway if you don't tell them how big it's going to be.

Lesson: If your framework lets you, enable Content-Length. It avoids weird truncation issues.

In Spring Boot, you can configure this:

server.servlet.context-parameters.maxSwallowSize=-1
server.compression.enabled=false # Compression can mess with content-length in some cases
Enter fullscreen mode Exit fullscreen mode

It's an ugly edge case, but it's real. I've fought it.

5. For slow operations, flush headers early to avoid timeouts

Cold starts can take 10+ seconds. If you don't send anything until the response is ready, some proxies and clients will time out on you.

Lesson: If you expect a response might take more than a couple seconds, flush the headers early. Keep the connection alive.

In Java Servlet:

response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.flushBuffer(); // Flush headers early
// Now do your slow processing
Enter fullscreen mode Exit fullscreen mode

The client knows the connection is still alive, and won't time out waiting.

6. CORS is harder than you think

MCP clients run from browser apps and desktop apps. CORS is non-negotiable. But there are a couple gotchas:

  • Always allow * or your specific origins
  • Always handle OPTIONS requests correctly
  • Always expose the right headers
  • Remember: OPTIONS requests don't have auth

My working CORS config for Spring Boot:

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "POST", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(false)
                .maxAge(3600);
    }
}
Enter fullscreen mode Exit fullscreen mode

That's it. That works. I wish I had this three weeks ago.

7. Health check endpoints save your sanity

When something goes wrong, you need a quick way to check if the server is up. Add a simple GET /health endpoint that returns 200 OK.

@GetMapping("/health")
public ResponseEntity<String> health() {
    return ResponseEntity.ok("OK");
}
Enter fullscreen mode Exit fullscreen mode

Uptime monitors, load balancers, and you will thank me later.

8. Read timeouts matter more than you think

MCP requests can take time — AI searching, database queries, whatever. Set your read timeout to at least 30 seconds.

In Spring Boot:

server.connection-timeout=30000
server.servlet.session.timeout=30m
Enter fullscreen mode Exit fullscreen mode

I've had too many requests timeout at 10 seconds just because the default was too low.

9. Log everything when debugging — then turn most of it off

When you're trying to figure out why a client won't connect, you need all the logs: request method, headers, query params, the first 1000 characters of the body, response status.

Once it's working, you can turn down the logging. But when it's broken, verbose logging saves hours.

Pro tip: Log the User-Agent header. It tells you which client your users are trying to use. Helps a lot with compatibility debugging.

10. Test with multiple clients before you ship

Don't just test with the one client you use every day. Different clients behave differently.

Test at minimum:

  • Claude Desktop
  • VS Code MCP extension
  • One other client of your choice

If it works in all three, you're good. If it only works in one, you've got compatibility issues you need to fix.

I've shipped MCP servers that worked fine for me but failed for 50% of users because I only tested one client. Don't be me.

11. ngrok is your best friend for development

You need a public HTTPS URL to test MCP with remote clients. ngrok gives you that in one command:

ngrok http 8080
Enter fullscreen mode Exit fullscreen mode

Done. You get a public HTTPS URL immediately. No port forwarding, no DNS, no nothing.

It's free for development use, and it's been rock solid for me. Every MCP developer needs this.

12. Start simple with deployment — you can always scale later

Don't jump straight to Kubernetes. Don't over-engineer it.

For personal projects and side projects:

  • Fly.io is amazing — fly launch and you're done in 5 minutes. ~$2-5/month for small projects.
  • Heroku is fine for testing, but don't rely on it for 24/7 — free dynos sleep after 30 minutes.
  • Vercel/Netlify functions work if you're doing something simple, but watch out for cold starts.
  • Your existing VPS works great if you already have one.

I run all my MCP servers on Fly.io, and it's been rock solid. Three months, zero downtime.

13. Query parameters are less secure than headers, but compatibility trumps purity

Purists will tell you never put API keys in query parameters because they get logged. They're not wrong.

But if some clients only support query parameters, you have to support query parameters.

My take: If it's a personal/side project, compatibility is more important than perfect security. If you're building something for production with sensitive data, yeah — lock it down. But most of us are building MCP servers for our own AI assistants to use. It's okay.

14. Simple architectures work better in the MCP world

MCP changes everything. AI clients are already smart. Your server doesn't need to be smart.

I built a knowledge base with 2000 lines of complex AI search ranking. Then I MCP-ified it to just 150 lines — search returns text snippets, AI does the ranking and reasoning.

It works better now than it ever did before.

Lesson: Your MCP server just needs to expose data and tools. Let AI do the thinking. Simple wins.

My 150-line MCP knowledge server:

@RestController
@RequestMapping("/mcp")
public class McpController {

    private final KnowledgeService knowledgeService;
    private final ObjectMapper objectMapper;

    @PostMapping("/tools/call")
    public ResponseEntity<McpResponse> search(@RequestBody @Valid McpRequest request) {
        // Just get the query from parameters
        String query = (String) request.getParams().get("query");

        // Search returns raw text snippets
        List<String> results = knowledgeService.search(query, 5);

        // Return them — AI does the rest
        List<McpContent> content = new ArrayList<>();
        content.add(new McpTextContent("Found " + results.size() + " results:\n\n" + String.join("\n---\n", results)));

        return ResponseEntity.ok(new McpResponse(content));
    }

    @PostMapping("/tools/list")
    public ResponseEntity<ToolsList> listTools() {
        // One tool — search knowledge base
        // That's all you need
        return ResponseEntity.ok(ToolsList.single(
            "search_knowledge",
            "Search the knowledge base for relevant information",
            Map.of(
                "query", Map.of(
                    "type", "string",
                    "description", "Search query"
                )
            )
        ));
    }
}
Enter fullscreen mode Exit fullscreen mode

That's it. 150 lines. Works better than my 2000-line monster.

15. Privacy is MCP's superpower — don't give it up

The best thing about MCP is that your data stays on your server. AI only gets the specific bits it needs for the current query. You don't have to upload all your private notes to OpenAI or Anthropic.

Lesson: Design for privacy from day one. Keep user data on their infrastructure. Don't collect more than you need.

This is MCP's big advantage over hosted AI services. Lean into it.

16. The ecosystem is still young — expect roughness

MCP is exciting and moving fast. But it's not 1.0 yet.

You will find:

  • Clients with different behaviors
  • Incomplete documentation
  • Unexpected edge cases
  • Bugs on both the client and server side

That's okay. It's part of the territory when you're building on a new protocol. Just roll with it, document what you learn, and help improve the ecosystem.

I've had a lot of fun building with MCP despite the rough edges. It's the most interesting thing to happen to AI integration in a long time.

17. Start small — you can iterate faster

Don't try to build the perfect MCP server with 20 tools on day one. Start with one tool. Get it working. Test it with multiple clients. Fix the compatibility issues. Then add more.

I started with one tool for my knowledge base (search_knowledge). It worked. Then I added get_document, then create_note, etc.

Incremental wins beat big-bang rewrites every time.


Pros and Cons: The Honest Truth About Building MCP Servers

After building 76 of them, here's what I really think:

✅ Pros

  1. Once and done: Implement once, works with every MCP-compatible client. No more custom integrations for every AI assistant.
  2. Privacy by design: Data stays where it belongs — on your infrastructure.
  3. Simple is better: You don't need to build AI into your service anymore. Just expose tools/data, AI does the rest.
  4. Growing ecosystem: More and more clients support MCP every month. It feels like the start of something big.
  5. It's actually fun: MCP changes how you think about AI integration. It's refreshing.

❌ Cons

  1. Fragmentation: Different clients do things differently. Compatibility is work.
  2. Immaturity: Docs are incomplete, best practices are still being written. You're a pioneer whether you want to be or not.
  3. Debugging can be painful: When something goes wrong, it's not always clear who's at fault — client or server.
  4. Your server needs to be publicly accessible: Local development is easy with ngrok, but production needs hosting. That's operational overhead.

Should You Build an MCP Server?

If you have data or tools that you want your AI assistant to access — yes. Do it. Even with the rough edges, it's better than any alternative I've tried.

Just start small. Follow these lessons. Avoid the mistakes I made. You'll save yourself weeks of debugging.

Me? I'm going to keep building MCP servers. It's transformed my 1,800-hour failed knowledge base project into something actually useful again. That alone makes it worth it for me.


What's Your Experience?

I've built 76 MCP servers and I'm still learning. Have you built an MCP server? What's the hardest lesson you learned? Did you run into different compatibility issues than I did? Drop a comment and share your experience — I'd love to hear it.


If you found this helpful, check out my open source knowledge base project Papers on GitHub — it's a working MCP server you can look at for reference: https://github.com/kevinten10/Papers

Top comments (0)