> For AI agents: the complete Sanity documentation index is available at [https://www.sanity.io/docs/llms.txt](https://www.sanity.io/docs/llms.txt).

# Connect Sanity Context with the Claude Agent SDK

Connect Sanity Context to a Python or TypeScript agent built with the Claude Agent SDK.

The [Claude Agent SDK](https://code.claude.com/docs/en/agent-sdk/overview) runs Claude as a general-purpose agent from Python or TypeScript. This guide shows you how to connect an Agent SDK agent to [Sanity Context](https://www.sanity.io/docs/context/introduction) and limit it to the Sanity tools. You'll build a script that loads your schema or Knowledge Base outline into the system prompt, connects to your Context MCP endpoint as a remote HTTP server, and answers a question about your content. The same code works whether the endpoint uses Knowledge Base sources, a dataset source, or both.

## Prerequisites

To follow this guide, you need:

- **Context MCP endpoint URL**: Shown on the MCP endpoint in the Context app in the Sanity Dashboard. To create an MCP endpoint, see [Configure a Context MCP endpoint](https://www.sanity.io/docs/context/configure-mcp), or expand **Create the endpoint with the Sanity CLI (optional)** at the end of this section.
- **Organization API token**: Create one in [Manage](https://www.sanity.io/manage/org/api/tokens) under **API > Tokens** at the organization level. Under **Organization permissions**, select **Context** with the **Viewer** role. Project tokens don't work.
- **Anthropic API key**: The SDK reads it from `ANTHROPIC_API_KEY`.
- **Node.js 22.18 or later** for TypeScript, or **Python 3.10 or later** for Python. Node.js 22.18 and later run `.ts` files directly.

Both SDKs bundle the Claude Code runtime they drive, so there's nothing else to install.

You can create a Knowledge Base and an MCP endpoint from the Sanity CLI instead of the Dashboard. If you haven't signed in, run `npx sanity@latest login`. You still create the organization token in Manage. Replace `ORGANIZATION_ID`, `PROJECT_ID`, and `KNOWLEDGE_BASE_ID` with your own values:

**npm**

```shell
# Create a Knowledge Base. The command prints its ID.
npx sanity@latest context create --organization ORGANIZATION_ID --title "Support" --description "Answers to customer support questions"

# Add sources: a dataset query, files, or a website to crawl (--url)
npx sanity@latest context sources create KNOWLEDGE_BASE_ID --sanity-project-id PROJECT_ID --sanity-dataset-id production --query '*[_type == "article"]'
npx sanity@latest context sources create KNOWLEDGE_BASE_ID --file ./returns-policy.pdf

# Build it, then review anything the build flagged
npx sanity@latest context build KNOWLEDGE_BASE_ID --watch
npx sanity@latest context issues list KNOWLEDGE_BASE_ID --status open

# Create the MCP endpoint and check that it's ready
npx sanity@latest context mcp create support --title "Support" --organization ORGANIZATION_ID --knowledge-base KNOWLEDGE_BASE_ID
npx sanity@latest context mcp status support --organization ORGANIZATION_ID
```

**pnpm**

```shell
# Create a Knowledge Base. The command prints its ID.
pnpm dlx sanity@latest context create --organization ORGANIZATION_ID --title "Support" --description "Answers to customer support questions"

# Add sources: a dataset query, files, or a website to crawl (--url)
pnpm dlx sanity@latest context sources create KNOWLEDGE_BASE_ID --sanity-project-id PROJECT_ID --sanity-dataset-id production --query '*[_type == "article"]'
pnpm dlx sanity@latest context sources create KNOWLEDGE_BASE_ID --file ./returns-policy.pdf

# Build it, then review anything the build flagged
pnpm dlx sanity@latest context build KNOWLEDGE_BASE_ID --watch
pnpm dlx sanity@latest context issues list KNOWLEDGE_BASE_ID --status open

# Create the MCP endpoint and check that it's ready
pnpm dlx sanity@latest context mcp create support --title "Support" --organization ORGANIZATION_ID --knowledge-base KNOWLEDGE_BASE_ID
pnpm dlx sanity@latest context mcp status support --organization ORGANIZATION_ID
```

**yarn**

```shell
# Create a Knowledge Base. The command prints its ID.
yarn dlx sanity@latest context create --organization ORGANIZATION_ID --title "Support" --description "Answers to customer support questions"

# Add sources: a dataset query, files, or a website to crawl (--url)
yarn dlx sanity@latest context sources create KNOWLEDGE_BASE_ID --sanity-project-id PROJECT_ID --sanity-dataset-id production --query '*[_type == "article"]'
yarn dlx sanity@latest context sources create KNOWLEDGE_BASE_ID --file ./returns-policy.pdf

# Build it, then review anything the build flagged
yarn dlx sanity@latest context build KNOWLEDGE_BASE_ID --watch
yarn dlx sanity@latest context issues list KNOWLEDGE_BASE_ID --status open

# Create the MCP endpoint and check that it's ready
yarn dlx sanity@latest context mcp create support --title "Support" --organization ORGANIZATION_ID --knowledge-base KNOWLEDGE_BASE_ID
yarn dlx sanity@latest context mcp status support --organization ORGANIZATION_ID
```

**bun**

```shell
# Create a Knowledge Base. The command prints its ID.
bunx sanity@latest context create --organization ORGANIZATION_ID --title "Support" --description "Answers to customer support questions"

# Add sources: a dataset query, files, or a website to crawl (--url)
bunx sanity@latest context sources create KNOWLEDGE_BASE_ID --sanity-project-id PROJECT_ID --sanity-dataset-id production --query '*[_type == "article"]'
bunx sanity@latest context sources create KNOWLEDGE_BASE_ID --file ./returns-policy.pdf

# Build it, then review anything the build flagged
bunx sanity@latest context build KNOWLEDGE_BASE_ID --watch
bunx sanity@latest context issues list KNOWLEDGE_BASE_ID --status open

# Create the MCP endpoint and check that it's ready
bunx sanity@latest context mcp create support --title "Support" --organization ORGANIZATION_ID --knowledge-base KNOWLEDGE_BASE_ID
bunx sanity@latest context mcp status support --organization ORGANIZATION_ID
```

`mcp create` prints the endpoint URL to use as `SANITY_CONTEXT_MCP_URL`. To serve a live dataset through a dataset source instead, replace `--knowledge-base KNOWLEDGE_BASE_ID` with `-p PROJECT_ID -d production`. See the [Context CLI command reference](https://www.sanity.io/docs/cli-reference/cli-context) for every flag.



## Install dependencies

#### TypeScript

**npm**

```shell
npm install @anthropic-ai/claude-agent-sdk
```

**pnpm**

```shell
pnpm add @anthropic-ai/claude-agent-sdk
```

**yarn**

```shell
yarn add @anthropic-ai/claude-agent-sdk
```

**bun**

```shell
bun add @anthropic-ai/claude-agent-sdk
```

Set the module type in `package.json` so Node.js loads `agent.ts` as an ES module and the top-level await runs:

**CLI**

```sh
npm pkg set type=module
```



#### Python

**CLI**

```sh
python3 -m venv .venv
source .venv/bin/activate
pip install claude-agent-sdk httpx python-dotenv
```

Install into a virtual environment. On recent Debian, Ubuntu and Homebrew Python installs, `pip install` against the system Python fails with `externally-managed-environment`.



## Set environment variables

Create a `.env` file next to `agent.ts` or `agent.py`. Replace each placeholder with your own value:

**.env**

```sh
SANITY_CONTEXT_MCP_URL=YOUR_MCP_ENDPOINT_URL
SANITY_ORGANIZATION_TOKEN=YOUR_ORGANIZATION_TOKEN
ANTHROPIC_API_KEY=YOUR_ANTHROPIC_API_KEY
```

## Create the agent

Create `agent.ts` or `agent.py` next to `.env`. The script works in three stages:

1. **Fetch initial context** from the `/initial-context` HTTP endpoint and add it to the system prompt. It's the schema overview for a dataset source, the Knowledge Base outline for Knowledge Base sources, or both if the endpoint has both, plus the endpoint's instructions. The agent writes accurate queries from the first turn and saves a tool call per conversation. For details, see [Inline initial context into your system prompt](https://www.sanity.io/docs/context/inline-initial-context).
2. **Connect to the MCP endpoint** by passing it as an `http` server with your organization token in the headers. The SDK names its tools `mcp__sanity__TOOL_NAME`, where `TOOL_NAME` is the Context MCP tool name. When the initial context is in the prompt, the disallowed tools list hides `mcp__sanity__initial_context`. If the fetch failed, the agent keeps the tool and calls it instead.
3. **Run the agent** with `query()`. The first message reports each MCP server's status, and the script stops unless Sanity is connected. Otherwise the agent would run without your content.

#### TypeScript

**agent.ts**

```typescript
import {query} from '@anthropic-ai/claude-agent-sdk'

function requireEnv(name: string): string {
  const value = process.env[name]
  if (!value) throw new Error(`Missing environment variable ${name}`)
  return value
}

const MCP_URL = requireEnv('SANITY_CONTEXT_MCP_URL')
const TOKEN = requireEnv('SANITY_ORGANIZATION_TOKEN')
const headers = {Authorization: `Bearer ${TOKEN}`}

// Fetch initial context: the schema overview, the Knowledge Base outline, or both.
async function fetchInitialContext(): Promise<string | null> {
  const url = new URL(MCP_URL)
  url.pathname = `${url.pathname.replace(/\/$/, '')}/initial-context`
  try {
    const response = await fetch(url, {headers, signal: AbortSignal.timeout(10_000)})
    if (!response.ok) throw new Error(`HTTP ${response.status}`)
    return await response.text()
  } catch (error) {
    console.warn(`Could not fetch initial context, the agent will call the tool instead: ${error}`)
    return null
  }
}

const initialContext = await fetchInitialContext()

let systemPrompt = 'You answer questions using the Sanity Context tools.'
if (initialContext) {
  systemPrompt += `\n\n# Data reference\n\n${initialContext}`
}

try {
  for await (const message of query({
    prompt: process.argv[2] ?? 'What content do we have?',
    options: {
      model: 'claude-sonnet-5-5',
      systemPrompt,
      mcpServers: {
        sanity: {type: 'http', url: MCP_URL, headers},
      },
      // No built-in tools (Bash, Read, Write, WebFetch, ...): only the Sanity MCP tools.
      tools: [],
      // Pre-approve every tool from the "sanity" server; deny anything else without prompting.
      allowedTools: ['mcp__sanity'],
      permissionMode: 'dontAsk',
      // The data reference is already in the system prompt, so hide the tool that returns it.
      disallowedTools: initialContext ? ['mcp__sanity__initial_context'] : [],
      // Ignore local Claude Code settings, CLAUDE.md files and other MCP servers.
      settingSources: [],
      strictMcpConfig: true,
      maxTurns: 10,
    },
  })) {
    // The first message reports MCP server status. Stop if Sanity isn't connected,
    // otherwise the agent would answer without any Sanity tools.
    if (message.type === 'system' && message.subtype === 'init') {
      const sanity = message.mcp_servers.find((server) => server.name === 'sanity')
      if (sanity?.status !== 'connected') {
        throw new Error(`Sanity Context MCP server status: ${sanity?.status}`)
      }
    }
    // The final message of a successful run carries the answer text.
    // On failure the SDK throws after the result message.
    if (message.type === 'result' && message.subtype === 'success' && !message.is_error) {
      console.log(message.result)
    }
  }
} catch (error) {
  console.error(error instanceof Error ? error.message : error)
  process.exitCode = 1
}
```



#### Python

**agent.py**

```python
import asyncio
import os
import sys
from urllib.parse import urlsplit, urlunsplit

import httpx
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, SystemMessage, query
from dotenv import load_dotenv

load_dotenv()

MCP_URL = os.environ["SANITY_CONTEXT_MCP_URL"]
TOKEN = os.environ["SANITY_ORGANIZATION_TOKEN"]
HEADERS = {"Authorization": f"Bearer {TOKEN}"}


def fetch_initial_context() -> str | None:
    """Fetch initial context: the schema overview, the Knowledge Base outline, or both."""
    parts = urlsplit(MCP_URL)
    url = urlunsplit(parts._replace(path=parts.path.rstrip("/") + "/initial-context"))
    try:
        response = httpx.get(url, headers=HEADERS, timeout=10)
        response.raise_for_status()
        return response.text
    except httpx.HTTPError as error:
        print(
            f"Could not fetch initial context, the agent will call the tool instead: {error}",
            file=sys.stderr,
        )
        return None


async def main() -> None:
    initial_context = fetch_initial_context()

    system_prompt = "You answer questions using the Sanity Context tools."
    if initial_context:
        system_prompt += f"\n\n# Data reference\n\n{initial_context}"

    options = ClaudeAgentOptions(
        model="claude-sonnet-5-5",
        system_prompt=system_prompt,
        mcp_servers={
            "sanity": {"type": "http", "url": MCP_URL, "headers": HEADERS},
        },
        # No built-in tools (Bash, Read, Write, WebFetch, ...): only the Sanity MCP tools.
        tools=[],
        # Pre-approve every tool from the "sanity" server; deny anything else without prompting.
        allowed_tools=["mcp__sanity"],
        permission_mode="dontAsk",
        # The data reference is already in the system prompt, so hide the tool that returns it.
        disallowed_tools=["mcp__sanity__initial_context"] if initial_context else [],
        # Ignore local Claude Code settings, CLAUDE.md files and other MCP servers.
        setting_sources=[],
        strict_mcp_config=True,
        max_turns=10,
    )

    prompt = sys.argv[1] if len(sys.argv) > 1 else "What content do we have?"
    async for message in query(prompt=prompt, options=options):
        # The first message reports MCP server status. Stop if Sanity isn't connected,
        # otherwise the agent would answer without any Sanity tools.
        if isinstance(message, SystemMessage) and message.subtype == "init":
            status = {s["name"]: s["status"] for s in message.data["mcp_servers"]}
            if status.get("sanity") != "connected":
                raise RuntimeError(f"Sanity Context MCP server status: {status.get('sanity')}")
        # The final message of a successful run carries the answer text.
        # On failure the SDK raises an exception instead.
        if isinstance(message, ResultMessage) and not message.is_error:
            print(message.result)


asyncio.run(main())
```



The Agent SDK is a general-purpose agent, so the options in the script narrow it to a content agent:

- `tools: []` (`tools=[]` in Python) removes the built-in tools, such as Bash, file editing and web fetch.
- `allowedTools: ['mcp__sanity']` approves every tool from the Sanity server, and the `dontAsk` permission mode denies anything else without prompting. Without the allow rule, every Sanity tool call is denied.
- `settingSources: []` and `strictMcpConfig: true` (`setting_sources=[]` and `strict_mcp_config=True` in Python) ignore local Claude Code settings, `CLAUDE.md` files, and other MCP servers, so the agent behaves the same on every machine.

## Run the agent

Pass a question as the first argument, or leave it out to ask "What content do we have?"

#### TypeScript

**CLI**

```sh
# Node reads the .env file with --env-file
node --env-file=.env agent.ts "Do you price match?"
```



#### Python

**CLI**

```sh
python agent.py "Do you price match?"
```



The agent calls the Sanity tools it needs, then prints its answer to the terminal. If the run fails, match the message against the list in [Troubleshoot common errors](https://www.sanity.io/docs/context/connect-claude-agent-sdk). To confirm the initial context loaded, ask "What content do you have access to?" With the initial context in the system prompt, the agent answers without calling a tool first.

## Dataset and Knowledge Base sources

The same code works with an endpoint that serves Knowledge Bases. What changes is what the endpoint returns:

##### Tools and initial context by source type

|  | Dataset source | Knowledge Base source |
| --- | --- | --- |
| Content source | A dataset | One or more Knowledge Bases |
| Initial context | Schema overview | Outline of each Knowledge Base, with its ID |
| Tools after filtering | mcp__sanity__schema_explorer, mcp__sanity__groq_query, mcp__sanity__array_field_reader | mcp__sanity__knowledge_base_search, mcp__sanity__knowledge_base_read |

An endpoint can have one dataset source and several Knowledge Base sources. If it has both types, it serves the tools from both columns, and initial context contains both the schema overview and the Knowledge Base outline. See [Context MCP tools](https://www.sanity.io/docs/context/mcp-tools) and [Context MCP source types](https://www.sanity.io/docs/context/source-types).

## Troubleshoot common errors

These are the most common errors and how to fix them:

- `[MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///…/agent.ts is not specified and it doesn't parse as CommonJS.` (TypeScript): The agent still runs, but `package.json` doesn't set the module type. Run `npm pkg set type=module`.
- `Missing environment variable SANITY_CONTEXT_MCP_URL` (TypeScript) or `KeyError: 'SANITY_CONTEXT_MCP_URL'` (Python): The `.env` file is missing or wasn't loaded. In TypeScript, run with `--env-file=.env`. In Python, `load_dotenv()` looks in the script's folder, then in each parent folder.
- `Sanity Context MCP server status: failed`, after a 401 on the initial context fetch: The token is missing, invalid, or belongs to another organization. Check the organization ID in the URL too.
- 403 from the MCP connection with JSON-RPC error `-32007` on an endpoint with a dataset source, or `-32006` on an endpoint with only Knowledge Base sources, or the same 403 from the `/initial-context` fetch with code `contextGrantRequired` or `knowledgeBaseAccessDenied`. The messages are "Serving a dataset requires an organization API token with Context access ('sanity.knowledge-base.read'). Create one under your organization's API settings in Manage." (dataset source) and "No access to knowledge base '<knowledge base ID>'. Requires one of: sanity.knowledge-base.read." (Knowledge Base sources only; `<knowledge base ID>` is a placeholder for the Knowledge Base's ID): On an endpoint with a dataset source, the token isn't an organization API token with Context Viewer permissions. On an endpoint with only Knowledge Base sources, the token has no read access to the Knowledge Base the message names. Project tokens are refused however broad their permissions. Create an organization token as described in [Prerequisites](https://www.sanity.io/docs/context/connect-claude-agent-sdk).
- `Invalid API key` or `Not logged in · Please run /login`: Set `ANTHROPIC_API_KEY` in the `.env` file.
- The agent answers that permission to run `groq_query` was denied: The allowed tools list is missing, or names a different server than the key in the MCP servers config.
- `No matching distribution found for claude-agent-sdk`: Your Python is older than 3.10, the minimum the SDK supports. The Python that ships with macOS is 3.9, so install a newer one and recreate the virtual environment.

## Next steps

- [Sanity Context patterns and best practices](https://www.sanity.io/docs/context/best-practices): Production patterns for public assistants, personalized agents, and multi-backend setups.
- [Inline initial context into your system prompt](https://www.sanity.io/docs/context/inline-initial-context): Control what the inlined payload contains and keep it fresh in long-running agents.
- [Knowledge Bases](https://www.sanity.io/docs/context/knowledge-bases): Organize your datasets, website and files into verified facts, each cited to its source. Context flags conflicting sources for your team to resolve before agents see them.
- [Agent skills](https://www.sanity.io/docs/ai/skills): Install the Sanity Context skills, which walk a coding agent such as Claude Code through creating an endpoint and building an agent.

