{"slug": "how-sanity-context-mcp-supercharged-ai-customer-support-in-basegent-bucket-space", "title": "How Sanity Context & MCP Supercharged AI Customer Support in Basegent & Bucket Space", "summary": "A developer built Basegent, an AI customer support platform, and integrated it with Sanity's Context MCP and Knowledge Base beta to replace traditional vector-search RAG with structured, live-queried policy documents. The integration uses a SanityContextSourceAdapter that calls Sanity's hosted MCP endpoint via JSON-RPC during chat sessions, paired with a multi-provider BYOK inference engine, and was piloted on the developer's Bucket Space bookmarking app.", "body_md": "*This is a submission for the [Sanity Challenge, Path One: Ship an Agent That Queries Real Content](https://dev.to/challenges/sanity-2026-09-16)*\n\nOver the past few months, I have been building **[Basegent](https://basegent.space)**, a modern AI customer support and operations platform. Basegent gives developers and SaaS teams an intelligent chat assistant that can resolve real customer problems, cite official documentation, and escalate smoothly to human operators when confidence drops.\n\nAt the same time, I run **[Bucket Space](https://mybucket.space)**—a modern web application built for smart bookmarking, content capture, and personal library organization.\n\nNaturally, I wanted Bucket to be the proving ground for Basegent. But as anyone who has deployed AI support bots in production knows, traditional Retrieval-Augmented Generation (RAG) has a dirty secret:\n\n**Traditional vector search treats company documentation like a bag of text chunks.** It slices policies into arbitrary paragraphs, calculates similarity embeddings, and hopes the LLM can resolve contradictions on the fly.\n\nIn real-world customer support, this naive approach fails catastrophically:\n\n`Free` vs. `Pro`), geographical market (` US` vs. `EU`), or deployment type (` standard` vs. `custom`). Similarity search has no concept of conditional logic.\nWhen Sanity announced the **Sanity Context MCP and Knowledge Base** beta in Sanity Labs, it clicked immediately. Sanity wasn't just offering another vector database; it was treating context as a managed, structured, verifiable source of truth with built-in conflict resolution and an open protocol (**Model Context Protocol - MCP**).\n\nI set out to connect the entire loop:\n\n`SanityContextSourceAdapter` directly into Basegent that talks to Sanity's hosted MCP endpoint, paired with a multi-provider Bring-Your-Own-Key (BYOK) inference engine.`@basegent/react` chat widget into Here is the story of how it works, how Sanity Context solved our hardest policy dilemmas, and how you can implement this pattern in your own stack.\n\n`production`.`@basegent/client`\nInstead of copying periodic data dumps or storing duplicate content, Basegent queries Sanity Context live during the chat conversation via MCP JSON-RPC:\n\n```\n[ Customer on Bucket Space (mybucket.space) ]\n                     │\n      Authenticated Query + Signed Token\n                     ▼\n       [ @basegent/react Chat Widget ]\n                     │\n                     ▼  SSE / WebSocket\n    [ Basegent Platform (basegent.space) ]\n                     │\n       ┌─────────────┴──────────────────────────┐\n       │ Multi-Provider BYOI / BYOK Engine      │\n       │ (Groq / OpenAI / Anthropic / Google)   │\n       └─────────────┬──────────────────────────┘\n                     │\n         1. Discover Outline (/initial-context)\n         2. Select Virtual Paths\n         3. Live MCP Tool Call (knowledge_base_read)\n                     ▼\n  [ Sanity Context MCP Endpoint (api.sanity.io) ]\n                     │\n   ┌─────────────────┴──────────────────────────┐\n   │ Sanity Knowledge Base (kb3NuTkXw21o)        │\n   │ - Reconciled Structured Policies           │\n   │ - Durable Standing Instructions            │\n   │ - Canonical Documentation Citations        │\n   └────────────────────────────────────────────┘\n                     │\n                     ▼\n[ Streamed Answer with Citations & 1-Click Human Escalation ]\n```\n\nTo power support for Bucket, I created a dedicated project in Sanity Labs under organization **Miracle Onyenma** (`oL4FZOkGh`) with project ID `jk662cms` and dataset `production`.\n\nSupport policies shouldn't be plain blobs of text. In Sanity Studio, we modeled `supportPolicy` documents with explicit schema constraints:\n\n``` js\n// schemas/supportPolicy.ts\nimport { defineType, defineField } from \"sanity\";\n\nexport const supportPolicy = defineType({\n  name: \"supportPolicy\",\n  title: \"Support Policy & Guide\",\n  type: \"document\",\n  fields: [\n    defineField({ name: \"policyKey\", type: \"string\", title: \"Policy Key\" }),\n    defineField({ name: \"title\", type: \"string\", title: \"Title\" }),\n    defineField({ name: \"claim\", type: \"text\", title: \"Core Claim / Rule\" }),\n    defineField({\n      name: \"appliesTo\",\n      type: \"object\",\n      title: \"Applies To\",\n      fields: [\n        { name: \"plans\", type: \"array\", of: [{ type: \"string\" }] },\n        { name: \"markets\", type: \"array\", of: [{ type: \"string\" }] },\n        { name: \"productIds\", type: \"array\", of: [{ type: \"string\" }] },\n      ],\n    }),\n    defineField({ name: \"effectiveFrom\", type: \"datetime\", title: \"Effective From\" }),\n    defineField({ name: \"effectiveUntil\", type: \"datetime\", title: \"Effective Until\" }),\n    defineField({ name: \"priority\", type: \"number\", title: \"Precedence Priority (0-100)\" }),\n    defineField({\n      name: \"authority\",\n      type: \"string\",\n      options: { list: [\"canonical\", \"legacy\", \"advisory\"] },\n    }),\n    defineField({ name: \"supersedes\", type: \"reference\", to: [{ type: \"supportPolicy\" }] }),\n    defineField({ name: \"sourceUrl\", type: \"url\", title: \"Canonical Source URL\" }),\n  ],\n});\n```\n\nWe populated this dataset with real Bucket documentation alongside intentional edge cases:\n\n`priority: 100`, `authority: canonical`, effective 2026).` priority: 10`, `authority: legacy`, expired end of 2025).` priority: 90`).\nNext, in Sanity Context Lab, we built the **Basegent Support Policies** Knowledge Base (`kb3NuTkXw21o`).\n\nThis is where Sanity Context shines. Instead of silently averaging out conflicting statements, Sanity actively parsed the documents, analyzed the domain boundaries, and flagged critical ambiguities in the **Issues Review** dashboard.\n\nSanity flagged a direct conflict between the custom deployment exception (7 days) and the legacy global rule (14 days) regarding custom products:\n\nSanity highlighted that while the new policy claims to supersede the legacy policy, its applicability was strictly scoped to the US Pro tier, leaving free and non-US tiers potentially ambiguous:\n\nWith one click in the Sanity interface, we resolved the issue by selecting the source-backed canonical claim. Sanity compiled this decision into a **durable standing instruction** that automatically survives future dataset rebuilds!\n\nOnce verified, we generated an organization-level Context Viewer token and pointed Sanity's hosted Context MCP endpoint (`https://api.sanity.io/v1/context/organizations/oL4FZOkGh/mcp/context`) to this Knowledge Base.\n\nIn Basegent, every customer support environment lives in an isolated tenant called a **Workspace**. We created the dedicated **Bucket** workspace (`slug: bucket`) at `basegent.space/Account`:\n\nIn Basegent's Sources dashboard (`/sources/new`), we added **Sanity Context** as a first-party knowledge source, supplying:\n\n`kb3NuTkXw21o`)\nBasegent's core runtime defines a provider-neutral `ContentSourceAdapter`. To query Sanity live, we implemented `SanityContextSourceAdapter`:\n\n`/initial-context`)\nWhen Basegent compiles its retrieval registry, it asks Sanity Context for its virtual outline:\n\n```\n// lib/sources/sanity-context-adapter.ts\nexport interface SanityContextConfig {\n  mcpUrl: string;\n  knowledgeBaseId: string;\n  organizationToken: string;\n}\n\nexport async function fetchKnowledgeIndex(\n  config: SanityContextConfig,\n  connectionId: string,\n): Promise<string> {\n  const url = new URL(config.mcpUrl);\n  url.pathname = `${url.pathname.replace(/\\/$/, \"\")}/initial-context`;\n  url.searchParams.set(\"mode\", \"knowledge_base\");\n  url.searchParams.set(\"knowledgeBases\", config.knowledgeBaseId);\n\n  const response = await fetch(url, {\n    headers: { Authorization: `Bearer ${config.organizationToken}` },\n    signal: AbortSignal.timeout(15_000),\n  });\n\n  if (!response.ok) {\n    throw new Error(`Sanity Context discovery failed with HTTP ${response.status}`);\n  }\n\n  const initialContext = await response.text();\n  const entries = parseKnowledgeBaseEntries(initialContext, config.knowledgeBaseId);\n\n  return [\n    `# Sanity Context Knowledge Base (${config.knowledgeBaseId})`,\n    \"Available virtual outline paths for live retrieval:\",\n    ...entries.map(\n      (entry) => `- sanity-context/${connectionId}/${config.knowledgeBaseId}/${entry.path}`,\n    ),\n  ].join(\"\\n\");\n}\n```\n\nWhen a customer asks a question, Basegent selects relevant virtual paths and executes the `knowledge_base_read` tool live over HTTP:\n\n```\nexport async function callKnowledgeBaseRead(\n  config: SanityContextConfig,\n  path: string,\n): Promise<string> {\n  const response = await fetch(config.mcpUrl, {\n    method: \"POST\",\n    headers: {\n      Authorization: `Bearer ${config.organizationToken}`,\n      \"Content-Type\": \"application/json\",\n      Accept: \"application/json, text/event-stream\",\n    },\n    body: JSON.stringify({\n      jsonrpc: \"2.0\",\n      id: `basegent-${Date.now()}-${path}`,\n      method: \"tools/call\",\n      params: {\n        name: \"knowledge_base_read\",\n        arguments: {\n          knowledgeBase: config.knowledgeBaseId,\n          paths: [path],\n        },\n      },\n    }),\n    signal: AbortSignal.timeout(20_000),\n  });\n\n  if (!response.ok) {\n    throw new Error(`Sanity MCP call error: HTTP ${response.status}`);\n  }\n\n  const payload = await response.json();\n  if (payload.error) throw new Error(payload.error.message);\n\n  // Extract clean text content from the MCP response\n  const textContent = payload.result?.content\n    ?.filter((part: any) => part.type === \"text\")\n    .map((part: any) => part.text)\n    .join(\"\\n\");\n\n  return textContent || JSON.stringify(payload.result?.structuredContent, null, 2);\n}\n```\n\nCustomer support agents cannot afford downtime or regional API rate limits. Basegent features a **Bring Your Own Intelligence (BYOI / BYOK)** engine that lets teams connect API keys from multiple providers with automatic fallbacks:\n\n``` js\n// lib/ai/provider-resolver.ts\nexport const SUPPORTED_PROVIDERS = [\"groq\", \"openai\", \"anthropic\", \"google\"] as const;\n\nexport const PROVIDER_DEFAULT_MODELS = {\n  groq: { normal: \"openai/gpt-oss-120b\", budget: \"llama-3.3-70b-versatile\" },\n  openai: { normal: \"gpt-4o\", budget: \"gpt-4o-mini\" },\n  anthropic: { normal: \"claude-3-5-sonnet-latest\", budget: \"claude-3-5-haiku-latest\" },\n  google: { normal: \"gemini-2.0-flash\", budget: \"gemini-1.5-flash\" },\n};\n\nexport async function executeWithFallback<T>(\n  candidates: Array<{ provider: string; apiKey: string; modelId: string }>,\n  action: (model: any) => Promise<T>,\n): Promise<T> {\n  let lastError: unknown;\n\n  for (const candidate of candidates) {\n    try {\n      const model = initModel(candidate.provider, candidate.apiKey, candidate.modelId);\n      return await action(model);\n    } catch (err: any) {\n      lastError = err;\n      if (isRateLimitOrQuotaError(err)) {\n        console.warn(`[Basegent AI] ${candidate.provider} exhausted, failing over...`);\n        continue;\n      }\n      throw err;\n    }\n  }\n\n  throw lastError ?? new Error(\"All configured AI providers failed.\");\n}\n```\n\nWhether running lightning-fast inference on **Groq**, deep reasoning on **Claude 3.5 Sonnet**, or cost-efficient answers on **Gemini 2.0 Flash**, the underlying knowledge remains anchored in Sanity Context.\n\n`mybucket.space`)\nWith Sanity and Basegent connected, the final step was integrating the support assistant into [mybucket.space](https://mybucket.space).\n\nBasegent publishes pre-built React components and TypeScript clients directly to npm:\n\n```\nnpm install @basegent/react @basegent/client\n```\n\nWhen a signed-in user opens the chat, Bucket issues an HMAC-signed customer token via an internal API route. This informs Basegent of the user's plan tier, market, and registration timestamp without exposing private customer data:\n\n``` js\n// components/shared/BasegentWidget.tsx\n\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { BasegentProvider, BasegentChat } from \"@basegent/react\";\nimport { useAuth } from \"@/components/providers/auth-provider\";\n\nexport function BasegentWidget() {\n  const { user } = useAuth();\n  const [customerToken, setCustomerToken] = useState<string | undefined>();\n\n  useEffect(() => {\n    if (!user || user.isAnonymous) return;\n\n    // Retrieve signed JWT/HMAC token with verified plan & market attributes\n    fetch(\"/api/support/basegent-token\", { method: \"POST\" })\n      .then((res) => res.json())\n      .then((data) => setCustomerToken(data.token))\n      .catch((err) => console.error(\"Could not sign Basegent token:\", err));\n  }, [user]);\n\n  return (\n    <BasegentProvider\n      tenantId={process.env.NEXT_PUBLIC_BASEGENT_TENANT_ID!}\n      apiBase=\"https://basegent.space\"\n      customerToken={customerToken}\n    >\n      <BasegentChat\n        title=\"Bucket Support\"\n        placeholder=\"Ask about features, shortcuts, or refund policies...\"\n        className=\"bottom-20 sm:bottom-24\"\n        showFAB={false} // Hidden in favor of our custom mobile bar trigger\n      />\n    </BasegentProvider>\n  );\n}\n```\n\nOn mobile screens, standard floating chat bubbles frequently block critical bottom navigation actions. We integrated a custom support trigger into Bucket's `MobileBar.tsx`, dynamically hiding the trigger when the user scrolls to the footer credits to preserve UI polish across iPhone, Android, and desktop viewports.\n\n**The Customer's Situation:**\n\nA customer who has been on the **Pro plan** in the **United States** for **21 days** submits this inquiry:\n\n*\"I am on the Pro plan in the US and bought the standard product 21 days ago. Can I still request a full refund, and how long will it take?\"*\n\n**What happens behind the scenes:**\n\n`knowledge_base_read` on `refund_eligibility/standard_windows` and `refund_processing`.` plan: pro`, `market: US`, purchase age: 21 days) and confirms they are `https://mybucket.space/settings` and offers a one-click button to escalate to a human agent.\n**The Customer's Inquiry:**\n\n*\"How do I set up the iOS shortcut to bookmark links from Safari?\"*\n\n`policy.bucket.guide.ios_shortcut` directly from Sanity Context.`https://www.icloud.com/shortcuts/57316fdc574b4deb97f93b0ff322c685`\n`https://mybucket.space/connect`, configure the action sheet, and optionally assign the shortcut to the iPhone 15/16 Action Button.\nTo allow anyone—including challenge judges—to verify live retrieval from our Sanity Context MCP endpoint without needing access to private codebases, here is a self-contained Node.js script:\n\n``` js\n// test-sanity-retrieval.mjs\n// Run with: node test-sanity-retrieval.mjs\nconst MCP_URL = \"https://api.sanity.io/v1/context/organizations/oL4FZOkGh/mcp/context\";\nconst KB_ID = \"kb3NuTkXw21o\";\nconst TOKEN = process.env.SANITY_CONTEXT_API_TOKEN;\n\nasync function run() {\n  if (!TOKEN) {\n    console.error(\"Please export SANITY_CONTEXT_API_TOKEN=<your_org_context_token>\");\n    process.exit(1);\n  }\n\n  console.log(\"1. Fetching Knowledge Base Outline via /initial-context...\");\n  const outlineRes = await fetch(\n    `${MCP_URL}/initial-context?mode=knowledge_base&knowledgeBases=${KB_ID}`,\n    { headers: { Authorization: `Bearer ${TOKEN}` } },\n  );\n\n  if (!outlineRes.ok) {\n    throw new Error(`Failed to fetch initial context: ${outlineRes.status}`);\n  }\n\n  const outline = await outlineRes.text();\n  console.log(\"Discovered Knowledge Base Outline:\\n\");\n  console.log(outline.slice(0, 500), \"...\\n\");\n\n  console.log(\"2. Executing MCP tool call 'knowledge_base_read'...\");\n  const toolRes = await fetch(MCP_URL, {\n    method: \"POST\",\n    headers: {\n      Authorization: `Bearer ${TOKEN}`,\n      \"Content-Type\": \"application/json\",\n      Accept: \"application/json\",\n    },\n    body: JSON.stringify({\n      jsonrpc: \"2.0\",\n      id: \"verify-challenge-call\",\n      method: \"tools/call\",\n      params: {\n        name: \"knowledge_base_read\",\n        arguments: {\n          knowledgeBase: KB_ID,\n          paths: [\"refund_eligibility/standard_windows\"],\n        },\n      },\n    }),\n  });\n\n  const toolPayload = await toolRes.json();\n  console.log(\"Sanity Knowledge Base Response:\\n\");\n  console.log(toolPayload.result?.content?.[0]?.text);\n}\n\nrun().catch(console.error);\n```\n\nBuilding this integration fundamentally changed how I view AI customer support:\n\nIf you are building an AI agent that touches real customers, stop slicing text into dumb chunks. Ground your agent in a real Sanity Knowledge Base—your customers (and your support team) will thank you.", "url": "https://wpnews.pro/news/how-sanity-context-mcp-supercharged-ai-customer-support-in-basegent-bucket-space", "canonical_source": "https://dev.to/miracleio/how-sanity-context-mcp-supercharged-ai-customer-support-in-basegent-bucket-space-2p75", "published_at": "2026-10-04 22:56:11+00:00", "updated_at": "2026-10-04 23:12:23.628411+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "ai-tools", "large-language-models", "ai-products"], "entities": ["Basegent", "Bucket Space", "Sanity", "Sanity Context MCP", "Model Context Protocol", "Miracle Onyenma", "Groq", "OpenAI"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/how-sanity-context-mcp-supercharged-ai-customer-support-in-basegent-bucket-space", "markdown": "https://wpnews.pro/news/how-sanity-context-mcp-supercharged-ai-customer-support-in-basegent-bucket-space.md", "text": "https://wpnews.pro/news/how-sanity-context-mcp-supercharged-ai-customer-support-in-basegent-bucket-space.txt", "jsonld": "https://wpnews.pro/news/how-sanity-context-mcp-supercharged-ai-customer-support-in-basegent-bucket-space.jsonld"}}