# Send to Claude
Source: https://speakeasy.com/blog/rag-vs-mcp

Ask Claude about Django 5.2's new features, and you get this:

![Claude has no knowledge of Django 5.2](/assets/blog/rag-vs-mcp/claude-no-django-knowledge.png)

Django 5.2 was released in April 2025. Claude's training data ends in January 2025, so it doesn't know about this release yet.

This is the core limitation of LLMs: their knowledge is frozen at training time. They can't access information published after their cutoff date or domain-specific data locked inside your organization. Claude doesn't know your API documentation, your company's internal policies, or yesterday's product updates.

To bridge this gap, you have two architectural options: Retrieval-Augmented Generation (RAG) or Model Context Protocol (MCP) servers. Both connect LLMs to external information, but they work differently and excel in different scenarios.

This guide compares RAG and MCP through a concrete example: teaching Claude about Django 5.2's new features. You'll learn how each approach works, what it costs in tokens, and when to use which.

## RAG vs MCP: Different problems, different solutions

**RAG** is an architecture pattern for semantic search. Documents are embedded into vectors, stored in a database, and retrieved based on semantic similarity to user queries.

**MCP** is a protocol for connecting LLMs to external systems. It standardizes how LLMs call tools and access data sources, and whether those tools perform RAG searches, query databases, or hit APIs.

They're not competing approaches. RAG solves issues like, "How do I search documents semantically?" while MCP solves problems like, "How do I connect my LLM to external systems?"

## What is RAG?

RAG combines information retrieval with text generation. Instead of relying only on training data, the LLM searches external databases for relevant context before generating an answer. RAG works in three steps:

1. **Retrieval:** The system sends the user's question to a retriever, which is usually a vector database like [ChromaDB](https://docs.trychroma.com/docs/overview/introduction), [Pinecone](https://docs.pinecone.io/guides/get-started/quickstart), or [Weaviate](https://weaviate.io/). The retriever then searches for semantically similar content and returns the most relevant chunks.

2. **Context injection:** The system adds retrieved information to the LLM prompt.

    ```python
    prompt = f"""You are a helpful nutritional assistant.

    Here is relevant information from our nutrition database:
    {context}

    Based on this information, please answer the following question:
    {question}

    Provide a clear answer using only the context above. If the context doesn't contain enough information, say so."""
    ```

3. **Generation:** The LLM generates an answer using both the retrieved context and the question, grounding its response in actual documentation rather than relying on training data alone.

![RAG example](/assets/blog/rag-vs-mcp/rag-architecture-diagram.png)

In the diagram above, a user asks, "What's new in django.contrib.gis?" The query goes to a vector database, which returns relevant snippets like, "Django 5.2 adds MultiCurve geometry." The LLM receives these snippets with instructions to answer only using the provided documents, resulting in a response like, "Django 5.2 adds MultiCurve support..."

## What is MCP?

The [Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro) standardizes how LLMs access external tools and data sources. Without MCP, each LLM provider uses different tool integration methods. Anthropic has tool use, OpenAI has function calling, and each uses its own schema. Building an integration for Claude means rebuilding it from scratch for GPT-5.

MCP provides one consistent interface. An MCP server works with any MCP-compatible system such as Claude, GPT-5, or custom applications. The protocol defines how tools, databases, and APIs get exposed, eliminating the need for provider-specific implementations.

## Why is RAG preferred over MCP resources?

To pass documents as context in MCP servers, MCP provides resource primitives that LLMs can consult to enrich their contexts before returning a response. But here is the thing: there is no processing or anything of the document. So, the context is just dumped into the context window, which can easily become bloated.

RAG is then preferred over MCP resources due to the vectorization that makes the document much more digestible, reducing token consumption and saving money in the long run.

## RAG vs MCP in practice

Both RAG and MCP can provide Claude Anthropic with Django 5.2 documentation, but in different ways.

Let's see how RAG and MCP handle the same task of finding information about new features in Django 5.2.

### RAG: Semantic search

Building a RAG system for Django documentation requires three components:

- A document processor that extracts and chunks text
- An embedder that converts text to vectors
- A vector database for storage

This implementation uses the PyPDF2 library for PDF extraction, the Sentence Transformers library for embeddings, ChromaDB for vector storage, and Anthropic for answer generation.

#### Indexing the documentation

The first step is to extract and preprocess the documentation. The following code reads the Django PDF, splits it into overlapping text segments to preserve context, embeds those chunks in vectors, and stores everything in ChromaDB for semantic search:

```python
from pathlib import Path
from PyPDF2 import PdfReader
from sentence_transformers import SentenceTransformer
import chromadb

pdf_path = Path("django.pdf")
reader = PdfReader(str(pdf_path))
text = "\n\n".join([p.extract_text() for p in reader.pages])
words = text.split()

chunks = [" ".join(words[i:i+500]) for i in range(0, len(words), 450)]
embeddings = SentenceTransformer("all-MiniLM-L6-v2").encode(chunks)

client = chromadb.PersistentClient(path="./chroma_db")
collection = client.create_collection("django_docs")
collection.add(ids=[f"chunk_{i}" for i in range(len(chunks))],
               embeddings=embeddings.tolist(),
               documents=chunks)
```

Here, we create 500-word chunks with 50-word overlaps. This overlap is essential because it ensures that concepts spanning chunk boundaries remain intact and searchable. We then convert each chunk into a numerical embedding, which is a 384-dimensional vector that captures its semantic meaning.

The `all-MiniLM-L6-v2` model handles this conversion. It's lightweight and fast, but still maintains good semantic accuracy.

#### Querying the system

When a user asks a question about Django 5.2, the system embeds the question using the same `all-MiniLM-L6-v2` model, retrieves the most relevant chunks from ChromaDB based on semantic similarity, and sends those chunks to Claude. Claude receives the retrieved documentation with instructions to answer using only the provided context.

```python
import os
from sentence_transformers import SentenceTransformer
import chromadb
import anthropic
from dotenv import load_dotenv

load_dotenv()

model = SentenceTransformer("all-MiniLM-L6-v2")
client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_collection("django_docs")

query = "Django 5.2 new features"
q_embed = model.encode(query).tolist()
results = collection.query(query_embeddings=[q_embed], n_results=3)
context = "\n\n".join(results["documents"][0])


client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    messages=[{"role": "user", "content": f"{context}\n\nQuestion: {query}"}]
)

print("Answer:")
print(response.content[0].text)
print(f"\nTokens used: {response.usage.input_tokens + response.usage.output_tokens}")
```

The system retrieves the three chunks most similar to the query, and formats them into the prompt along with the question. Claude then generates an answer grounded in the actual Django documentation rather than its training data.

### RAG outcome

When a user asks about Django 5.2, the system embeds the query in a vector, matches it against the ChromaDB content, and retrieves the three 500-word chunks most semantically similar to the query:

```python
import os
import anthropic
from mcp.client.stdio import stdio_client
from mcp import ClientSession, StdioServerParameters
import asyncio
import json
from dotenv import load_dotenv

load_dotenv()

async def query_mcp(question: str):
    # 1. Connect to MCP server
    server_params = StdioServerParameters(
        command="python3",
        args=["mcp_server.py"],
        env=None
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # 2. Call search tool
            result = await session.call_tool(
                "search_django_docs",
                arguments={"query": question}
            )

            return json.loads(result.content[0].text)