MCP Best Practices Checklist: 77 Articles Later, Here's What I Wish I Knew Before Building 10+ MCP Servers
Honestly, I can't believe it's been almost three months since I started building MCP servers for my projects. And if you count how many articles I've written about it... well, that's 77 articles. Yeah, 77. I may have gone a bit overboard. But hey, when you learn something the hard way 77 different times, you start to see patterns.
Let me back up. I built my first MCP server for my 1,847-hour knowledge base project called Papers. If you haven't heard of it, it's this side project I've been working on for six years that basically went from "amazing idea" to "99.4% ROI loss" to "reborn through MCP." It's a whole thing, you can read the other articles if you're into failure porn.
The point is: after building MCP servers for Papers, Capa-Java, Spatial Memory, AI-Tools... after hitting every possible error, fighting with CORS, debugging authentication, waiting 10 seconds for timeouts, going through three months of deployment headaches... I've compiled a checklist. And today I'm sharing it with you.
No fluff, just the stuff that'll save you hours of Googling and crying into your coffee. Let's go.
1. Authentication: Support All Four API Key Locations
I learned this the hard way. I spent three days wondering why my MCP server worked with Claude Desktop but not with Cursor, and why it worked with Cursor but not with another client I was testing.
Turns out... different MCP clients put the API key in different places:
- Some use
X-API-Keyheader - Some use
Authorization: Bearer {key} - Some put it in the query string as
api_key - Some use
apiKey(camelCase) instead ofapi_key(snake_case)
If you only support one, you're guaranteed to break someone. Your users don't care about your "standards" — they just want it to work.
Here's what I do now in Java Spring Boot:
@Component
public class McpAuthFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
// Try all four locations
String apiKey = req.getHeader("X-API-Key");
if (apiKey == null || apiKey.isEmpty()) {
apiKey = req.getHeader("Authorization");
if (apiKey != null && apiKey.startsWith("Bearer ")) {
apiKey = apiKey.substring(7);
}
}
if (apiKey == null || apiKey.isEmpty()) {
apiKey = req.getParameter("api_key");
}
if (apiKey == null || apiKey.isEmpty()) {
apiKey = req.getParameter("apiKey");
}
// Validate API key
if (!isValidKey(apiKey)) {
res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid API key");
return;
}
chain.doFilter(request, response);
}
private boolean isValidKey(String apiKey) {
// Your validation logic here
return validKeys.contains(apiKey);
}
}
One more critical thing: OPTIONS preflight requests for CORS don't include authentication. You need to skip auth for OPTIONS requests:
// In your CORS config or filter
if ("OPTIONS".equalsIgnoreCase(req.getMethod())) {
chain.doFilter(request, response);
return;
}
Trust me, if you forget this part, you'll spend half a day wondering why CORS is broken when everything looks correct.
Pros of this approach: Any client can connect, no matter where they put the key.
Cons: Query parameters are logged by some servers, which is less secure.
Verdict: Compatibility beats purity. If you're running a public service, still use headers. But for personal MCP servers where you're the only user, it's fine.
2. Error Handling: Never Return Empty Responses
Here's another thing I learned the hard way. Your tools can fail. That's okay. But if you return an empty response when something goes wrong... the client just hangs. Forever. Or until the timeout hits. Which feels like forever when you're debugging.
Always return human-readable error messages. Even when there are no results. Even when something goes wrong.
Bad:
{
"content": []
}
Good:
{
"content": [
{
"type": "text",
"text": "No results found for your search query 'xyz'. Try broadening your search terms."
}
]
}
Same thing for actual errors:
try {
// do something dangerous
return successResult();
} catch (Exception e) {
logger.error("Search failed", e);
return errorResult("Search failed: " + e.getMessage());
}
Another pro tip: For slow operations (like cold start that might take 10+ seconds), flush the buffer early if your framework allows it. This keeps the connection from timing out:
// In Spring Boot, you can flush the writer early
response.getWriter().flush();
3. Let the Framework Handle JSON. Never Build It Manually.
I saw this in some early MCP examples and tried it myself. Don't do it.
This is a crime:
// DON'T DO THIS
String json = "{\"content\": [{\"type\": \"text\", \"text\": \"" + userInput + "\"}]}";
One unescaped quote, one backslash that needs escaping, and your entire response is invalid JSON. The client will hang or crash, and you'll spend an hour trying to figure out why "it looks correct in the browser."
Let your JSON library handle serialization. Always. Even for simple responses.
In Java with Jackson:
// Define your classes once
public record McpResponseContent(String type, String text) {}
public record McpResponse(List<McpResponseContent> content) {}
// Then just
String json = objectMapper.writeValueAsString(
new McpResponse(List.of(new McpResponseContent("text", result)))
);
It's a few extra lines, but you'll never have a JSON escaping error again. Worth it. 100x worth it.
4. CORS: You're Gonna Need It. Here's a Working Config.
If you're running an MCP server that web-based clients will connect to... you need CORS configured correctly. And "correctly" isn't what you think.
Most importantly:
- Allow
OPTIONSmethod - Allow the headers clients send (
Content-Type,Authorization,X-API-Key) - Expose any headers you want clients to read
- Credentials allow if you're using cookies
Here's a working Spring Boot CORS configuration I use everywhere now:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*") // For development; restrict this in production
.allowedMethods("GET", "POST", "OPTIONS")
.allowedHeaders("Content-Type", "Authorization", "X-API-Key")
.exposedHeaders("Content-Type")
.allowCredentials(true);
}
}
In Node.js Express:
const cors = require('cors');
app.use(cors({
origin: '*', // restrict in production
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key']
}));
And remember what I said earlier — skip auth on OPTIONS requests. CORS will thank you.
5. Deployment: Start Simple, Add Complexity When You Need It
I've deployed MCP servers to five different platforms now: Heroku, Fly.io, Tencent Cloud Serverless, VPS, and used ngrok for development. Here's what I can tell you:
For development: Use ngrok. It's free for non-commercial, one command:
ngrok http 8080
Done. You've got a public HTTPS URL your MCP clients can connect to. Can't beat that.
For personal 24/7 projects: Fly.io is my current favorite. fly launch walks you through everything, automatic HTTPS, persistent volumes if you need them, costs like $2-5 a month for small projects. No surprises.
For low-traffic personal projects: Serverless works great. It's cheap (often free for low usage), but watch out for cold starts. If your user has to wait 10 seconds for a cold start, that's okay for personal use but bad for public apps.
For multiple services: VPS gives you full control, but you have to maintain it. More expensive, more work. Only go here if you need it.
Pro tip no matter where you deploy: Add a health check endpoint that returns 200 OK:
@RestController
public class HealthController {
@GetMapping("/health")
public String health() {
return "OK";
}
}
Your deployment platform can ping it to know if your app is still alive. Some platforms require it. Even if they don't, it's useful for debugging.
6. Content-Length: If Things Are Getting Truncated, Set It Explicitly
This one took me a while to debug. Some MCP clients don't handle chunked encoding properly. If your responses are getting cut off halfway through... explicitly set the Content-Length header.
In Java Spring Boot, you can do it like this:
byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
response.setContentLength(jsonBytes.length);
response.getOutputStream().write(jsonBytes);
Instead of writing the string directly, calculate the length first, set the header, then write the bytes. This avoids chunked encoding and makes everyone happy.
It's an extra couple lines, but if you're having problems with truncated responses, this fixes it 99% of the time.
7. Architecture: Keep It Simple. AI Does the Heavy Lifting Anyway.
This is the big one. This is the lesson that took me 1,847 hours and six years to learn.
Before MCP, I spent years building this complicated knowledge base with:
- Semantic search
- Embeddings generation
- Classification models
- Automatic tagging
- Custom ranking algorithms
All in the knowledge base itself. Thousands of lines of code.
After MCP? It's 150 lines total. I just expose three tools:
-
search_knowledge: takes query, returns matching text snippets -
get_note: takes note id, returns full note -
search_notes_by_tag: takes tag, returns notes with that tag
That's it. AI does all the heavy lifting now. It understands the query, it ranks the results, it synthesizes the answer. My knowledge base just provides the data through a standard interface.
Honestly, it's better than the complicated version. It's faster, it's simpler, it crashes less, and it works with every MCP-compatible AI client.
If you're building an MCP server right now, ask yourself: "Do I need this logic in my server, or can the AI handle it?"
Nine times out of ten, the AI can handle it. Keep your server simple.
My Personal Checklist Before I Ship Any MCP Server
I actually go through this every time now. Saves me so much time:
- [ ] Auth: Supports X-API-Key / Authorization / api_key / apiKey ✓
- [ ] CORS: OPTIONS requests skip auth, allowed headers include all three auth headers ✓
- [ ] Error handling: No empty responses — even "no results" has human text ✓
- [ ] JSON: All serialization done by library, never manually built ✓
- [ ] Slow operations: Flush buffer early for long requests ✓
- [ ] Health check:
/healthendpoint returns 200 OK ✓ - [ ] Content-Length: If using manual output, set explicit Content-Length ✓
- [ ] Testing: Tested with at least two different MCP clients (they behave differently!) ✓
- [ ] Simple: Did I remove logic that AI can do better? ✓
Pros & Cons of Building MCP Servers in 2026
Let me be completely honest with you — I'm a big fan of MCP, but it's not all rainbows and unicorns.
Pros:
- Standard protocol means one implementation works everywhere. This is huge. No more building custom integrations for every AI client.
- Privacy win: Your data stays on your server. AI only gets the specific snippets it needs for the current query. You don't have to upload all your notes to OpenAI or Anthropic.
- Simple is better: The architecture forces you to keep things simple, which is almost always good.
- Ecosystem is growing: More and more clients support MCP every month. It feels like the start of something big.
- It gave my 1,800-hour failed side project new life. That alone makes it worth it for me.
Cons:
- Ecosystem is still young. Different clients do things differently. You're going to have to handle the incompatibilities.
- Debugging can be frustrating. When something doesn't work, it's often not obvious if it's your server or the client.
- Not many production examples yet. A lot of what's out there is toy examples, not real world lessons like this.
- Your server needs to be publicly accessible. For development that means ngrok, for production that means hosting. Extra work.
Overall: If you've got a project that can benefit from AI integration, building an MCP server is absolutely worth doing today. Just expect some rough edges. The pattern is solid, even if the ecosystem is still maturing.
What's Next for Me?
I'm still over here writing articles about MCP because I'm still learning things. Every time I deploy a new MCP server, I hit something new that breaks. Which means there's another article worth writing.
The good news is that each new article gets a little shorter because the basics are solved. I can just point people to this checklist now instead of repeating everything.
If you're just starting out with MCP, I hope this checklist saves you the hours I spent debugging all this stuff. Building MCP servers is fun once you've got the basics sorted out.
Let's Talk
I'm curious: Have you built any MCP servers yet? What's the weirdest bug you hit? Did you go through the same pain I did with authentication and CORS, or did you find a smoother path?
Drop a comment below and let me know — I'd love to hear your stories. And if you found this checklist helpful, feel free to share it with other people building MCP servers. They'll thank you.
This article is part of my ongoing series about building MCP servers for real-world side projects. Check out Papers on GitHub to see the actual MCP server this checklist came out of.
Top comments (0)