Connect Sanity Context with OpenAI Agents SDK
Connect Sanity Context to a Python or TypeScript agent using the OpenAI Agents SDK with built-in MCP support.
The OpenAI Agents SDK (openai-agents for Python, @openai/agents for TypeScript) builds agents with built-in MCP support and discovers MCP tools automatically. This guide shows you how to connect an Agents SDK agent to Sanity Context. You'll build a script that loads your schema or Knowledge Base outline into the agent's instructions, connects to your Context MCP endpoint, and answers a question about your content.
The same code works whether the endpoint uses Knowledge Base sources, a dataset source, or both. With Knowledge Base sources only, the tools are knowledge_base_search and knowledge_base_read instead of the dataset source tools, and the initial context is the Knowledge Base outline instead of your schema. With both source types, the endpoint serves the dataset source tools and the Knowledge Base source tools, and its initial context contains both the schema overview and the Knowledge Base outline. See Context MCP source types.
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, or expand Create the endpoint with the Sanity CLI (optional) at the end of this section.
- Organization API token: Create one in Manage under API > Tokens at the organization level. Under Organization permissions, select Context with the Viewer role. Project tokens are refused.
- OpenAI API key: The SDK reads it from
OPENAI_API_KEY. - Python 3.10 or later, required by
openai-agents, or Node.js 22.18 or later for TypeScript. Node.js 22.18 and later run.tsfiles directly.
Create the endpoint with the Sanity CLI (optional)
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:
# 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
# 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
# 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
# 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 for every flag.
Install dependencies
In a new project directory, install the Agents SDK for your language:
npm install @openai/agents zod
pnpm add @openai/agents zod
yarn add @openai/agents zod
bun add @openai/agents zod
zod is a required peer dependency of @openai/agents. If you type-check with tsc, set "skipLibCheck": true in tsconfig.json, because the SDK's own type files fail library checks. Set the module type in package.json so Node.js loads agent.ts as an ES module:
npm pkg set type=module
python3 -m venv .venv source .venv/bin/activate pip install openai-agents 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. In TypeScript, Node.js reads it with --env-file; in Python, load_dotenv() reads it. Replace each placeholder with your own value:
SANITY_CONTEXT_MCP_URL=YOUR_MCP_ENDPOINT_URL SANITY_ORGANIZATION_TOKEN=YOUR_ORGANIZATION_TOKEN OPENAI_API_KEY=YOUR_OPENAI_API_KEY
Create the agent
Create agent.ts or agent.py next to .env. The script works in three stages:
- Fetch initial context from the
/initial-contextHTTP endpoint and add it to the agent's instructions. The agent starts with a compressed schema or Knowledge Base outline, which saves a tool call on every conversation. For details, see Inline initial context into your system prompt. - Connect to the MCP endpoint with
MCPServerStreamableHttp, authenticating with your organization API token. The SDK discovers tools automatically when you pass the server inmcp_servers(Python) ormcpServers(TypeScript). A static tool filter,create_static_tool_filter(Python) orcreateMCPToolStaticFilter(TypeScript), blocks theinitial_contexttool because its payload is already in the instructions. - Create the agent and run it with
Runner.run(Python) orrun()(TypeScript). The agent makes tool calls as it explores your content. Without amodelargument,Agentuses the SDK's default model and reads credentials fromOPENAI_API_KEY, so a missing key fails at this stage, after the MCP connection has already succeeded.
import {Agent, MCPServerStreamableHttp, createMCPToolStaticFilter, run} from '@openai/agents'
const MCP_URL = process.env.SANITY_CONTEXT_MCP_URL
const API_TOKEN = process.env.SANITY_ORGANIZATION_TOKEN
if (!MCP_URL || !API_TOKEN) {
throw new Error('Set SANITY_CONTEXT_MCP_URL and SANITY_ORGANIZATION_TOKEN')
}
const headers = {Authorization: `Bearer ${API_TOKEN}`}
// 1. Fetch the initial context (schema overview or Knowledge Base outline).
async function fetchInitialContext(): Promise<string | null> {
const url = new URL(MCP_URL!)
url.pathname = `${url.pathname.replace(/\/$/, '')}/initial-context`
try {
const res = await fetch(url, {headers})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return await res.text()
} catch (err) {
console.warn(`Could not fetch initial context, the agent will call the tool instead: ${err}`)
return null
}
}
const initialContext = await fetchInitialContext()
let instructions = 'You are a helpful assistant.'
if (initialContext) {
instructions += `\n\n# Data reference\n\n${initialContext}`
}
// 2. Connect to the Sanity Context MCP server over Streamable HTTP.
const server = new MCPServerStreamableHttp({
name: 'sanity',
url: MCP_URL,
requestInit: {headers},
clientSessionTimeoutSeconds: 30, // default is 5s, tight for a large schema
// Hide initial_context only if its content is already in the instructions.
toolFilter: initialContext
? createMCPToolStaticFilter({blocked: ['initial_context']})
: undefined,
})
await server.connect()
try {
// 3. Create the agent and run it.
const agent = new Agent({
name: 'Assistant',
instructions,
mcpServers: [server],
})
const result = await run(agent, process.argv[2] ?? 'What content do we have?')
console.log(result.finalOutput)
} finally {
await server.close()
}Run it with a question. Node.js reads the .env file with --env-file:
node --env-file=.env agent.ts "Do you price match?"
A few details differ in the TypeScript SDK:
connect()andclose()are explicit calls, so the example wraps the run intry/finallyinstead of a context manager.- The Bearer header goes in
requestInit.headers. That option is untyped, so TypeScript doesn't catch a typo there. createMCPToolStaticFiltertakesblocked, not Python'sblocked_tool_names.- There are two timeouts.
clientSessionTimeoutSeconds(default 5) covers connecting and listing tools;timeout(default 60,000 ms) covers each tool call. - The SDK lists tools again on every turn. Set
cacheToolsList: trueto list them once per connection. - If the initial-context fetch fails, the TypeScript example logs a warning and keeps the
initial_contexttool, so the agent can call it instead. The Python example raises instead.
import asyncio
import os
from urllib.parse import urlparse, urlunparse
import httpx
from dotenv import load_dotenv
from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp, create_static_tool_filter
load_dotenv()
MCP_URL = os.environ["SANITY_CONTEXT_MCP_URL"]
API_TOKEN = os.environ["SANITY_ORGANIZATION_TOKEN"]
async def main():
# 1. Fetch initial context over HTTP
parsed = urlparse(MCP_URL)
initial_context_url = urlunparse(
parsed._replace(path=parsed.path.rstrip("/") + "/initial-context")
)
async with httpx.AsyncClient() as http:
resp = await http.get(
initial_context_url,
headers={"Authorization": f"Bearer {API_TOKEN}"},
)
resp.raise_for_status()
initial_context = resp.text
# 2. Connect to Sanity Context MCP
async with MCPServerStreamableHttp(
name="sanity",
params={
"url": MCP_URL,
"headers": {"Authorization": f"Bearer {API_TOKEN}"},
},
# The default is 5 seconds, which is tight for a large schema
client_session_timeout_seconds=30,
tool_filter=create_static_tool_filter(
blocked_tool_names=["initial_context"],
),
) as server:
# 3. Create the agent and run it
agent = Agent(
name="Assistant",
instructions=(
"You are a helpful assistant.\n\n"
"# Data reference\n\n"
"Use this to understand what's available and write better queries.\n\n"
+ initial_context
),
mcp_servers=[server],
)
result = await Runner.run(agent, "What content do we have?")
print(result.final_output)
asyncio.run(main())Run the script with the virtual environment active. load_dotenv() reads the .env file:
python agent.pyThe agent calls the Context MCP tools to explore your content, then prints its answer to the terminal. If the run fails, match the message against the lists in Troubleshoot common errors.
Troubleshoot common errors
These are the most common errors and how to fix them:
Error: Set SANITY_CONTEXT_MCP_URL and SANITY_ORGANIZATION_TOKEN: The environment variables aren't loaded. Run with--env-file=.env.SdkHttpError: Version negotiation failed: the server requires authorization (HTTP 401), afterCould not fetch initial context ... HTTP 401: The token is missing, invalid, or belongs to another organization.- 403 from the MCP connection with JSON-RPC error
-32007on an endpoint with a dataset source, or-32006on an endpoint with only Knowledge Base sources, or the same 403 from the/initial-contextfetch with codecontextGrantRequiredorknowledgeBaseAccessDenied. 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. Missing credentials. Please pass an `apiKey`, `workloadIdentity`, `adminAPIKey`, or set the `OPENAI_API_KEY` or `OPENAI_ADMIN_KEY` environment variable.: AddOPENAI_API_KEYto the.envfile. This surfaces atrun(), after the MCP connection has already succeeded.[MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///…/agent.ts is not specified and it doesn't parse as CommonJS.: The agent still runs, butpackage.jsondoesn't set the module type. Runnpm pkg set type=module.
KeyError: 'SANITY_CONTEXT_MCP_URL': The.envfile is missing, or isn't in the directory you run the script from.load_dotenv()returns without error when it finds no file.httpx.HTTPStatusError: Client error '401 Unauthorized'raised byresp.raise_for_status(): The value inSANITY_ORGANIZATION_TOKENisn't a valid organization API token.openai.OpenAIError: Missing credentials. Please pass an `api_key`, `workload_identity`, `admin_api_key`, or set the `OPENAI_API_KEY` or `OPENAI_ADMIN_KEY` environment variable.: This surfaces atRunner.run, after the MCP connection and the initial-context fetch have already succeeded.httpx.HTTPStatusError: Client error '403 Forbidden'from the/initial-contextfetch, with codecontextGrantRequiredin the response body: The token isn't an organization API token with Context Viewer permissions. Project tokens are refused however broad their permissions. Create an organization token as described in Prerequisites. A 403 that names a Knowledge Base means the token can't read a Knowledge Base the endpoint serves.
Next steps
- Sanity Context patterns and best practices: Production patterns for public assistants, personalized agents, and multi-backend setups.
- Inline initial context into your system prompt: Control what the inlined payload contains and keep it fresh in long-running agents.
- Add insights to Sanity Context: Track and analyze agent conversations.
- AI shopping assistant walkthrough: A full reference implementation using Next.js and the Vercel AI SDK.