LLM Providers

Connect Claude to internal content via MCP or DIY retrieval with Sanity Context

Claude

Anthropic's Claude models with an MCP connector in the Messages API and a custom tool-calling path for wiring your own typed retrieval into the agent loop.

Visit Claude

You wire Claude up to answer questions about your product catalog. It works in the demo. Then a customer asks "which of the newer soundbars support Dolby Atmos and are in stock in the EU warehouse," and Claude confidently invents two models that do not exist. The query carried a structural component, a date range, a feature flag, an inventory state, that a bare vector search or a JSON dump can never resolve, so the model hedged or hallucinated. The fix is not a smarter model. It is better retrieval.

Sanity Context is the product that gives agents structured, governed access to your content. Its primary surface today is Context MCP, a hosted, read-only MCP endpoint that any agent loop can connect to, exposing schema reads, GROQ queries, reference traversal, and optional semantic search across a Sanity dataset. Knowledge Bases is the second surface, for unstructured sources like PDFs, websites, and support databases. Sanity is the Content Operating System for the AI era, an intelligent backend that keeps the content Claude reads governed and fresh.

This article walks the two ways to connect Claude to internal content: the MCP connector (the fast, low-code path) and a DIY custom tool that runs a typed GROQ query (full control over the predicate and scoring). We will show where each one wins, and why the structural side of the query is where agents fail first.

The failure mode: Claude hallucinates when the query has structure

Start with the actual bug, because it shapes everything downstream. A user asks Claude something with a real structural component: a version number, a category, a price ceiling, an "in stock" constraint. If your retrieval is a pure vector lookup over embedded product descriptions, similarity does not respect any of those constraints. The nearest neighbors to "Dolby Atmos soundbar under 800 in the EU warehouse" are other soundbar descriptions, ranked by prose similarity, not by price or stock. The right answer might be the twelfth-nearest neighbor, or absent from the top-k entirely. Claude gets a slate of plausible-but-wrong documents, and depending on how you wrote the system prompt, it either hedges or fills the gap with invented specs.

The tell is that swapping models does not help. Move from Sonnet to Opus, turn up the thinking budget, and the empty-or-wrong result set is still empty or wrong. Anthropic's own contextual retrieval research is blunt about this: contextual embeddings alone cut top-20 retrieval failures by 35%, contextual BM25 on top took that to 49%, and adding reranking reached 67%. None of the three layers alone was enough. That number is the whole argument. If the best embeddings work in the industry still leaves a third of your hard queries failing at top-20, then "we have embeddings" is not a retrieval strategy, and the model was never the bottleneck.

So the question for a Claude developer is not "which model," it is "what does Claude see per turn." The model is independent of the retrieval layer. The interesting work is in the tool that fetches the documents, and there are two honest ways to build it: attach a hosted MCP server, or write a custom tool of your own.

Path one: the MCP connector in the Messages API

Claude's MCP connector lets you talk to a remote MCP server directly from the Messages API, without standing up a separate MCP client process. You pass an `mcp_servers` array where each entry has `type: "url"`, an `https://` URL, a unique name, and an optional `authorization_token` for OAuth. You add a matching `tools` entry of `{"type": "mcp_toolset", "mcp_server_name": "..."}` and set the beta header (current at time of writing, `mcp-client-2025-11-20`). Claude then discovers the server's tools and calls them itself; you do not hand-write the round trip.

Two constraints matter for internal content. First, the connector currently supports only tool calls from the MCP spec over this path, not prompts or resources, and the server must be publicly reachable over HTTP using Streamable HTTP or SSE. Local STDIO servers cannot be connected directly. Second, toolset config supports allowlisting (set `default_config.enabled=false`, then enable specific tools), denylisting (enable all, disable named ones), and per-tool overrides. Anthropic's docs explicitly recommend denylisting write or destructive tools when you are building a read-only assistant.

That read-only posture is exactly the shape of Context MCP: a hosted, read-only HTTP endpoint. You point Claude's connector at it and get schema-aware read tools out of the box, GROQ queries and schema lookups, without writing a client. Writes are simply not on this surface; in Sanity, mutations go through Agent Actions, not MCP, so there is no destructive tool to denylist in the first place. The auth boundary stays clean, and the fastest way in is genuinely fast.

âš ī¸

Tool descriptions are trusted text

Every MCP server you attach injects its tool descriptions into Claude's context on every turn. A sloppy or malicious server can prompt-inject your agent before the user has typed a word. Read the source of every MCP you install, and prefer read-only, hosted endpoints whose surface you can reason about. Context MCP being read-only is not just a safety nicety; it shrinks the trusted-text and attack surface you have to audit.

Attach a hosted MCP server to Claude in the Messages API

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

const message = await client.beta.messages.create({
  model: "claude-opus-5", // current at time of writing
  max_tokens: 1024,
  betas: ["mcp-client-2025-11-20"],
  mcp_servers: [
    {
      type: "url",
      url: "https://mcp.sanity.io/context",
      name: "sanity-context",
      authorization_token: process.env.SANITY_MCP_TOKEN,
    },
  ],
  tools: [
    { type: "mcp_toolset", mcp_server_name: "sanity-context" },
  ],
  messages: [
    {
      role: "user",
      content: "Which Atmos soundbars under 800 are in stock in the EU warehouse?",
    },
  ],
});

console.log(message.content);

Path two: a DIY custom tool with full query control

The MCP connector is the low-code path. Sometimes you want the opposite: total control over the predicate, the projection, and the scoring. That is Claude's ordinary function-calling path. You define a tool with a `name`, a `description`, and a JSON Schema `input_schema`. Claude returns `stop_reason: "tool_use"` with one or more `tool_use` blocks naming your tool and its JSON arguments. Your application runs the actual query and sends the answer back as a `tool_result` block that references the `tool_use_id` in a follow-up Messages request. Client tools like this run in your app, unlike server tools such as `web_search` that run on Anthropic's infrastructure.

The behavior is steerable in ways that matter for retrieval. With the default `tool_choice: {"type": "auto"}`, Claude decides per turn whether to call the tool; a system-prompt line like "Use the tools to investigate before responding" raises tool use, and `disable_parallel_tool_use` pins Claude to one call per turn. For a typed content query you usually want exactly one well-formed call, not three speculative ones, so that flag earns its keep. Anthropic's SDK Tool Runner will execute your tool and post results back automatically, so you are not hand-wiring the loop unless you want to.

The win of the DIY path is that you own the query. You decide which fields become tool inputs, how the projection is shaped, and how scoring is weighted. The cost is that you are now maintaining a query and, if you go the vector route yourself, an index. That trade is the whole subject of the next section.

Structured retrieval first, hybrid only when it earns it

Here is the counter-intuitive part, and it holds regardless of which path you picked. When you look at how agents actually call Context MCP in production, the heavy majority of calls are structured: GROQ queries and schema lookups, with the compressed initial context behind that. Semantic search is a small slice. Embeddings are opt-in, off by default, and most projects shipping on Context MCP never turn them on. That is not because embeddings are bad. It is because the structural side of the query is where agents fail first, and a working structured retrieval gets you further than most teams expect before semantic ranking becomes the bottleneck.

So do not reach for a vector index on day one. A pure structured query, GROQ or SQL or GraphQL, gives you exactly what you asked for. It falls over the moment the user says "the cozy one" or "something like X," pure vibes that live in no field. But most internal-content questions from Claude are not vibes; they are "the 2024 models, in stock, under 800," and a predicate answers those precisely and freshly. You only reach for hybrid when structured retrieval is genuinely leaving relevance on the table.

When you do reach for it, hybrid means structured predicates plus BM25 plus optional embeddings, in one query, not embeddings everywhere. The predicates do the filtering that has to hold. A score pipeline blends a keyword match with a semantic similarity score. The result is a small, ranked list that satisfies both the constraints and the vibe. In GROQ that composes inside a single query, which is why the freshness problem does not become a second system to run.

â„šī¸

The freshness problem is a real line item

You can build hybrid retrieval yourself with pgvector, Elasticsearch, Algolia, or Pinecone plus a metadata filter layer. What none of them hand you is a content pipeline that keeps the index fresh: incremental indexing, re-embedding on change, deletion handling, and backfill for schema changes. When retrieval is wired into your content backend, freshness stops being something you maintain. When it is a separate vector DB plus glue code, it stays on your roadmap forever.

Hybrid retrieval in one GROQ query (structured predicates + BM25 + semantic)

*[
  _type == "product"
  && category == $category
  && price < $maxPrice
  && stockLocation == $warehouse
]
| score(
    boost([title] match text::query($queryText), 2),
    text::semanticSimilarity($queryText)
  )
| order(_score desc)
[0...10] {
  _id,
  title,
  price,
  "stock": stockLocation->{ name, available }
}

Why retrieval fails even after you fix the query

Suppose you build the DIY tool well and still see wrong answers. The next failure is subtler, and it is not about operators. Real datasets carry knowledge the schema does not encode. A field called `body` that is actually a slug. A reference chain the schema does not visibly connect, where a product points at a family that points at the spec sheet. Data-quality issues the types cannot reveal, like a `category` that is populated inconsistently across a few thousand records. Claude cannot infer any of that from the schema alone, so its generated query targets the wrong field and comes back empty, and the model hedges or invents.

Sanity's own schema-exploration work ran against Sonos's catalog, an honest nightmare of a dataset, and landed around 83% accuracy on a mix of difficulties using Sonnet 4.5 for reasoning, roughly 40 seconds of thinking per hard question. Getting there was not a model upgrade. It meant teaching the retrieval step the counter-intuitive field names, the second-order reference chains, and the data-quality quirks that a schema will never tell you. None of that is a model problem. It is a context problem: the model needed to know the shape of the data, not just its types.

This is where the read-only, schema-aware nature of Context MCP pays off past the demo. The tools it exposes are aware of the actual schema and its reference graph, so Claude traverses real relationships instead of guessing at field names. When you point Claude's connector at it, the schema reads and reference traversal come from the dataset's real structure, which is exactly the knowledge that a hand-rolled JSON dump or a bare vector store strips away.

Unstructured content: when to reach for Knowledge Bases instead of GROQ

GROQ retrieval is the right tool for structured content: your catalog, your articles with real schema, anything with fields you can filter on. It is the wrong tool for a folder of PDFs, a scraped marketing site, or a support database full of free-text tickets. Those sources have no clean schema to query. If you try to force them through a predicate, you get nothing useful, and if you dump them into Claude's context whole, you blow the window and pay for tokens the model mostly ignores.

For that shape of content, the right Sanity Context surface is Knowledge Bases, not GROQ. Knowledge Bases turns messy sources, Sanity datasets, support databases, websites, and PDFs, into well-ordered documents with a clear table of contents, so agents answer faster, more accurately, and at lower cost. The agent picks its strategy depending on the question: structured retrieval for the catalog query, Knowledge Bases for "what does our returns policy say about opened items." Both live under the same Sanity Context product, so Claude reaches one governed source rather than a vector DB, a JSON blob, and a CMS sync job stitched together.

There is an honest boundary here worth naming. Not everything belongs in Sanity. A high-volume, machine-generated corpus that needs no editorial governance, say, millions of log-derived embeddings, is fine in a dedicated vector DB. The routing rule is simple: structured, governed content goes to GROQ retrieval; unstructured, human-authored sources go to Knowledge Bases; ungoverned machine data can stay where it is. Match the surface to the content shape, and Claude stops guessing.

✨

Author the agent's prompt like content, gate it like code

The system prompt that steers Claude is usually a string in the codebase, so marketing cannot read it, compliance cannot review the never-say list, and support cannot update escalation language without a pull request. Managed as content in the Studio, it gains real-time collaboration, version history, and rollback for free. Content Releases lets you stage a prompt change and preview it before it ships, the same way you stage a homepage change.

Choosing a path, and what it costs you either way

Both paths connect Claude to the same governed content, so the choice is about control and maintenance, not capability. Reach for the MCP connector when you want the fastest way in: attach Context MCP as an `mcp_server`, get schema-aware read tools out of the box, and let Claude discover and call them. You write almost no glue. You inherit the read-only posture, which means there is no destructive tool to guard, and the schema-aware traversal that keeps the model from guessing at field names. For most internal-content assistants, this is the correct default.

Reach for the DIY custom tool when you need to own the query end to end: a specific projection shape, a particular scoring weight, a predicate the connector does not express the way you want. You define the `input_schema`, execute the GROQ yourself, and return a `tool_result`. You get full control, and you accept the maintenance that comes with it. If you also decide to run your own vector index, remember the freshness tax: incremental indexing, re-embedding on change, and deletion handling are a real project, and a class of bug all their own.

Whatever path you choose, the model is yours and stays independent. Context MCP does not care whether you point Anthropic, OpenAI, Gemini, or an open-weight model at it; the retrieval, the schema, and the tool surface stay the same. That independence is the point. Sanity is the intelligent backend for teams building AI content operations at scale, keeping what Claude reads structured, fresh, and governed, so the hard part of your agent becomes the reasoning, not the context assembly.