A Board Game agent built using Sanity Context and Vercel's AI SDK Jarod Reyes built a CLI board game recommendation agent using Sanity Context and Vercel's AI SDK. The agent queries real-time data from BoardGameGeek via Sanity's Content Lake to recommend games based on natural-language requests. The project demonstrates how MCP servers enable AI agents to access external data for accurate, up-to-date responses. A Board Game agent built using Sanity Context and Vercel's AI SDK An OpenAI Agent, a board game API, and Sanity Context walk into a bar… the result: a CLI agent that returns exact game recs with real-time data using queries Jarod Reyes Head of Developer Experience & Community at Sanity Published 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 /docs/ai/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-cli What 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 /docs/content-lake/groq-introduction GROQ is Sanity's open-source query language /glossary/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 boardGame schema, 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 /glossary/node-js 20+ nodejs.org https://nodejs.org . Run node --version to confirm. A Sanity account — free at sanity.io / You can create an account usingas shown below npm create sanity@latest A BoardGameGeek XML API token — registration is required. Create an application at boardgamegeek.com/applications https://boardgamegeek.com/applications , then create a token and send it as Authorization: Bearer … on every API request. See Using the XML API https://boardgamegeek.com/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 https://sdk.vercel.ai/providers/ai-sdk-providers .- 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-agent Follow 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 manua Your 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 --env This 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 : js // 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: js 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 deploy Pull board game data into Content Lake Install the XML parsing package: npm install fast-xml-parser Create 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 https://github.com/jarodreyes/boardgame-sanity-cli/blob/main/ingest.mjs js // 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.mjs Create 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 https://boardgamegeek.com/applications for your registered app. SANITY PROJECT ID=your project id BGG API TOKEN=your bgg bearer token Run the ingestion: sanity exec ingest.mjs --with-user-token Why we use getCliClient getCliClient picks up your projectId, dataset, and apiVersion directly from sanity.config.ts — no duplication. The --with-user-tokenflag passes your active sanity login session to the script, so you don't need a separate SANITY API TOKEN environment variable for local ingestion runs. You only need an API token when running in a non-interactive environment like CI. Expected 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 Lake BGG'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 /docs/ai/skills 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-context Open sanity.config.ts and add the plugin: js 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-games Copy 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' AGENT SYSTEM PROMPT The system prompt is worth understanding because it controls how the agent queries your data. It sets the query strategy what to try first, how to widen if a query returns zero results , the BGG-specific field conventions exact tag strings like "Co-operative Play" rather than the casual phrase "co-op" , and the temporal logic how the agent interprets "new" or "recent" relative to the current date . The prompt in this repo was written with help from the Sanity Context skill and iterated against real queries. It's detailed because GROQ is precise. A vague "find cooperative games" won't match anything if the agent guesses at a field name. The tradeoff is token cost per call: a longer system prompt means slightly higher cost per query, but it cuts down on the number of follow-up tool calls the model needs to make. You'll want to customize this for your own data. The field names, array values, and fallback logic are all specific to the BGG dataset. Swap those out for your schema and you have a solid starting point. 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/openai Worth 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=