Building a Custom RAG MCP Server for Claude with Firecrawl (2026)
A custom RAG MCP server gives Claude access to a private knowledge base built from your crawled web content. Firecrawl crawls URLs → markdown → chunk → embed → store in ChromaDB or pgvector. Your MCP server exposes a tool that queries the vector store and returns relevant chunks. Claude uses them to answer questions with your data. Start with Firecrawl.
What a RAG MCP server does
Without RAG, Claude relies only on its training data and live tools (e.g., Firecrawl's scrape/crawl). A custom RAG MCP server adds a third source: your own indexed content.
Flow:
- Ingest — Firecrawl crawls your docs, product pages, or competitor sites.
- Process — Chunk markdown, embed with OpenAI (or similar), store in a vector DB.
- Expose — MCP server tool
query_knowledge_baseaccepts a question and returns top-k chunks. - Use — Claude calls the tool when the user asks about your domain; it grounds answers in your data.
Use cases: internal documentation, product knowledge bases, competitor research, support FAQs.
Architecture
Firecrawl crawl → Markdown → Chunk → Embed → Vector DB (Chroma/pgvector)
↓
Claude Desktop ← MCP Server ← query_knowledge_base(question)
Firecrawl handles crawling and clean markdown. You handle chunking, embedding, and storage. The MCP server is a thin wrapper that queries the vector store and returns formatted chunks. See Firecrawl RAG knowledge base for the full ingestion pipeline.
Step-by-step build
Step 1: Crawl with Firecrawl
import Firecrawl from '@mendable/firecrawl-js';
const firecrawl = new Firecrawl({ apiKey: process.env.FIRECRAWL_API_KEY });
const job = await firecrawl.crawl('https://docs.yourproduct.com', {
limit: 50,
scrapeOptions: { formats: ['markdown'], onlyMainContent: true },
pollInterval: 2,
timeout: 120,
});
const documents = (job.data ?? []).map((doc) => ({
url: doc.metadata?.sourceURL ?? doc.url,
title: doc.metadata?.title ?? '',
content: doc.markdown ?? '',
}));
Step 2: Chunk and embed
import OpenAI from 'openai';
import { ChromaClient } from 'chromadb';
const openai = new OpenAI();
const chroma = new ChromaClient();
const collection = await chroma.getOrCreateCollection({ name: 'docs', dimension: 1536 });
async function ingest(documents) {
const chunkSize = 1000;
const chunkOverlap = 150;
for (const doc of documents) {
const chunks = chunkText(doc.content, chunkSize, chunkOverlap);
for (let i = 0; i < chunks.length; i++) {
const res = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: chunks[i],
});
await collection.add({
ids: [`${doc.url}#${i}`],
embeddings: [res.data[0].embedding],
metadatas: [{ url: doc.url, title: doc.title, chunk: i }],
documents: [chunks[i]],
});
}
}
}
function chunkText(text, size, overlap) {
const chunks = [];
for (let i = 0; i < text.length; i += size - overlap) {
chunks.push(text.slice(i, i + size));
}
return chunks;
}
Use pgvector instead of Chroma if you prefer PostgreSQL. Preserve url and title in metadata for citations.
Step 3: MCP server with vector query tool
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { ChromaClient } from 'chromadb';
import OpenAI from 'openai';
const chroma = new ChromaClient();
const collection = await chroma.getOrCreateCollection({ name: 'docs', dimension: 1536 });
const openai = new OpenAI();
const server = new Server({ name: 'rag-mcp', version: '1.0.0' }, { capabilities: { tools: {} } });
server.setRequestHandler('tools/call', async (request) => {
const { name, arguments: args } = request.params;
if (name !== 'query_knowledge_base') {
throw new Error(`Unknown tool: ${name}`);
}
const question = (args as { question?: string })?.question ?? '';
const embedding = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: question,
});
const results = await collection.query({
queryEmbeddings: [embedding.data[0].embedding],
nResults: 5,
include: ['documents', 'metadatas'],
});
const chunks = (results.documents?.[0] ?? []).map((doc, i) => {
const meta = results.metadatas?.[0]?.[i] ?? {};
return `[${meta.url}]\n${doc}`;
});
return { content: [{ type: 'text', text: chunks.join('\n\n---\n\n') }] };
});
server.setRequestHandler('tools/list', async () => ({
tools: [
{
name: 'query_knowledge_base',
description: 'Search the private documentation and product knowledge base. Use when the user asks about features, setup, APIs, or internal docs.',
inputSchema: {
type: 'object',
properties: { question: { type: 'string', description: 'Natural language question or search query' } },
required: ['question'],
},
},
],
}));
const transport = new StdioServerTransport();
await server.connect(transport);
Step 4: Register in Claude Desktop
Add to claude_desktop_config.json:
{
"mcpServers": {
"rag-docs": {
"command": "node",
"args": ["/path/to/your/rag-mcp-server/dist/index.js"],
"env": {
"OPENAI_API_KEY": "sk-...",
"FIRECRAWL_API_KEY": "fc-..."
}
}
}
}
Restart Claude Desktop. Claude can now call query_knowledge_base when users ask about your docs or product.
Use cases
| Use case | Source URLs | Output |
|---|---|---|
| Internal docs | Your Confluence, Notion, or docs site | Answer questions about processes and policies |
| Product knowledge base | Your docs, changelog, API reference | Feature explainers, setup help |
| Competitor research | Competitor blogs, product pages | Summarize positioning, feature comparisons |
| Support corpus | Help center, FAQ pages | Answer support-style questions |
Crawl once or on a schedule. Re-embed when content changes. The MCP server always queries the latest indexed data.
Comparison: Custom RAG MCP vs Firecrawl MCP vs Apify MCP
| Attribute | Custom RAG MCP | Firecrawl MCP | Apify MCP |
|---|---|---|---|
| Data source | Your pre-indexed vector store | Live scrape/crawl on demand | Pre-built Actors (SERP, maps, etc.) |
| Latency | Fast (local/same-region DB query) | Per-request scrape (~2–10s) | Per-request Actor run (varies) |
| Cost | Embedding + vector DB + Firecrawl (ingestion) | Firecrawl credits per scrape | Apify compute units |
| Freshness | Depends on crawl schedule | Always live | Always live |
| Customization | Full control over sources and schema | Fixed scrape/crawl/extract tools | Fixed Actor set |
| Best for | Private docs, product KB, internal data | Ad-hoc web research | SERP, maps, e-commerce, social |
Use custom RAG MCP when you have a defined corpus (docs, product, support) and want fast, private retrieval. Use Firecrawl MCP for live web scraping. Use Apify MCP for platform-specific data (Google, LinkedIn, etc.). See Firecrawl MCP server setup and Apify Claude Desktop MCP.
Ingestion automation
Run the ingestion as a cron job or GitHub Action:
# cron: daily at 2 AM
0 2 * * * cd /app/rag-ingest && node ingest.js
Or use Make.com or n8n to trigger Firecrawl → chunk → embed → upsert on a schedule. Track crawl_timestamp in metadata to avoid re-embedding unchanged pages.
Index 20–50 key pages first. Test the MCP tool with real questions. Tune chunk size and top-k before scaling.
An MCP server that exposes a tool to query a vector store. Claude calls the tool with a question; the tool returns relevant chunks from your indexed content. Claude uses them to ground its answers.
Firecrawl MCP scrapes/crawls live on each request. A custom RAG MCP queries a pre-indexed vector store. RAG is faster and cheaper for repeated queries over the same corpus; Firecrawl MCP is for ad-hoc live web access.
ChromaDB for simplicity and local dev. pgvector (PostgreSQL) for production and integration with existing data. Both work with OpenAI embeddings.
Depends on how often content changes. Docs: weekly. Product pages: daily. Support FAQs: weekly or on publish. Track crawl_timestamp and upsert only changed pages to reduce cost.
Yes. Use RAG MCP for your private corpus. Use Firecrawl MCP for live web research. Claude chooses the right tool based on the question.
Apify MCP runs Actors (scrapers). You can build a custom RAG MCP that ingests from Apify Actors (e.g., Website Content Crawler) instead of Firecrawl. Architecture is similar: crawl → chunk → embed → vector store → MCP tool.




