# How Sanity Context & MCP Supercharged AI Customer Support in Basegent & Bucket Space

> Source: <https://dev.to/miracleio/how-sanity-context-mcp-supercharged-ai-customer-support-in-basegent-bucket-space-2p75>
> Published: 2026-10-04 22:56:11+00:00

*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)*

Over 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.

At the same time, I run **[Bucket Space](https://mybucket.space)**—a modern web application built for smart bookmarking, content capture, and personal library organization.

Naturally, 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:

**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.

In real-world customer support, this naive approach fails catastrophically:

`Free` vs. `Pro`), geographical market (` US` vs. `EU`), or deployment type (` standard` vs. `custom`). Similarity search has no concept of conditional logic.
When 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**).

I set out to connect the entire loop:

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

`production`.`@basegent/client`
Instead of copying periodic data dumps or storing duplicate content, Basegent queries Sanity Context live during the chat conversation via MCP JSON-RPC:

```
[ Customer on Bucket Space (mybucket.space) ]
                     │
      Authenticated Query + Signed Token
                     ▼
       [ @basegent/react Chat Widget ]
                     │
                     ▼  SSE / WebSocket
    [ Basegent Platform (basegent.space) ]
                     │
       ┌─────────────┴──────────────────────────┐
       │ Multi-Provider BYOI / BYOK Engine      │
       │ (Groq / OpenAI / Anthropic / Google)   │
       └─────────────┬──────────────────────────┘
                     │
         1. Discover Outline (/initial-context)
         2. Select Virtual Paths
         3. Live MCP Tool Call (knowledge_base_read)
                     ▼
  [ Sanity Context MCP Endpoint (api.sanity.io) ]
                     │
   ┌─────────────────┴──────────────────────────┐
   │ Sanity Knowledge Base (kb3NuTkXw21o)        │
   │ - Reconciled Structured Policies           │
   │ - Durable Standing Instructions            │
   │ - Canonical Documentation Citations        │
   └────────────────────────────────────────────┘
                     │
                     ▼
[ Streamed Answer with Citations & 1-Click Human Escalation ]
```

To power support for Bucket, I created a dedicated project in Sanity Labs under organization **Miracle Onyenma** (`oL4FZOkGh`) with project ID `jk662cms` and dataset `production`.

Support policies shouldn't be plain blobs of text. In Sanity Studio, we modeled `supportPolicy` documents with explicit schema constraints:

``` js
// schemas/supportPolicy.ts
import { defineType, defineField } from "sanity";

export const supportPolicy = defineType({
  name: "supportPolicy",
  title: "Support Policy & Guide",
  type: "document",
  fields: [
    defineField({ name: "policyKey", type: "string", title: "Policy Key" }),
    defineField({ name: "title", type: "string", title: "Title" }),
    defineField({ name: "claim", type: "text", title: "Core Claim / Rule" }),
    defineField({
      name: "appliesTo",
      type: "object",
      title: "Applies To",
      fields: [
        { name: "plans", type: "array", of: [{ type: "string" }] },
        { name: "markets", type: "array", of: [{ type: "string" }] },
        { name: "productIds", type: "array", of: [{ type: "string" }] },
      ],
    }),
    defineField({ name: "effectiveFrom", type: "datetime", title: "Effective From" }),
    defineField({ name: "effectiveUntil", type: "datetime", title: "Effective Until" }),
    defineField({ name: "priority", type: "number", title: "Precedence Priority (0-100)" }),
    defineField({
      name: "authority",
      type: "string",
      options: { list: ["canonical", "legacy", "advisory"] },
    }),
    defineField({ name: "supersedes", type: "reference", to: [{ type: "supportPolicy" }] }),
    defineField({ name: "sourceUrl", type: "url", title: "Canonical Source URL" }),
  ],
});
```

We populated this dataset with real Bucket documentation alongside intentional edge cases:

`priority: 100`, `authority: canonical`, effective 2026).` priority: 10`, `authority: legacy`, expired end of 2025).` priority: 90`).
Next, in Sanity Context Lab, we built the **Basegent Support Policies** Knowledge Base (`kb3NuTkXw21o`).

This 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.

Sanity flagged a direct conflict between the custom deployment exception (7 days) and the legacy global rule (14 days) regarding custom products:

Sanity 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:

With 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!

Once 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.

In 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`:

In Basegent's Sources dashboard (`/sources/new`), we added **Sanity Context** as a first-party knowledge source, supplying:

`kb3NuTkXw21o`)
Basegent's core runtime defines a provider-neutral `ContentSourceAdapter`. To query Sanity live, we implemented `SanityContextSourceAdapter`:

`/initial-context`)
When Basegent compiles its retrieval registry, it asks Sanity Context for its virtual outline:

```
// lib/sources/sanity-context-adapter.ts
export interface SanityContextConfig {
  mcpUrl: string;
  knowledgeBaseId: string;
  organizationToken: string;
}

export async function fetchKnowledgeIndex(
  config: SanityContextConfig,
  connectionId: string,
): Promise<string> {
  const url = new URL(config.mcpUrl);
  url.pathname = `${url.pathname.replace(/\/$/, "")}/initial-context`;
  url.searchParams.set("mode", "knowledge_base");
  url.searchParams.set("knowledgeBases", config.knowledgeBaseId);

  const response = await fetch(url, {
    headers: { Authorization: `Bearer ${config.organizationToken}` },
    signal: AbortSignal.timeout(15_000),
  });

  if (!response.ok) {
    throw new Error(`Sanity Context discovery failed with HTTP ${response.status}`);
  }

  const initialContext = await response.text();
  const entries = parseKnowledgeBaseEntries(initialContext, config.knowledgeBaseId);

  return [
    `# Sanity Context Knowledge Base (${config.knowledgeBaseId})`,
    "Available virtual outline paths for live retrieval:",
    ...entries.map(
      (entry) => `- sanity-context/${connectionId}/${config.knowledgeBaseId}/${entry.path}`,
    ),
  ].join("\n");
}
```

When a customer asks a question, Basegent selects relevant virtual paths and executes the `knowledge_base_read` tool live over HTTP:

```
export async function callKnowledgeBaseRead(
  config: SanityContextConfig,
  path: string,
): Promise<string> {
  const response = await fetch(config.mcpUrl, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${config.organizationToken}`,
      "Content-Type": "application/json",
      Accept: "application/json, text/event-stream",
    },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: `basegent-${Date.now()}-${path}`,
      method: "tools/call",
      params: {
        name: "knowledge_base_read",
        arguments: {
          knowledgeBase: config.knowledgeBaseId,
          paths: [path],
        },
      },
    }),
    signal: AbortSignal.timeout(20_000),
  });

  if (!response.ok) {
    throw new Error(`Sanity MCP call error: HTTP ${response.status}`);
  }

  const payload = await response.json();
  if (payload.error) throw new Error(payload.error.message);

  // Extract clean text content from the MCP response
  const textContent = payload.result?.content
    ?.filter((part: any) => part.type === "text")
    .map((part: any) => part.text)
    .join("\n");

  return textContent || JSON.stringify(payload.result?.structuredContent, null, 2);
}
```

Customer 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:

``` js
// lib/ai/provider-resolver.ts
export const SUPPORTED_PROVIDERS = ["groq", "openai", "anthropic", "google"] as const;

export const PROVIDER_DEFAULT_MODELS = {
  groq: { normal: "openai/gpt-oss-120b", budget: "llama-3.3-70b-versatile" },
  openai: { normal: "gpt-4o", budget: "gpt-4o-mini" },
  anthropic: { normal: "claude-3-5-sonnet-latest", budget: "claude-3-5-haiku-latest" },
  google: { normal: "gemini-2.0-flash", budget: "gemini-1.5-flash" },
};

export async function executeWithFallback<T>(
  candidates: Array<{ provider: string; apiKey: string; modelId: string }>,
  action: (model: any) => Promise<T>,
): Promise<T> {
  let lastError: unknown;

  for (const candidate of candidates) {
    try {
      const model = initModel(candidate.provider, candidate.apiKey, candidate.modelId);
      return await action(model);
    } catch (err: any) {
      lastError = err;
      if (isRateLimitOrQuotaError(err)) {
        console.warn(`[Basegent AI] ${candidate.provider} exhausted, failing over...`);
        continue;
      }
      throw err;
    }
  }

  throw lastError ?? new Error("All configured AI providers failed.");
}
```

Whether 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.

`mybucket.space`)
With Sanity and Basegent connected, the final step was integrating the support assistant into [mybucket.space](https://mybucket.space).

Basegent publishes pre-built React components and TypeScript clients directly to npm:

```
npm install @basegent/react @basegent/client
```

When 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:

``` js
// components/shared/BasegentWidget.tsx
"use client";

import { useEffect, useState } from "react";
import { BasegentProvider, BasegentChat } from "@basegent/react";
import { useAuth } from "@/components/providers/auth-provider";

export function BasegentWidget() {
  const { user } = useAuth();
  const [customerToken, setCustomerToken] = useState<string | undefined>();

  useEffect(() => {
    if (!user || user.isAnonymous) return;

    // Retrieve signed JWT/HMAC token with verified plan & market attributes
    fetch("/api/support/basegent-token", { method: "POST" })
      .then((res) => res.json())
      .then((data) => setCustomerToken(data.token))
      .catch((err) => console.error("Could not sign Basegent token:", err));
  }, [user]);

  return (
    <BasegentProvider
      tenantId={process.env.NEXT_PUBLIC_BASEGENT_TENANT_ID!}
      apiBase="https://basegent.space"
      customerToken={customerToken}
    >
      <BasegentChat
        title="Bucket Support"
        placeholder="Ask about features, shortcuts, or refund policies..."
        className="bottom-20 sm:bottom-24"
        showFAB={false} // Hidden in favor of our custom mobile bar trigger
      />
    </BasegentProvider>
  );
}
```

On 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.

**The Customer's Situation:**

A customer who has been on the **Pro plan** in the **United States** for **21 days** submits this inquiry:

*"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?"*

**What happens behind the scenes:**

`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.
**The Customer's Inquiry:**

*"How do I set up the iOS shortcut to bookmark links from Safari?"*

`policy.bucket.guide.ios_shortcut` directly from Sanity Context.`https://www.icloud.com/shortcuts/57316fdc574b4deb97f93b0ff322c685`
`https://mybucket.space/connect`, configure the action sheet, and optionally assign the shortcut to the iPhone 15/16 Action Button.
To 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:

``` js
// test-sanity-retrieval.mjs
// Run with: node test-sanity-retrieval.mjs
const MCP_URL = "https://api.sanity.io/v1/context/organizations/oL4FZOkGh/mcp/context";
const KB_ID = "kb3NuTkXw21o";
const TOKEN = process.env.SANITY_CONTEXT_API_TOKEN;

async function run() {
  if (!TOKEN) {
    console.error("Please export SANITY_CONTEXT_API_TOKEN=<your_org_context_token>");
    process.exit(1);
  }

  console.log("1. Fetching Knowledge Base Outline via /initial-context...");
  const outlineRes = await fetch(
    `${MCP_URL}/initial-context?mode=knowledge_base&knowledgeBases=${KB_ID}`,
    { headers: { Authorization: `Bearer ${TOKEN}` } },
  );

  if (!outlineRes.ok) {
    throw new Error(`Failed to fetch initial context: ${outlineRes.status}`);
  }

  const outline = await outlineRes.text();
  console.log("Discovered Knowledge Base Outline:\n");
  console.log(outline.slice(0, 500), "...\n");

  console.log("2. Executing MCP tool call 'knowledge_base_read'...");
  const toolRes = await fetch(MCP_URL, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${TOKEN}`,
      "Content-Type": "application/json",
      Accept: "application/json",
    },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: "verify-challenge-call",
      method: "tools/call",
      params: {
        name: "knowledge_base_read",
        arguments: {
          knowledgeBase: KB_ID,
          paths: ["refund_eligibility/standard_windows"],
        },
      },
    }),
  });

  const toolPayload = await toolRes.json();
  console.log("Sanity Knowledge Base Response:\n");
  console.log(toolPayload.result?.content?.[0]?.text);
}

run().catch(console.error);
```

Building this integration fundamentally changed how I view AI customer support:

If 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.
