> 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 Mastra

Connect Sanity Context to a TypeScript agent built with Mastra, using MCPClient from @mastra/mcp.

[Mastra](https://mastra.ai) is a TypeScript framework for building AI agents. This guide shows you how to connect a Mastra agent to [Sanity Context](https://www.sanity.io/docs/context/introduction), first as a standalone script and then as an agent registered in a Mastra project. `MCPClient` from `@mastra/mcp` connects to the Context MCP endpoint over Streamable HTTP and hands its tools to the agent. 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 are refused.
- **Anthropic API key**: Mastra's model router reads it from `ANTHROPIC_API_KEY`. Any provider the router supports works; change the model string and set that provider's key.
- **Node.js 22.18 or later**: Mastra requires Node.js 22.13 or later, and 22.18 runs `.ts` files directly.

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

In a new project directory, install Mastra, its MCP client, and Zod. The TypeScript packages are only needed for type checking:

**npm**

```shell
npm install @mastra/core@^1 @mastra/mcp@^2 zod@^4
npm install -D typescript @types/node
```

**pnpm**

```shell
pnpm add @mastra/core@^1 @mastra/mcp@^2 zod@^4
pnpm add -D typescript @types/node
```

**yarn**

```shell
yarn add @mastra/core@^1 @mastra/mcp@^2 zod@^4
yarn add --dev typescript @types/node
```

**bun**

```shell
bun add @mastra/core@^1 @mastra/mcp@^2 zod@^4
bun add --dev typescript @types/node
```

`@mastra/mcp` 2 requires `zod` 4.2 or later as a peer dependency. Set the module type in `package.json` so Node.js loads `agent.ts` as an ES module:

**CLI**

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

For type checking in your editor or with `npx tsc`, add a `tsconfig.json`. TypeScript 6 and later don't load `@types` packages automatically, so list Node's types with `"types": ["node"]` or TypeScript reports `Cannot find name 'process'`. Bundler module resolution matches a Mastra project, where imports don't need file extensions.

**tsconfig.json**

```json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "es2022",
    "moduleResolution": "bundler",
    "strict": true,
    "types": ["node"],
    "noEmit": true,
    "skipLibCheck": true
  }
}
```

## Set environment variables

Create a `.env` file next to `agent.ts`, or in the project root of a Mastra project. 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` next to `.env`. The standalone script works in three stages:

1. **Fetch initial context** from the `/initial-context` HTTP endpoint and add it to the instructions. 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** with `MCPClient`, authenticated with your organization token. `listToolsWithErrors()` returns the tools plus any connection error. Plain `listTools()` logs a failed connection and returns no tools, so the agent would answer without your content.
3. **Drop the initial context tool and create the agent**. Mastra prefixes each tool name with the server key, so the tool is `sanity_initial_context`. The script removes it only when the fetch succeeded; otherwise the agent keeps the tool and calls it instead. Mastra agents stop after 5 steps by default, which can cut off a search and return an empty answer with no error, so the agent sets `defaultOptions: {maxSteps: 10}`. The option applies to `generate()`, `stream()`, and Mastra Studio.

**agent.ts**

```typescript
import {Agent} from '@mastra/core/agent'
import {MCPClient} from '@mastra/mcp'

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}`}

// 1. Fetch the initial context: a schema overview (dataset source)
//    or a Knowledge Base outline (Knowledge Base sources).
const url = new URL(mcpUrl)
url.pathname = url.pathname.replace(/\/$/, '') + '/initial-context'
let initialContext: string | undefined
try {
  const res = await fetch(url, {headers, signal: AbortSignal.timeout(10_000)})
  if (res.ok) initialContext = await res.text()
  else console.warn(`initial-context returned ${res.status}; continuing without it`)
} catch (err) {
  console.warn('Could not fetch initial context; continuing without it:', err)
}

// 2. Connect to the Sanity Context MCP server over Streamable HTTP.
const mcp = new MCPClient({
  servers: {
    sanity: {
      url: new URL(mcpUrl),
      requestInit: {headers},
    },
  },
})

// Tool names are prefixed with the server key: sanity_groq_query, ...
// listToolsWithErrors() reports a failed connection instead of returning no tools.
const {tools, errors} = await mcp.listToolsWithErrors()
if (errors.sanity) {
  await mcp.disconnect()
  throw new Error(`Could not connect to Sanity Context: ${errors.sanity}`)
}

// 3. The schema or outline is already in the instructions, so drop the
//    tool that returns it. Keep it if the fetch failed.
if (initialContext) delete tools['sanity_initial_context']

const agent = new Agent({
  id: 'docs-agent',
  name: 'Docs Agent',
  instructions: [
    'You answer questions using content from Sanity. Use the available tools to look things up.',
    initialContext ? `# Data reference\n\n${initialContext}` : '',
  ].join('\n\n'),
  model: 'anthropic/claude-sonnet-5-5',
  // Mastra stops after 5 steps by default. A question that needs several lookups can hit the
  // limit mid-search and return an empty answer.
  defaultOptions: {maxSteps: 10},
  tools,
})

try {
  const result = await agent.generate(process.argv[2] ?? 'What content do we have?')
  console.log(result.text)
} finally {
  await mcp.disconnect()
}
```

## Run the agent

Run the script. Node.js reads the `.env` file with `--env-file`. Pass a question as the first argument, or leave it out to ask "What content do we have?"

**CLI**

```sh
node --env-file=.env agent.ts "How long do I have to return something?"
```

The agent looks up the answer with the Context MCP tools and prints it to the terminal. With the initial context in its instructions, it answers "What content do you have access to?" without calling a tool first. If the run fails, match the message against the list in [Troubleshoot common errors](https://www.sanity.io/docs/context/connect-mastra).

## Add the agent to a Mastra project

In a Mastra project, register the agent so it appears in Mastra Studio and the server routes. Instead of fetching at import time, the agent fetches the initial context on first use and reuses it. Both `instructions` and `tools` accept async functions.

**src/mastra/agents/docs-agent.ts**

```typescript
import {Agent} from '@mastra/core/agent'
import {MCPClient} from '@mastra/mcp'

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}`}

export const sanityMcp = new MCPClient({
  id: 'sanity-mcp',
  servers: {
    sanity: {
      url: new URL(mcpUrl),
      requestInit: {headers},
    },
  },
})

// Fetch the schema overview or Knowledge Base outline once, on first use,
// and reuse it for every request.
let initialContext: Promise<string | undefined> | undefined
function getInitialContext() {
  initialContext ??= (async () => {
    const url = new URL(mcpUrl!)
    url.pathname = url.pathname.replace(/\/$/, '') + '/initial-context'
    try {
      const res = await fetch(url, {headers, signal: AbortSignal.timeout(10_000)})
      if (res.ok) return await res.text()
      console.warn(`initial-context returned ${res.status}; continuing without it`)
    } catch (err) {
      console.warn('Could not fetch initial context; continuing without it:', err)
    }
    return undefined
  })()
  return initialContext
}

export const docsAgent = new Agent({
  id: 'docs-agent',
  name: 'Docs Agent',
  instructions: async () => {
    const context = await getInitialContext()
    return [
      'You answer questions using content from Sanity. Use the available tools to look things up.',
      context ? `# Data reference\n\n${context}` : '',
    ].join('\n\n')
  },
  model: 'anthropic/claude-sonnet-5-5',
  // Mastra stops after 5 steps by default. A question that needs several lookups can hit the
  // limit mid-search and return an empty answer.
  defaultOptions: {maxSteps: 10},
  tools: async () => {
    const {tools, errors} = await sanityMcp.listToolsWithErrors()
    if (errors.sanity) {
      throw new Error(`Could not connect to Sanity Context: ${errors.sanity}`)
    }
    // The instructions already carry the initial context, so drop the tool
    // that returns it. Keep it if the fetch failed.
    if (await getInitialContext()) delete tools['sanity_initial_context']
    return tools
  },
})
```

**src/mastra/index.ts**

```typescript
import {Mastra} from '@mastra/core'
import {docsAgent} from './agents/docs-agent'

export const mastra = new Mastra({
  agents: {docsAgent},
})
```

Run `npx mastra dev`. It loads `.env` from the project root, and **Docs Agent** appears in Mastra Studio with the Sanity tools. A project made with `create-mastra` already includes the `mastra` CLI. Otherwise, add it with `npm install -D mastra`.

The cached initial context lasts for the life of the process, so restart the server to pick up changes to the endpoint's Instructions, or replace the cache with one that expires after a few minutes.

### One agent per endpoint

Each Context endpoint has its own content source, GROQ filter, and Instructions. Several endpoints can serve the same Knowledge Base, so you can give each agent only the content meant for it. Make one `MCPClient` per agent, so listing one agent's tools doesn't connect to every endpoint:

**src/mastra/agents/create-sanity-context-agent.ts**

```typescript
import {Agent} from '@mastra/core/agent'
import {MCPClient} from '@mastra/mcp'

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

export function createSanityContextAgent(opts: {
  id: string
  name: string
  mcpUrl: string
  instructions: string
}) {
  const mcp = new MCPClient({
    id: `${opts.id}-mcp`,
    servers: {sanity: {url: new URL(opts.mcpUrl), requestInit: {headers}}},
  })

  let initialContext: Promise<string | undefined> | undefined
  const getInitialContext = () =>
    (initialContext ??= (async () => {
      const url = new URL(opts.mcpUrl)
      url.pathname = url.pathname.replace(/\/$/, '') + '/initial-context'
      try {
        const res = await fetch(url, {headers, signal: AbortSignal.timeout(10_000)})
        if (res.ok) return await res.text()
        console.warn(`${opts.id}: initial-context returned ${res.status}`)
      } catch (err) {
        console.warn(`${opts.id}: could not fetch initial context:`, err)
      }
      return undefined
    })())

  const agent = new Agent({
    id: opts.id,
    name: opts.name,
    instructions: async () => {
      const context = await getInitialContext()
      return [opts.instructions, context ? `# Data reference\n\n${context}` : ''].join('\n\n')
    },
    model: 'anthropic/claude-sonnet-5-5',
    // Mastra stops after 5 steps by default, which can cut off a search mid-way
    defaultOptions: {maxSteps: 10},
    tools: async () => {
      const {tools, errors} = await mcp.listToolsWithErrors()
      if (errors.sanity) throw new Error(`Could not connect to Sanity Context: ${errors.sanity}`)
      if (await getInitialContext()) delete tools['sanity_initial_context']
      return tools
    },
  })
  return {agent, mcp}
}
```

Then register one agent per endpoint, for example a support agent on a Knowledge Base endpoint and a catalog agent on a dataset endpoint. Add each endpoint URL to `.env`, here as `SANITY_SUPPORT_MCP_URL` and `SANITY_CATALOG_MCP_URL`:

**src/mastra/index.ts**

```typescript
import {Mastra} from '@mastra/core'
import {createSanityContextAgent} from './agents/create-sanity-context-agent'

const support = createSanityContextAgent({
  id: 'support-agent',
  name: 'Support Agent',
  mcpUrl: process.env.SANITY_SUPPORT_MCP_URL!,
  instructions: 'You answer customer support questions. Cite the entry you used.',
})

const catalog = createSanityContextAgent({
  id: 'catalog-agent',
  name: 'Catalog Agent',
  mcpUrl: process.env.SANITY_CATALOG_MCP_URL!,
  instructions: 'You help shoppers find products. Only recommend products you found.',
})

export const mastra = new Mastra({
  agents: {supportAgent: support.agent, catalogAgent: catalog.agent},
})
```

To authenticate each request with a different token, such as one per customer, pass tools per call instead: create an `MCPClient` with a unique `id` for the request, call `listToolsetsWithErrors()`, and pass the result as `agent.generate(prompt, {toolsets})`. Toolset keys aren't prefixed, so remove `toolsets.sanity.initial_context`.

## 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 | sanity_schema_explorer, sanity_groq_query, sanity_array_field_reader | sanity_knowledge_base_search, 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.`: The script still runs, but `package.json` doesn't set the module type. Run `npm pkg set type=module`.
- `Error: Set SANITY_CONTEXT_MCP_URL and SANITY_ORGANIZATION_TOKEN`: The `.env` file is missing or wasn't loaded. Run the script with `--env-file=.env`, or check that `.env` is in the project root for `mastra dev`.
- `Could not connect to Sanity Context: Failed to connect to MCP server sanity: ... (HTTP 401)`, after `initial-context returned 401`: The token is missing, invalid, or belongs to another organization. Check the organization ID in the URL too. Mastra logs the raw connection error a few times before the script throws.
- 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, as described in [Prerequisites](https://www.sanity.io/docs/context/connect-mastra).
- `Could not find API key process.env.ANTHROPIC_API_KEY for model id anthropic/claude-sonnet-5-5`: Add `ANTHROPIC_API_KEY` to the `.env` file, or the key for the provider in your model string.
- `MCPClient was initialized multiple times with the same configuration options.`: Give each `MCPClient` its own `id`.
- The answer is empty after several tool calls: The agent hit Mastra's default limit of 5 steps while still searching. Set `defaultOptions: {maxSteps: 10}` on the agent.

## 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.
- [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.
- [Build a Knowledge Base for Agents (with Sanity)](https://mastra.ai/workshops/build-a-knowledge-base-for-agents-with-sanity): The Mastra workshop recording, with one Knowledge Base shared across scoped agents.

