Building a Unified MCP Knowledge Server: How I Connect 6 Different Knowledge Bases to Claude Desktop (And What Broke)
Honestly, I didn't think this would work.
After 76 articles about my 1,800-hour failed knowledge base project called Papers, I finally tried something crazy: connect six completely different knowledge sources through a single MCP server to Claude Desktop. Everything from Markdown notes to Confluence pages to Jira tickets to GitHub issues.
The result? It actually works better than I expected. But not without some spectacular breakage along the way.
Let me walk you through what I built, what I broke, and what I learned. If you're thinking about building your own MCP knowledge server, this is the stuff nobody tells you upfront.
The Problem: My Knowledge Was Already Everywhere
So here's the thing โ I'm a hoarder of knowledge. I've got notes everywhere:
- Personal Markdown notes (~2,800 files in Papers)
- Company Confluence (internal docs that can't leave the network)
- Jira tickets (engineering project planning)
- GitHub issues (open source project tracking)
- Google Drive docs (shared meeting notes)
- Bookmarks (articles I saved for later)
Before MCP, if I wanted Claude to answer a question that spanned multiple sources, I had to manually copy-paste from each place. Painful. Slow. I'd forget half the stuff I knew even existed.
I figured MCP could fix this. One standardized interface. Claude asks the questions. My server finds the knowledge. Everyone's happy.
Right?
The Architecture: One Server, Multiple Providers
The architecture ended up being simpler than I thought. MCP doesn't care where your data comes from โ it just cares that you implement tools/list and tools/call correctly.
Here's what the core Java Spring Boot implementation looks like:
// KnowledgeProvider.java - common interface for all sources
public interface KnowledgeProvider {
String getName();
List<KnowledgeResult> search(String query, int limit);
KnowledgeResult getById(String id);
boolean isAvailable();
}
// Multiple providers registered in Spring
@Configuration
public class KnowledgeConfig {
@Bean
public List<KnowledgeProvider> knowledgeProviders(
MarkdownKnowledgeProvider markdown,
ConfluenceKnowledgeProvider confluence,
JiraKnowledgeProvider jira,
GitHubKnowledgeProvider github,
GoogleDriveKnowledgeProvider drive,
BookmarkKnowledgeProvider bookmarks
) {
return List.of(markdown, confluence, jira, github, drive, bookmarks);
}
}
Then the main MCP controller just delegates to all providers and aggregates the results:
// MCPServerController.java
@RestController
@RequestMapping("/mcp")
public class MCPServerController {
private final List<KnowledgeProvider> providers;
private final KnowledgeSearchService searchService;
public MCPServerController(List<KnowledgeProvider> providers,
KnowledgeSearchService searchService) {
this.providers = providers;
this.searchService = searchService;
}
@PostMapping("/tools/call")
public McpResponse callTool(@RequestBody McpRequest request) {
String toolName = request.getToolName();
Map<String, Object> args = request.getArguments();
if ("search_knowledge".equals(toolName)) {
String query = (String) args.get("query");
int limit = args.containsKey("limit") ? (int) args.get("limit") : 10;
// Search across ALL providers in parallel
List<KnowledgeResult> allResults = providers.stream()
.filter(KnowledgeProvider::isAvailable)
.flatMap(p -> p.search(query, limit).stream())
.sorted(Comparator.comparingDouble(KnowledgeResult::getScore).reversed())
.limit(limit)
.toList();
return McpResponse.text(searchService.formatResults(allResults));
}
if ("get_knowledge".equals(toolName)) {
String id = (String) args.get("id");
String providerName = parseProviderFromId(id);
KnowledgeProvider provider = findProvider(providerName);
KnowledgeResult result = provider.getById(id);
return McpResponse.text(result.getContent());
}
return McpResponse.error("Unknown tool: " + toolName);
}
}
That's basically it. Around 150 lines of code. The whole thing is dead simple.
What Broke: Six Lessons I Learned The Hard Way
I thought the main challenges would be around authentication or search accuracy. Nope. The problems that broke me were completely unexpected.
1. Context Window Hell: Your Results Eat Claude's Context
Honestly, this one caught me by surprise. I figured "more results = better answers." Wrong.
When you search across six knowledge providers, you can easily get 50+ results. If you send back full content for each, you've just eaten 50k tokens of Claude's 100k context window. Claude can't answer your question because it's drowning in results.
The fix: Only send back snippets + metadata in search. Let Claude explicitly ask for the full content of the results it actually cares about.
// Before: Bad - send full content
public record KnowledgeResult(
String id,
String title,
String fullContent, // ๐ eats context for breakfast
double score,
String provider
) {}
// After: Good - send snippet only, let Claude fetch full content if needed
public record KnowledgeResult(
String id,
String title,
String snippet, // ๐ 1-2 sentences max
int wordCount, // ๐ tell Claude how big it is
double score,
String provider
) {}
Now Claude can scan the search results, pick the 2-3 that look relevant, and fetch only those. Saves so much context. Game changer.
2. Different Search Patterns Need Different Tools
At first, I only had one tool: search_knowledge. It worked for simple queries but failed miserably for more complex questions.
The problem? Different knowledge sources need different search approaches. Searching Jira tickets isn't the same as searching Markdown notes.
The fix: Expose multiple tools, one for each source type. Let Claude decide which one to use:
-
search_all_knowledge- across everything (good for broad questions) -
search_personal_notes- only my Markdown files -
search_confluence- only internal docs -
search_jira- filter by project/status too -
search_github- issues and PRs
Turns out Claude is pretty good at picking the right tool for the question. If I ask "What was the conclusion from the last performance meeting?", Claude will automatically search Confluence. If I ask "What did I write about MCP error handling?", it searches personal notes.
3. CORS Preflight Needs Special Handling For Authentication
Remember how I wrote about MCP authentication in the last article? This one is a specific pain point that bit me again.
When connecting from Claude Desktop, it sends an OPTIONS preflight request before your actual request. And guess what? It doesn't send the API key in the preflight.
If your filter rejects unauthenticated OPTIONS requests, the whole connection fails. CORS breaks. Claude can't connect at all.
The fix: Always allow unauthenticated OPTIONS requests for CORS:
// McpCorsConfig.java - Java Spring Boot example
@Configuration
public class McpCorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/mcp/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("*");
}
}
// McpAuthFilter.java - skip auth for OPTIONS
@Component
public class McpAuthFilter extends OncePerRequestFilter {
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
// Allow OPTIONS without authentication
return HttpMethod.OPTIONS.name().equals(request.getMethod());
}
@Override
protected void doFilterInternal(...) {
// Validate API key here for actual requests
}
}
That's it. Three lines of configuration fixed this. Saved me three hours of debugging why Claude Desktop couldn't connect even though Postman worked fine.
4. Third-Party Rate Limits Will Kill You
When you're pulling data from GitHub, Google Drive, Confluence โ all of them have rate limits. If you search in parallel across all providers, you can hit those limits faster than you think.
I once did a search that hit my GitHub rate limit in 30 seconds. Had to wait an hour before it worked again. Not great.
The fix: Add per-provider rate limiting and graceful degradation. If one provider is rate limited, still return results from the others instead of failing the whole search.
// RateLimitedKnowledgeProvider.java - wrapper example
public class RateLimitedKnowledgeProvider implements KnowledgeProvider {
private final KnowledgeProvider delegate;
private final RateLimiter rateLimiter;
private final AtomicInteger concurrentRequests;
private final int maxConcurrent;
public RateLimitedKnowledgeProvider(KnowledgeProvider delegate,
int requestsPerMinute,
int maxConcurrent) {
this.delegate = delegate;
this.rateLimiter = RateLimiter.create(requestsPerMinute);
this.concurrentRequests = new AtomicInteger(0);
this.maxConcurrent = maxConcurrent;
}
@Override
public List<KnowledgeResult> search(String query, int limit) {
if (!rateLimiter.tryAcquire() || concurrentRequests.get() >= maxConcurrent) {
// Rate limited - return empty, don't fail the whole search
return List.of();
}
try {
concurrentRequests.incrementAndGet();
return delegate.search(query, limit);
} finally {
concurrentRequests.decrementAndGet();
}
}
}
Graceful degradation is your friend. Users would rather get some results than no results because one API was rate limited.
5. Content Formatting Needs To Be MCP-Friendly
MCP responses are just text. But when you pull content from different sources, they all have different formatting:
- Confluence uses Storage Format (HTML)
- Jira uses Wiki markup
- Markdown is... well, markdown
- Google Docs export to plain text
If you just send raw HTML to Claude, it still works โ but it gets confused by all the divs and classes and inline styles. It starts paying attention to the markup instead of the content.
The fix: Normalize everything to common Markdown before sending it to MCP. I wrote a simple converter for each source:
// HtmlToMarkdown.java - using commonmark-java
public class HtmlToMarkdown {
public static String convert(String html) {
// Remove script/style tags
String cleaned = html.replaceAll("<script[^>]*>.*?</script>", "")
.replaceAll("<style[^>]*>.*?</style>", "");
// Convert headings, links, lists to markdown
// ... you get the idea
return cleaned;
}
}
Clean Markdown is smaller, cleaner, and Claude understands it perfectly. No cruft.
6. Some Providers Just Don't Add Value (And That's OK)
After building all six providers, I did a month of real usage. Guess what? Two of them barely ever get used.
My Google Drive integration works technically, but I almost never ask questions about my Google Drive docs. Most of that stuff is meeting notes from months ago that I don't need anymore. Same with bookmarks โ I use them for reading later, but rarely need to search them through Claude.
The fix: Don't feel like you need to connect everything. Start with the sources you actually search daily. Add more only when you actually need them.
My current usage breakdown after a month:
- Personal Markdown notes: 65% of searches
- Confluence: 20%
- Jira: 10%
- GitHub Issues: 4%
- Everything else: 1% combined
Honestly, if I had to do it again, I would have started with just personal notes + Confluence. Added Jira later. Skipped the others until I actually needed them. Saved a week of work.
Pros & Cons: Let's Be Honest
Pros โ
It just works โ Once you get past the initial setup headaches, Claude Desktop just works with all your knowledge. Ask a question, it searches automatically, it fetches what it needs, it answers. Feels magical.
Privacy first โ All your data stays on your server. Claude only gets the specific snippets it asks for. You don't have to upload your entire knowledge base to the cloud. Perfect for internal company docs that can't leave your network.
Standard protocol = future proof โ Once you implement MCP once, it works with every MCP-compatible client. Claude Desktop already supports it. Other clients are adding support. You don't have to re-integrate for every new AI assistant.
Composable โ Want to add a new knowledge source? Just implement the
KnowledgeProviderinterface. Done. No need to change anything else.
Cons โ
You still need to host it โ Your MCP server needs to be publicly accessible for Claude Desktop to connect. If you're running it locally, you need ngrok for development. For 24/7 access, you need to host it somewhere. I wrote about deployment options in the last article.
No built-in caching โ Every search hits your live APIs. If your Confluence is slow, your search is slow. You need to add your own caching. I cache search results for 15 minutes, which works pretty well for my use case.
Rate limits are real โ See above. If you're integrating third-party APIs, you will hit rate limits eventually. Plan for it.
MCP is still young โ The protocol is evolving. Some client behaviors are a bit rough around the edges. Expect things to change. That said, it's already completely usable for personal projects.
Would I Do It Again?
Absolutely.
After six years of building this over-engineered knowledge base that basically nobody used (including me), MCP finally made it useful. Every day I use Claude Desktop to answer questions about my own knowledge. It's like having a research assistant that's read all my notes.
The crazy part? It only took 150 lines of code to make it work once I had the knowledge providers. That's it. All the hard work was done over the past six years collecting the knowledge. MCP just connected it to AI.
The biggest lesson I've taken away from all this MCP experimentation:
AI doesn't need your knowledge to be "AI-ready" with fancy embeddings and vector databases and all that junk you read about on Hacker News.
It just needs a standard way to search and retrieve. AI does the understanding. You just need to give it access to your stuff.
My entire search implementation is basically string.contains() on steroids. No fancy vector embeddings. No expensive LLM reranking. Just simple full-text search across all my notes. And it works better than the fancy 2000-line implementation I built six years ago.
Simple wins. Again.
Your Turn
Have you tried building an MCP server that aggregates multiple knowledge sources? What surprised you? Did you run into the same CORS/preflight problems I did, or did you find a different issue?
Drop a comment below and share your experience. I'm still learning this stuff too, and I'd love to hear what worked for you.
And if you want to see the full code, check out the project on GitHub ๐
https://github.com/kevinten10/Papers
Happy building!
Top comments (0)