Skip to main content

Building a Custom RAG MCP Server for Claude with Firecrawl (2026)

· 7 min read
Yassine El Haddad
Software Developer & Automation Specialist

I build production AI agents, web scrapers, and automation pipelines. Most of what I publish here comes from the actual problems they run into: proxies that get banned, anti-bot stacks that fingerprint your client, RAG that drifts when the underlying data moves. Stack: Python, TypeScript, Go, FastAPI, LangChain, Crawlee, Playwright, deployed on AWS, GCP, and Cloudflare.

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:

  1. Ingest — Firecrawl crawls your docs, product pages, or competitor sites.
  2. Process — Chunk markdown, embed with OpenAI (or similar), store in a vector DB.
  3. Expose — MCP server tool query_knowledge_base accepts a question and returns top-k chunks.
  4. 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 caseSource URLsOutput
Internal docsYour Confluence, Notion, or docs siteAnswer questions about processes and policies
Product knowledge baseYour docs, changelog, API referenceFeature explainers, setup help
Competitor researchCompetitor blogs, product pagesSummarize positioning, feature comparisons
Support corpusHelp center, FAQ pagesAnswer 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

AttributeCustom RAG MCPFirecrawl MCPApify MCP
Data sourceYour pre-indexed vector storeLive scrape/crawl on demandPre-built Actors (SERP, maps, etc.)
LatencyFast (local/same-region DB query)Per-request scrape (~2–10s)Per-request Actor run (varies)
CostEmbedding + vector DB + Firecrawl (ingestion)Firecrawl credits per scrapeApify compute units
FreshnessDepends on crawl scheduleAlways liveAlways live
CustomizationFull control over sources and schemaFixed scrape/crawl/extract toolsFixed Actor set
Best forPrivate docs, product KB, internal dataAd-hoc web researchSERP, 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.

Apify Affiliate Banner 728x90Apify Affiliate Banner 728x90Apify Affiliate Banner 300x50Apify Affiliate Banner 300x50
Start with a small corpus

Index 20–50 key pages first. Test the MCP tool with real questions. Tune chunk size and top-k before scaling.



Firecrawl for RAG | Firecrawl RAG guide

Frequently Asked Questions

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.

Common mistakes and fixes

MCP tool returns empty or irrelevant chunks

Improve chunking (smaller size, overlap). Add metadata filters. Re-embed with a better model. Check vector store has indexed documents.

Claude doesn't use the RAG tool

Write a clear tool description. Include example queries. Ensure the tool returns concise, relevant snippets rather than huge dumps.