MCP Server Design Lessons: Why My 1,800-Hour Knowledge Base Finally Makes Sense After MCP Optimization
Honestly, I didn't see this coming.
Six years ago, I started building Papers — my "perfect" personal knowledge management system. The vision was grand: AI-powered semantic search, automatic tagging, recommendation engine, everything you'd expect from a modern knowledge base. I poured 1,847 hours of my free time into it. I saved 2,847 articles. And today? I use it about 15 minutes a day. That's a 0.05% efficiency rate. And a net ROI of -$112,090.
Yeah, you read that right. Minus $112k.
But here's the thing — six months ago, I discovered the Model Context Protocol (MCP). And something clicked. My "failed" knowledge base suddenly became useful again. Not because I added more AI. Because I removed almost all the AI.
Let me show you what happened, what I learned, and why MCP might change how you think about building AI-ready tools.
The Old Way: AI Everywhere, Nothing Worked
Before MCP, I believed the lie: "Your knowledge base needs to be smart. It needs to understand your content, connect the dots, make recommendations."
So I built all that:
// This is what my KnowledgeService looked like in 2023 — 2000+ lines of pain
@Service
public class ComplexAIDrivenKnowledgeService {
private final EmbeddingService embeddingService;
private final SemanticSearch semanticSearch;
private final TagRecommendationEngine recommendationEngine;
private final KnowledgeGraphConnector graphConnector;
private final PersonalizationEngine personalizationEngine;
public SearchResult search(String query) {
// 1. Embed the query
Embedding queryEmbedding = embeddingService.embed(query);
// 2. Semantic search against 2000+ documents
List<ScoredDocument> scored = semanticSearch.search(queryEmbedding, 10);
// 3. Recommend tags based on query
List<String> tags = recommendationEngine.recommend(query);
// 4. Connect to knowledge graph for related concepts
List<Concept> related = graphConnector.findRelated(tags);
// 5. Personalize results based on my reading history
List<Document> personalized = personalizationEngine.rank(scored, related);
// 6. Return everything... that takes 3-7 seconds
return new SearchResult(personalized, tags, related);
}
}
Sounds impressive, right? It wasn't.
The average search took 4.2 seconds. The AI recommendations had a 0.2% click-through rate. I almost never used the knowledge graph. And half the time, the semantic search returned worse results than just... plain old text search.
I'd overengineered the hell out of it. Because I thought the system needed to be smart.
The MCP Epiphany: AI Is Already In The Client
MCP changed everything. But not because it gave me better AI. Because it made me realize something obvious:
The AI client is already smart. You don't need AI in your server.
Think about it. When you use Claude Desktop, VS Code with GitHub Copilot, or any MCP-compatible client — the AI is already there. It can understand natural language. It can connect dots. It can synthesize information. All you need to do is give it the relevant documents.
That's it.
So I rewrote everything. Here's what my MCP-optimized knowledge service looks like now:
// 2026 — MCP-optimized. 50 lines. Works better.
@Service
public class SimpleMcpKnowledgeService {
private final List<KnowledgeItem> allItems;
public SimpleMcpKnowledgeService() {
// Load all knowledge items from JSON on startup
this.allItems = loadAllItemsFromDisk();
}
// MCP tool: search knowledge base
public List<KnowledgeItem> search(String query) {
String lowerQuery = query.toLowerCase();
// That's it. That's the search.
List<KnowledgeItem> results = allItems.stream()
.filter(item ->
item.getTitle().toLowerCase().contains(lowerQuery) ||
item.getContent().toLowerCase().contains(lowerQuery) ||
item.getTags().stream().anyMatch(t -> t.toLowerCase().contains(lowerQuery))
)
.limit(10)
.toList();
// Return plain text content. AI does the rest.
return results;
}
// MCP tool: get specific article by ID
public KnowledgeItem getArticle(String id) {
return allItems.stream()
.filter(item -> item.getId().equals(id))
.findFirst()
.orElse(null);
}
// MCP tool: list recent articles
public List<KnowledgeItem> listRecent(int count) {
return allItems.stream()
.sorted(Comparator.comparing(KnowledgeItem::getCreatedAt).reversed())
.limit(count)
.toList();
}
}
That's it. 50 lines. No embeddings. No semantic search. No recommendation engine. Just... string.contains().
And the MCP server controller? Super simple. You just need to implement two endpoints: tools/list and tools/call.
@RestController
@RequestMapping("/mcp")
public class McpServerController {
private final SimpleMcpKnowledgeService knowledgeService;
private final ObjectMapper objectMapper;
public McpServerController(SimpleMcpKnowledgeService knowledgeService, ObjectMapper objectMapper) {
this.knowledgeService = knowledgeService;
this.objectMapper = objectMapper;
}
// MCP endpoint: list all available tools
@PostMapping("/tools/list")
public ResponseEntity<McpToolsResponse> listTools() {
List<McpTool> tools = List.of(
McpTool.builder()
.name("search_knowledge")
.description("Search my personal knowledge base for articles matching a query")
.inputSchema(buildInputSchema(
Map.of("query", Map.of("type", "string", "description", "Search query"))
))
.build(),
McpTool.builder()
.name("get_article")
.description("Get full article content by ID")
.inputSchema(buildInputSchema(
Map.of("id", Map.of("type", "string", "description", "Article ID"))
))
.build(),
McpTool.builder()
.name("list_recent")
.description("List most recent articles")
.inputSchema(buildInputSchema(
Map.of("count", Map.of("type", "integer", "description", "Number of articles to return", "default", 10))
))
.build()
);
return ResponseEntity.ok(new McpToolsResponse(tools));
}
// MCP endpoint: call a tool
@PostMapping("/tools/call")
public ResponseEntity<McpCallResponse> callTool(@RequestBody McpCallRequest request) {
String toolName = request.getName();
Map<String, Object> args = request.getArguments();
try {
Object result = switch (toolName) {
case "search_knowledge" -> {
String query = (String) args.get("query");
yield knowledgeService.search(query);
}
case "get_article" -> {
String id = (String) args.get("id");
yield knowledgeService.getArticle(id);
}
case "list_recent" -> {
int count = args.get("count") != null ? (int) args.get("count") : 10;
yield knowledgeService.listRecent(count);
}
default -> throw new IllegalArgumentException("Unknown tool: " + toolName);
};
// Return the result as JSON — AI client handles the rest
return ResponseEntity.ok(McpCallResponse.success(result));
} catch (Exception e) {
return ResponseEntity.ok(McpCallResponse.error(e.getMessage()));
}
}
// Helper to build JSON Schema for tool inputs
private JsonSchema buildInputSchema(Map<String, Object> properties) {
// Omitted for brevity — basically just build a JSON Schema object
return new JsonSchema("object", properties);
}
}
That's the entire MCP server. Two endpoints. Three tools. Less than 150 lines of code.
And it works better than the 2000-line AI monster I had before.
Why This Actually Works (Counterintuitive Lessons)
I know what you're thinking: "Wait, that's it? No fancy AI?" Yeah. That's it. And here's why it works:
1. The AI Already Lives In The Client
When I use Claude Desktop with my MCP server, Claude does the understanding. I don't need my server to understand the query. I just need to get the relevant documents into Claude's context. Claude already knows how to synthesize, connect dots, answer questions.
My server doesn't need to be smart. It just needs to be a good data provider. That's it.
2. Simpler = Faster = More Useful
Before: 3-7 seconds per search. Now: 50ms. That's 60x faster. The difference is night and day. When search is instant, you use it more. When it's slow, you avoid it. That's just human nature.
I doubled my usage in the first month just because it's fast.
3. Privacy By Default
Because all the intelligence is in the client, my data stays on my server. I don't need to upload all 2,847 articles to some third-party embedding service. Claude only gets the specific articles I'm searching for right now. The rest stays private.
That's a huge win for privacy. And it's free. I don't pay for 2,847 embeddings every time I update something.
4. Standard Protocol Means It Works Everywhere
Once I implemented MCP, it just works with every MCP-compatible client. Claude Desktop. VS Code. Any future client that supports MCP. I don't need to build custom integrations for each one. Implement once, use everywhere.
That's the power of standards. I implemented MCP in an afternoon. And now it works everywhere.
The Honest Pros & Cons
Let me be real with you — this approach isn't for everyone. Here's what works, and what doesn't.
Pros ✅
- Blazing fast: 50ms vs 3-7 seconds. Night and day difference.
- Dead simple to implement: I rewrote the whole thing in a weekend.
- Privacy friendly: Your data stays on your server. Only what you need gets sent.
- Cheap to run: No expensive embedding API calls. No GPU required.
- Works with all MCP clients: One implementation, everywhere.
- Actually gets used: Faster = more usage = more value. The ironic part is my "stupid" simple system gets used more than my "smart" complex one.
- Evolves with you: Add new tools as you need them. No big bang rewrite.
Cons ❌
- MCP ecosystem is still young: Not every AI client supports MCP yet. You need a compatible client.
- Requires your server to be accessible: If you want to use it from anywhere, your MCP server needs to be reachable. Local development works with ngrok, but 24/7 use needs hosting.
- No server-side "understanding": If your client isn't smart, this approach falls apart. But that's kind of the point — you let the client do what it's good at.
-
Basic search only:
string.contains()isn't perfect. Sometimes you get false positives. But honestly? The AI client filters that out better than my old semantic search did. - Scaling questions: If you have 100,000 articles, loading everything into memory might not work. But for personal use (thousands of articles), it's totally fine.
When Should You Use This Approach?
Based on my experience, this MCP-optimized "keep it simple stupid" architecture works great when:
- You're building a personal tool or small service
- Your data stays relatively small (under 10,000 items)
- You're connecting to an AI client that already has strong reasoning capabilities
- Privacy is important to you
- You don't want to manage complex infrastructure
It's less ideal when:
- You're building a public service for thousands of users
- You have massive amounts of data that needs indexing at scale
- You need to support non-MCP clients
But for personal knowledge bases, internal tools, connectors that expose your data to AI — this is gold.
What I Learned The Hard Way
I spent six years overengineering a knowledge base because I bought into the hype: "Every system needs AI. Every component needs to be smart."
MCP taught me the opposite:
In the AI client era, your server doesn't need to be smart. It just needs to be available. It just needs to expose your data through a standard protocol. The AI client already does the heavy lifting.
My 1,800-hour "failed" project is actually useful now. Not because I added more AI. Because I removed almost all the AI and let the client do what it's good at.
The ironic part? The system that took me six years to build only became useful after I threw away 90% of the code.
So if you have an old project that's not working out, maybe try this. Strip away all the complexity. Expose your data via MCP. Let the AI client handle the smarts.
You might be surprised like I was.
Your Turn
Have you tried building an MCP server? Are you working on a knowledge management system that feels overengineered? Did you have a similar experience where simpler ended up being better after AI became ubiquitous?
I'd love to hear your thoughts in the comments below. What's your MCP design philosophy?
Top comments (0)