# 3. Format results and send to Claude
mcp_docs = "\n\n".join([
    f"[Page {r['page']}]\n{r['text']}"
    for r in mcp_results["results"]
])

response = anthropic_client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=200,
    messages=[{
        "role": "user",
        "content": f"I searched Django docs for: \"{question}\"\n\nFound {mcp_results['results_count']} pages:\n\n{mcp_docs}\n\nProvide a short, direct answer."
    }]
)
```

The server returns 50 pages containing words like "Django", "5.2", "new", and "features". This includes relevant content, such as release notes and feature documentation, alongside many irrelevant pages: configuration files mentioning "new" settings, migration guides with "Django" in headers, tutorial pages with "features" in titles, and 45 other loosely related pages. Claude reads through everything to find the answer.

![Using MCP servers](/assets/blog/rag-vs-mcp/mcp-implementation-result.png)

The MCP implementation provided an incomplete answer. The naive keyword search found pages containing "Django", "features", "new", or "5.2" throughout the 2,000+ page document. However, the implementation hit a hard limit: Claude's API has a maximum context window. The system can only send the first 50 matching pages to stay within token limits.

The most relevant release notes pages weren't necessarily among the first 50 pages. The keyword matching has no way to prioritize which pages are most relevant because it just returns them in document order. Even if we had unlimited context, sending hundreds of pages per query would be impractical: costs would exceed $9 per query and response times would stretch beyond 90 seconds.

The MCP keyword search uses:

- **Tokens:** 30,048
- **Cost:** $0.0925
- **Response time:** 30.41 seconds

### Comparing outcomes

Looking at the resource use for each implementation, it's clear that RAG is the more efficient option. Generating a response with RAG uses less than a third of the tokens and time it takes with MCP.

| Metric             | RAG    | MCP     |
| ------------------ | ------ | ------- |
| **Input tokens**   | 9,619  | 30,048  |
| **Cost per query** | $0.036 | $0.0925 |
| **Response time**  | 8.89s  | 30.41s  |

This stark difference is due to the use of vectorization in the RAG implementation. By processing documents into chunks and sending only three chunks to Claude, RAG keeps both the token use and the monetary cost of each query low.

In contrast, the MCP server loads the Django document as an MCP resource primitive that is not processed. By passing 50 unprocessed document pages in to the context window, MCP keyword matching uses more than triple the resources of a RAG semantic search.

The RAG implementation also resulted in a more complete answer. Because RAG uses semantic similarity, it provides more focused, relevant context. The embedding model converted the "Django 5.2 new features" query into a vector that's mathematically similar to vectors for "release notes", "version updates", and "feature announcements", enabling the system to retrieve content based on conceptual proximity.

Without the same semantic reasoning, the MCP keyword search delivered the first 50 pages containing "Django", "new", or "features" rather than the 50 most relevant pages containing those keywords.

## When to use MCP

MCP shines when working with dynamic, structured data that changes frequently. For example, consider running an e-commerce platform with 100,000+ products. Your prices change daily, the inventory updates in real time, and you constantly add new products.

With RAG, every time something changes, you'd need to re-embed that data and update your vector database. Given your extensive product catalog, you'd need to regenerate embeddings for thousands of items daily. The embedding process alone could cost $5-10 per day and take 10-30 minutes to complete. You'd always have a sync lag between your real data and the data the LLM can access.

With MCP, you could use tools like `get_product_info` and `search_products` to access the actual platform data:

```python
@mcp.tool()
def get_product_info(product_id: str) -> dict:
    response = requests.get(f"https://api.yourstore.com/products/{product_id}")
    return response.json()

@mcp.tool()
def search_products(query: str, category: str = None) -> list:
    return api.search(query=query, in_stock=True, category=category)
```

This pattern works beautifully for any structured data that changes often, including user accounts, order statuses, inventory levels, API rate limits, and server metrics. MCP also enables actions, enabling agents not just to read data but also to create orders, update records, or trigger workflows.

RAG remains the better choice for static content, like documentation, where semantic search matters and the content rarely changes. The ideal architecture often combines both: using RAG to search product documentation and tutorials, and MCP to handle live inventory lookups and order placement.

## Combining RAG and MCP

Given their differences, you may think you shouldn't use RAG and MCP together, but they combine perfectly. In fact, MCP tools can perform RAG queries, acting as the bridge between Claude and your vector database.

For example, instead of Claude directly accessing your vector store, you can build an MCP tool that handles the entire RAG pipeline processes — embedding the query, searching vectors, and returning relevant chunks. From Claude's perspective, it just calls a tool, but under the hood, that tool conducts a sophisticated semantic search.

```python
@mcp.tool()
async def search_documentation(query: str, top_k: int = 3) -> dict:
    """
    Search technical documentation using semantic similarity.
    Returns the most relevant documentation chunks.
    """
    # 1. Embed the query
    query_embedding = embedder.encode(query)

    # 2. Search vector database
    results = collection.query(
        query_embeddings=[query_embedding.tolist()],
        n_results=top_k
    )

    # 3. Return formatted results
    return {
        "query": query,
        "chunks": [
            {
                "text": doc,
                "page": meta["page_num"],
                "similarity": 1 - distance
            }
            for doc, meta, distance in zip(
                results["documents"][0],
                results["metadatas"][0],
                results["distances"][0]
            )
        ]
    }
```

This architecture gives you the best of both worlds. You benefit from RAG's semantic search and efficient token usage, while MCP provides standardization, discoverability, and easy integration with Claude Desktop and other MCP clients. The vector database stays behind your MCP server, handling the heavy lifting, while Claude simply calls tools when it needs context.

You can even combine multiple approaches in one system, using:
- MCP resources for static reference data (for example, API schemas and configuration files)
- MCP tools with RAG for searchable documentation
- Additional MCP tools for actions like creating tickets or updating records

The MCP protocol unifies all these capabilities in one interface.

The following projects on GitHub implement these patterns:

- [`mcp-crawl4ai-rag`](https://github.com/coleam00/mcp-crawl4ai-rag)
- [`rag-memory-mcp`](https://github.com/ttommyth/rag-memory-mcp)

## Conclusion

RAG and MCP aren't competing approaches; you can use them to complement each other. RAG provides semantic search capability, and MCP standardizes how your AI connects to external systems, whether those systems perform RAG searches, database queries, or API calls.

RAG works best for searching documentation because of its semantic understanding and low token costs. MCP excels at live data queries and actions like creating records and triggering workflows. Modern production AI systems use RAG for searching knowledge bases and MCP for carrying out real-time operations.