A Board Game agent built using Sanity Context and Vercel's AI SDK
Written by Jarod Reyes
Missing Image!
I love board games. They are a very good excuse to entice my kids away from their books and spend some time with the parents. Plus I think I read somewhere that they increase neuroplasticity and I could use plenty more of that.
I decided to use Sanity Context to build an agent that recommends games based on my interests. Sanity Context is an MCP server. MCP (Model Context Protocol) is an open standard for connecting AI agents to external data and tools. Any agent that supports MCP can connect to it: Claude, GPT-4o, whatever you're building with for your next project. In this tutorial I'll walk through the key steps so you can follow along or use your favorite coding assistant to build something similar.
If you'd like to follow along with the full code files checkout this repo:
git clone https://github.com/sanity-labs/boardgame-agent-cliWhat we're actually building
Before we dive in any further let's see this thing in action:
I asked the agent, using GPT-4o, to recommend a new cooperative board game for my family that uses narrative story telling and city-building. When I tried this same query with OpenAI's latest GPT 5.5 model it was not able to find me a board game made later than 2023. Instead my agent recommended a top-rated game from this year called Cozy Stickerville.
Notice that my agent didn't guess. Behind the scenes it ran a GROQ query (GROQ is Sanity's open-source query language) against our Content Lake and returned a game that was released in 2026, has both mechanics tagged in their records, directly from BoardGameGeek's (BGG) API. Rad, let's build it.
The recipe
By the end of this tutorial you'll have:
- A Sanity project with a
boardGameschema, populated from BGG's XML API - A configured Context plugin for Sanity Studio that scopes an AI agent to your board game data
- A rather robust agent module that answers natural-language questions by running real GROQ queries against your own Content Lake (this is where you should spend the most time customizing).
For this demo specifically I wanted to focus on the build patterns of building an agent with Context and show that agents can live in different interfaces - which means there is no frontend. It's a CLI, ya' dig?
Prerequisites
- Node.js 20+ — nodejs.org. Run
node --versionto confirm. - A Sanity account — free at sanity.io
You can create an account usingnpm create sanity@latestas shown below - A BoardGameGeek XML API token — registration is required. Create an application at boardgamegeek.com/applications, then create a token and send it as
Authorization: Bearer …on every API request. See Using the XML API. - An OpenAI API key — the agent script uses GPT-4o by default. You can swap in any Vercel AI SDK provider.
- A deployed Sanity Studio — studio is where you configure Context for the agent.
Create a Sanity project
npm create sanity@latest -- --template clean --dataset production
--project-name=bgg-agent-tutorial --output-path bgg-agentFollow the prompts. When asked to install the MCP server, choose yes, it allows you and your agent to interact with Sanity's docs and tools directly.
Expected output:
✔ Running pnpm install
✅ Success! Your Studio has been created.
(cd ~/.../bgg-agent to navigate to your new project directory)
Get started by running pnpm dev to launch your Studio's development server
Other helpful commands:
npx sanity docs browse to open the documentation in a browser
npx sanity manage to open the project settings in a browser
npx sanity help to explore the CLI manuaYour project ID will now be in will now be in sanity.config.ts. Keep it handy, we'll need to add this to the .env file.
Or clone from GitHub: Clone the repo, run npm install, then run:
npx sanity init --envThis will walk you through logging in and selecting (or creating) a project, and automatically write your projectId and dataset to a .env file. Then copy any remaining variables from .env.example and update sanity.config.ts and sanity.cli.ts to match.
Define the board game schema
In Sanity, a schema is a TypeScript definition that describes the shape of your documents, what fields they have, what types those fields are, and how they're validated. It's the contract between your content and everything that reads it: Studio uses it to render the right editing form, your frontend uses it to know what to expect, and Context uses it to expose your data structure to the AI agent.
The default Studio template includes a placeholder schemaTypes/index.ts with a sample type. We're going to replace that with the actual boardGame document type and split it into its own file while we're at it, which is the convention for maintainable Sanity projects.
Create a new file at schemaTypes/documents/board-game.ts:
// schemaTypes/documents/board-game.ts
import {defineField, defineType, defineArrayMember} from 'sanity'
import {ControlsIcon} from '@sanity/icons'
export const boardGame = defineType({
name: 'boardGame',
title: 'Board Game',
type: 'document',
icon: ControlsIcon,
fields: [
defineField({
name: 'bggId',
title: 'BGG ID',
type: 'number',
validation: (rule) => rule.required(),
}),
defineField({
name: 'name',
title: 'Name',
type: 'string',
validation: (rule) => rule.required(),
}),
defineField({name: 'yearPublished', title: 'Year Published', type: 'number'}),
defineField({name: 'minPlayers', title: 'Min Players', type: 'number'}),
defineField({name: 'maxPlayers', title: 'Max Players', type: 'number'}),
defineField({name: 'minPlaytime', title: 'Min Playtime (min)', type: 'number'}),
defineField({name: 'maxPlaytime', title: 'Max Playtime (min)', type: 'number'}),
defineField({name: 'averageRating', title: 'BGG Average Rating', type: 'number'}),
defineField({name: 'weight', title: 'Complexity Weight (1–5)', type: 'number'}),
defineField({
name: 'categories',
title: 'Categories',
type: 'array',
of: [defineArrayMember({type: 'string'})],
}),
defineField({
name: 'mechanics',
title: 'Mechanics',
type: 'array',
of: [defineArrayMember({type: 'string'})],
}),
defineField({
name: 'designers',
title: 'Designers',
type: 'array',
of: [defineArrayMember({type: 'string'})],
}),
],
})Then update schemaTypes/index.ts to import from it:
import {boardGame} from './board-game'
export const schemaTypes = [boardGame]The mechanics and categories arrays are what make the GROQ queries genuinely useful later - they let the agent filter by structured tags rather than approximate text matching.
Next we need to deploy the schema to Content Lake so the Sanity Context server knows your data shape, we'll use the following sanity deploy command which has the added benefit of deploying our studio as well.
npx sanity deployPull board game data into Content Lake
Install the XML parsing package:
npm install fast-xml-parserCreate ingest.mjs at the project root. This is not the full file, but gives you the shape. You can see my version here: https://github.com/jarodreyes/boardgame-sanity-cli/blob/main/ingest.mjs
// ingest.mjs - Fetch top 50 games from BoardGameGeek using the XML API
import {getCliClient} from 'sanity/cli'
import {XMLParser} from 'fast-xml-parser'
// getCliClient reads projectId, dataset, and apiVersion from your sanity.config.ts
// automatically. Run this script with: sanity exec ingest.mjs --with-user-token
const client = getCliClient({useCdn: false})
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '@_',
isArray: (name) => ['item', 'name', 'link'].includes(name),
})
// add function to set Auth Headers for BGG API
// add function to batch request (20 IDs max per BGG call)
// add function to retry on fetch fail
...
// This is not the full code sample needed to handle ingesting BGG data.
// For the full file, which handles batching, auth, and
// a bunch of other specific nuances for the BGG API, view it at:
// https://github.com/jarodreyes/boardgame-sanity-cli/blob/main/ingest.mjsCreate a .env file at the project root. For Sanity, go to sanity.io/manage, open your project, click API → Tokens, and create one with Editor permissions. For BGG, use the bearer token from Applications → Tokens for your registered app.
SANITY_PROJECT_ID=your_project_id
BGG_API_TOKEN=your_bgg_bearer_tokenRun the ingestion:
sanity exec ingest.mjs --with-user-tokenExpected output:
Fetched 50 IDs from BGG hot list
Fetching details for 58 games...
Fetching game details batch 1/3 (20 games)...
Fetching game details batch 2/3 (20 games)...
Fetching game details batch 3/3 (18 games)...
Imported 58 board games into Content LakeBGG's thing endpoint accepts at most 20 IDs per request; the script batches automatically and waits 2 seconds between batches.
Start the Studio (npm run dev, then open localhost:3333). After the default small ingest you should see on the order of ~60 board games; each with ratings, complexity weights, mechanics, categories, player counts, playtime ranges, and designer credits from BGG.
Install Sanity Context
Sanity Context is an MCP server. Once configured in your Studio, it gives any MCP-compatible agent three tools to work with: initial_context (a compressed overview of your schema and document count), groq_query (live GROQ access to your Content Lake), and schema_explorer (field-level inspection so the agent builds accurate queries without guessing at field names).
Sanity ships a skill that automates the Context configuration. Run it from your coding assistant and it handles the plugin install, Context document creation, and MCP URL setup.
To set it up manually:
npm install @sanity/agent-contextOpen sanity.config.ts and add the plugin:
import {defineConfig} from 'sanity'
import {structureTool} from 'sanity/structure'
import {agentContextPlugin} from '@sanity/agent-context/studio'
import {schemaTypes} from './schemaTypes'
export default defineConfig({
name: 'default',
title: 'BGG Agent',
projectId: 'your-project-id',
dataset: 'production',
plugins: [structureTool(), agentContextPlugin()],
schema: {types: schemaTypes},
})Restart the Studio after the config change.
Create the Context document
In the Studio sidebar, you'll see a new Context section. Click it, then Create new Sanity Context. Fill in these fields:
Save the document. The Studio generates an MCP URL (the API path includes a date version, e.g. v2026-04-09 — use exactly what Studio shows, not a guess):
https://api.sanity.io/vYYYY-MM-DD/agent-context/your-project-id/production/board-gamesCopy it.
Connect the agent
Create agent.mjs at the project root:
// Requires: SANITY_CONTEXT_MCP_URL, SANITY_API_READ_TOKEN (Viewer), OPENAI_API_KEY
import 'dotenv/config'
import {randomUUID} from 'node:crypto'
import {generateText, stepCountIs} from 'ai'
import {createMCPClient} from '@ai-sdk/mcp'
import {openai} from '@ai-sdk/openai'
import {createClient} from '@sanity/client'
import {sanityInsightsIntegration} from './agent-insights-telemetry.mjs'
import boxen from 'boxen'
import chalk from 'chalk'The agent script depends on two more packages: the Vercel AI SDK (ai) and an OpenAI provider (@ai-sdk/openai). Install them with npm:
npm install ai @ai-sdk/openaiWorth pausing here on what an agent loop actually is. When you call generateText, the model runs in a loop rather than responding once and stopping. The model decides to call a tool, the SDK executes that tool against your MCP server, the result comes back to the model, and the model decides what to do next. That cycle continues until the model has enough information to give a final answer, or until it hits the stopWhen limit.
In this agent, that means the model is running live GROQ queries against your Content Lake mid-conversation, reading real results, and deciding whether it needs more data before responding. The retrieval happens inside the loop, driven by the model.
The OpenAI provider is the adapter that connects generateText to GPT-4o. The Vercel AI SDK supports other providers too, so if you'd rather use Anthropic or Gemini, swap the provider import and you're good.
Add three more variables to .env. Create a Sanity API token with Viewer permissions:
SANITY_CONTEXT_MCP_URL=<paste the full URL from the Sanity Context document in Studio>
SANITY_API_READ_TOKEN=your_viewer_token
OPENAI_API_KEY=your_openai_keyThis pattern works for your data too
Now you can start building your own agents on top of your content. Here are a few queries that show just how precise this gets:
npm run agent "Find games that combine Worker Placement with Deck Building"
npm run agent "What is the BGG complexity weight and average rating of Wingspan?"
npm run agent "Which game in the database has the most mechanics listed?"The agent runs a GROQ expression against real records, reads the result, and gives you an exact answer. It counts array lengths, finds maximums, traverses references, whatever your schema supports.
Swap out board games for a product catalog, a documentation site, a recipe database, or a content library. The architecture is the same. The agents you build for your users get smarter the more you invest in your content, not because a new model dropped, but because your data got better.
What's happening under the hood
When you ask the agent a question, it reaches the Sanity Context MCP server. The agent doesn't retrieve context and answer from memory. It runs queries, reads the results, and builds its response from live data. The instructions in the Context document guide how it frames and presents those results.
On top of all this, you can wire up any agent, using any AI you want, and have complete control of the experience for you or your end user/customer… all using Javascript/Typescript/language of your choice.
Your Sanity content becomes structured data the agent can query with the precision of a database.
I am proud to say that after the agent recommended Cozy Stickerville, I actually went and bought it, excited to play it with the fam tonight.
If you are building with Sanity Context or want help figuring out how to use it for your work join our Discord. We will be having some live sessions showing off agents built with Sanity and it'll be a great place to ask questions.