> 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 Google ADK

Connect Sanity Context to a Python or TypeScript agent built with Google's Agent Development Kit (ADK).

Google's [Agent Development Kit (ADK)](https://adk.dev) is a framework for building AI agents in Python and TypeScript. This guide shows you how to connect an ADK agent to [Sanity Context](https://www.sanity.io/docs/context/introduction). You'll build an agent that loads your schema or Knowledge Base outline into its instruction, connects to your Context MCP endpoint over Streamable HTTP, and answers questions 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.
- **Gemini API key**: ADK reads it from `GOOGLE_API_KEY`. Get one from [Google AI Studio](https://aistudio.google.com/apikey). To use Vertex AI instead, set `GOOGLE_GENAI_USE_VERTEXAI=TRUE`, `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION`.
- **Node.js 22.18 or later** for TypeScript, or **Python 3.11 or later** for Python. Node.js 22.18 and later run `.ts` files directly. On macOS with Python 3.14 or later, see the watchdog entry in [Troubleshoot common errors](https://www.sanity.io/docs/context/connect-google-adk) before you 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 @google/adk @modelcontextprotocol/sdk
npm install -D @google/adk-devtools
```

**pnpm**

```shell
pnpm add @google/adk @modelcontextprotocol/sdk
pnpm add -D @google/adk-devtools
```

**yarn**

```shell
yarn add @google/adk @modelcontextprotocol/sdk
yarn add --dev @google/adk-devtools
```

**bun**

```shell
bun add @google/adk @modelcontextprotocol/sdk
bun add --dev @google/adk-devtools
```

`@modelcontextprotocol/sdk` is an optional peer dependency of `@google/adk`, so install it yourself. `@google/adk-devtools` provides the `adk` command. Set the module type in `package.json` so Node.js loads `agent.ts` as an ES module:

**CLI**

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

If you type-check with `tsc`, set `"skipLibCheck": true` in `tsconfig.json`. ADK's own type files report errors, though the example code type-checks.



#### Python

**CLI**

```sh
python3 -m venv .venv
source .venv/bin/activate
pip install "google-adk[mcp]"
```

Install into a virtual environment. On recent Debian, Ubuntu and Homebrew Python installs, `pip install` against the system Python fails with `externally-managed-environment`. The `[mcp]` extra installs ADK's MCP support. Without it, importing `McpToolset` fails. `httpx` and `python-dotenv` come with ADK.



## Set environment variables

Create a `.env` file. For TypeScript, put it next to `agent.ts`. For Python, put it inside the agent folder (`sanity_agent/.env`), where both `adk run` and `load_dotenv()` look. Replace each placeholder with your own value:

**.env**

```sh
SANITY_CONTEXT_MCP_URL=YOUR_MCP_ENDPOINT_URL
SANITY_ORGANIZATION_TOKEN=YOUR_ORGANIZATION_TOKEN
GOOGLE_API_KEY=YOUR_GEMINI_API_KEY
```

## Create the agent

Create the agent in `agent.ts` (TypeScript) or `sanity_agent/agent.py` (Python). The file exports the agent as `rootAgent` (TypeScript) or `root_agent` (Python) so the `adk` command can load it. The agent works in three stages:

1. **Fetch initial context** from the `/initial-context` HTTP endpoint and add it to the instruction. 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. In Python, a 401 or 403 stops the script here, because ADK would otherwise run the agent without the toolset that failed to load. 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** with `MCPToolset` (TypeScript) or `McpToolset` (Python) over Streamable HTTP, authenticated with your organization token. The tool filter hides `initial_context` only when its content is already in the instruction. If the fetch failed, the agent keeps the tool and calls it instead. In Python, `timeout=30` raises the 5-second default for connecting.
3. **Run the agent with a function instruction**. ADK replaces `{name}` placeholders in string instructions with session state, and GROQ projections in the initial context, such as `{title}`, would raise an error. In TypeScript, model errors such as an invalid API key arrive as events, so the loop checks `event.errorMessage`.

#### TypeScript

**agent.ts**

```typescript
import {fileURLToPath} from 'node:url'
import {InMemoryRunner, isFinalResponse, LlmAgent, MCPToolset, stringifyContent} from '@google/adk'

const mcpUrl = process.env.SANITY_CONTEXT_MCP_URL
const token = process.env.SANITY_ORGANIZATION_TOKEN
if (!mcpUrl || !token) {
  throw new Error('Set SANITY_CONTEXT_MCP_URL and SANITY_ORGANIZATION_TOKEN.')
}
const headers = {Authorization: `Bearer ${token}`}

// Fetch the schema overview (dataset source) or Knowledge Base outline (Knowledge Base sources).
async function fetchInitialContext(): Promise<string | undefined> {
  const url = new URL(mcpUrl!)
  url.pathname = `${url.pathname.replace(/\/$/, '')}/initial-context`
  try {
    const response = await fetch(url, {headers, signal: AbortSignal.timeout(30_000)})
    if (!response.ok) throw new Error(`${response.status} ${response.statusText}`)
    return await response.text()
  } catch (error) {
    console.warn('Could not fetch initial context:', error)
    return undefined
  }
}

const initialContext = await fetchInitialContext()

let instruction = `You answer questions using the Sanity Context tools.
Use the tools to look up content before you answer, and don't make up facts.`

if (initialContext) {
  instruction += `\n\n# Data reference\n\n${initialContext}`
}

const sanityContext = new MCPToolset(
  {
    type: 'StreamableHTTPConnectionParams',
    url: mcpUrl,
    transportOptions: {requestInit: {headers}},
  },
  // The initial context is already in the instruction, so hide the tool
  // that returns it. Keep it if the fetch failed.
  (tool) => !(initialContext && tool.name === 'initial_context'),
)

export const rootAgent = new LlmAgent({
  name: 'sanity_context_agent',
  model: 'gemini-flash-latest',
  // A function instruction skips ADK's {placeholder} state injection, which
  // would otherwise fail on GROQ projections like {title} in the reference.
  instruction: () => instruction,
  tools: [sanityContext],
})

async function main() {
  const runner = new InMemoryRunner({agent: rootAgent, appName: 'sanity_context'})
  for await (const event of runner.runEphemeral({
    userId: 'user',
    newMessage: {role: 'user', parts: [{text: process.argv[2] ?? 'What content do we have?'}]},
  })) {
    // Model errors (like an invalid API key) arrive as events, not exceptions.
    if (event.errorMessage) {
      throw new Error(`${event.errorCode}: ${event.errorMessage}`)
    }
    if (isFinalResponse(event) && event.content?.parts?.length) {
      console.log(stringifyContent(event))
    }
  }
}

// Run main() only when executed directly, not when the adk command imports the file.
if (process.argv[1] === fileURLToPath(import.meta.url)) {
  await main()
}
```



#### Python

**sanity_agent/agent.py**

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

import httpx
from dotenv import load_dotenv
from google.adk.agents import Agent
from google.adk.agents.readonly_context import ReadonlyContext
from google.adk.runners import InMemoryRunner
from google.adk.tools import BaseTool
from google.adk.tools.mcp_tool import McpToolset, StreamableHTTPConnectionParams
from google.genai import types

load_dotenv()

logger = logging.getLogger(__name__)

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


def fetch_initial_context() -> str | None:
    """Fetch the schema overview (dataset source) or Knowledge Base outline (Knowledge Base sources)."""
    url = urlsplit(MCP_URL)
    url = url._replace(path=url.path.rstrip("/") + "/initial-context")
    try:
        response = httpx.get(urlunsplit(url), headers=HEADERS, timeout=30)
        response.raise_for_status()
        return response.text
    except httpx.HTTPStatusError as error:
        # A bad token fails the MCP connection too, and ADK would then run the
        # agent without any Sanity tools. Stop here instead.
        if error.response.status_code in (401, 403):
            raise RuntimeError(
                f"Sanity Context rejected the token ({error.response.status_code}): {error.response.text}"
            ) from error
        logger.warning("Could not fetch initial context: %s", error)
        return None
    except httpx.HTTPError as error:
        logger.warning("Could not fetch initial context: %s", error)
        return None


initial_context = fetch_initial_context()

INSTRUCTION = """You answer questions using the Sanity Context tools.
Use the tools to look up content before you answer, and don't make up facts."""

if initial_context:
    INSTRUCTION += f"\n\n# Data reference\n\n{initial_context}"


def instruction(ctx: ReadonlyContext) -> str:
    # A function instruction skips ADK's {placeholder} state injection, which
    # would otherwise fail on GROQ projections like {title} in the reference.
    return INSTRUCTION


def skip_initial_context(tool: BaseTool, ctx: ReadonlyContext | None = None) -> bool:
    # The initial context is already in the instruction, so hide the tool
    # that returns it. Keep it if the fetch failed.
    return not (initial_context and tool.name == "initial_context")


sanity_context = McpToolset(
    connection_params=StreamableHTTPConnectionParams(
        url=MCP_URL,
        headers=HEADERS,
        timeout=30,
    ),
    tool_filter=skip_initial_context,
)

root_agent = Agent(
    name="sanity_context_agent",
    model="gemini-flash-latest",
    instruction=instruction,
    tools=[sanity_context],
)


async def main() -> None:
    runner = InMemoryRunner(agent=root_agent, app_name="sanity_context")
    session = await runner.session_service.create_session(
        app_name=runner.app_name, user_id="user"
    )
    question = sys.argv[1] if len(sys.argv) > 1 else "What content do we have?"
    message = types.Content(role="user", parts=[types.Part(text=question)])
    try:
        async for event in runner.run_async(
            user_id="user", session_id=session.id, new_message=message
        ):
            if event.is_final_response() and event.content and event.content.parts:
                print("".join(p.text for p in event.content.parts if p.text and not p.thought))
    finally:
        await runner.close()


if __name__ == "__main__":
    asyncio.run(main())
```

ADK loads agents as packages, so add an `__init__.py` next to `agent.py`:

**sanity_agent/__init__.py**

```python
from . import agent
```



## Run the agent

Pass a question as the first argument, or leave it out to ask "What content do we have?" To chat in the terminal instead, use `adk run`.

#### TypeScript

**npm**

```shell
# One question, then exit. Node reads the .env file with --env-file.
node --env-file=.env agent.ts "What is your return window?"

# Chat in the terminal (reads .env from the current folder)
npx adk run agent.ts
```

**pnpm**

```shell
# One question, then exit. Node reads the .env file with --env-file.
node --env-file=.env agent.ts "What is your return window?"

# Chat in the terminal (reads .env from the current folder)
pnpm dlx adk run agent.ts
```

**yarn**

```shell
# One question, then exit. Node reads the .env file with --env-file.
node --env-file=.env agent.ts "What is your return window?"

# Chat in the terminal (reads .env from the current folder)
yarn dlx adk run agent.ts
```

**bun**

```shell
# One question, then exit. Node reads the .env file with --env-file.
node --env-file=.env agent.ts "What is your return window?"

# Chat in the terminal (reads .env from the current folder)
bunx adk run agent.ts
```



#### Python

Run these from the folder that contains `sanity_agent/`:

**CLI**

```sh
# One question, then exit
python sanity_agent/agent.py "What is your return window?"

# Chat in the terminal
adk run sanity_agent
```



The script prints the agent's answer and exits. With the initial context in the instruction, a question like "What content do you have access to?" gets an answer without a tool call first. If the run fails, match the message against the list in [Troubleshoot common errors](https://www.sanity.io/docs/context/connect-google-adk).

## 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 | schema_explorer, groq_query, array_field_reader | knowledge_base_search, 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:

- `Error: Set SANITY_CONTEXT_MCP_URL and SANITY_ORGANIZATION_TOKEN.` (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, put it inside `sanity_agent/`.
- `[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`.
- `Failed to create MCP session: ... (HTTP 401)` (TypeScript) or `RuntimeError: Sanity Context rejected the token (401)` (Python): 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 with the Context permission.
- `Error: API key must be provided via constructor or the GOOGLE_GENAI_API_KEY, GOOGLE_API_KEY or GEMINI_API_KEY environment variable.` (TypeScript) or `ValueError: No API key was provided.` (Python): Add `GOOGLE_API_KEY` to the `.env` file.
- `Error: Context variable not found: `title`.` (TypeScript) or `KeyError: "Context variable not found: `title` in agent 'sanity_context_agent'."` (Python): The instruction was changed to a plain string. Keep it a function.
- `Failed building wheel for watchdog`: ADK pins watchdog 6, which has no prebuilt macOS wheel for Python 3.14 or later, so pip has to compile it. watchdog 7 installs without compiling and works with ADK. Install ADK with uv and override the pin: create `overrides.txt` containing `watchdog>=7`, then run `uv pip install "google-adk[mcp]" --override overrides.txt`.

Python may also log `Failed to configure mTLS using AsyncAuthorizedSession`. It's harmless; set `GOOGLE_API_USE_CLIENT_CERTIFICATE=false` to silence it. TypeScript ADK prints its own log lines to stdout alongside the answer, so filter them if you pipe the output.

## 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.
- [ADK MCP tools](https://adk.dev/tools-custom/mcp-tools/): More on the MCP toolset, including filtering and connection options.

