Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions skills/sanity-kb-setup/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
name: sanity-kb-setup
description: Set up, check and fix a Sanity Context Knowledge Base, then connect coding agents to it over MCP. Use when the user wants a Knowledge Base or "KB" planned, created or built, its conflicts or issues reviewed or resolved, its content corrected or audited, or an agent such as Claude Code, Cursor or Codex connected to one. Also use when they say "what did the build flag" or "pick the winners", or when setup is blocked by a missing Sanity project, login, organisation token or Knowledge Base slot.
compatibility: Needs a Node version supported by the project's installed Sanity packages, a Sanity project with the `sanity` package installed, and `npx sanity login`. Tested with Sanity 6.14.0, which requires Node >=22.12.
metadata:
version: "1.2.0"
---

# Sanity Knowledge Base setup

Take a Sanity project to a Knowledge Base that agents can read and trust. You do the work with the Sanity CLI, the project's own files and the examples in `references/api.md`.

## Stages

Read only the file for the stage you are in.

| Stage | When | Read |
|---|---|---|
| 1. Plan | The project has no `kb-setup.md` | `references/plan.md`, then `references/type-roles-and-queries.md`. For a `turbo-start-sanity` repo, also `references/turbo-start-sanity.md` |
| 2. Create | The user said yes to the plan | `references/create.md` and `references/cli.md` |
| 3. Check | A build or rebuild just finished | `references/check.md` and `references/api.md` |
| 4. Resolve | The user replied with picks such as `1A 2B` | `references/resolve.md` and `references/api.md` |
| 5. Fix content | Issues are resolved and the documents still hold the losing claims | `references/fix-content.md` |
| 6. Connect | The user wants an agent to read the Knowledge Base | `references/connect-agents.md` |
| Blocked | A command fails, a result looks wrong, or something is missing | `references/blocked.md` |

Stages 2 to 5 are one flow. A finished build sends you straight to stage 3. The stops are the ones marked in the stage files, where a person has to say yes or pick.

## Where things stand

Check what exists before any stage.

- `kb-setup.md` in the project is the plan, called the sheet in these files. It records the Knowledge Base id once one exists.
- `npx sanity context list --organization <org-id>` shows the organisation's Knowledge Bases. `references/create.md` says when to reuse one.
- `npx sanity context imports list <kb-id>` shows which sources already imported. Add each source once.

Then name the state in these words.

| State | Means |
|---|---|
| Planned | `kb-setup.md` exists, nothing is created |
| Built | Sources imported and a build finished. Nobody has checked what it says |
| Reviewed | You compared the entries with the source claims, and the user has seen every disagreement, raised or silent |
| Clean | Reviewed, the picks are resolved, the content is corrected, and a rebuild raised nothing new |
| Connected | An agent reads it, and you confirmed the endpoint serves this Knowledge Base |

Connected doesn't require Clean. Some users keep conflicts on purpose, for a demo or to test a checking tool. If the project looks like that, for example a README that lists planted conflicts, ask before stage 4 or 5. If they keep the conflicts, stop at Reviewed and go to stage 6.

## How a Knowledge Base works

Sanity reads the sources, sorts the facts into topics, and writes one cited entry per topic. Where sources disagree it keeps one claim and may file an issue. Agents read the entries over MCP with `initial_context` and `knowledge_base_read`.

- **The purpose steers the build.** It decides which sources the build keeps, which subjects lead and how deep each entry goes.
- **A dataset source is one GROQ query over one dataset.** It starts with `*[`, reads published documents only, and matches between 1 and 5,000 documents.
- **File sources never re-sync.** To update one, delete the import and add the new file.
- **A silent settlement is a disagreement the build settled without filing an issue.** The issues list misses these, and results vary between builds of the same sources. Only reading the entries finds them.
- **Resolving changes the Knowledge Base, not the website.** The losing claim stays in the document until someone corrects it.

## Ground rules

- Name only types, fields and files you have read. A query that names a missing field fails silently and indexes nulls.
- A `0` or `[]` is a suspect zero. Run the checks in `references/blocked.md` before you act on it.
- Content comes from the user. Never seed, import, generate or reset content in a real project, and never offer to. Seed only a project the user calls a demo.
- Never write a token, key or password into a file or a chat message. They live in environment variables, and you never see one.
- A person decides every delete, resolution and dataset write. Delete only the Knowledge Base or import the user names. Resolve only the issues they picked. Write only the exact change list they approved, with every edit guarded by the revision you reviewed, and as drafts unless they ask to publish.
- The CLI's `context` commands and the `client.context` API are missing from Sanity's public docs. If one behaves differently from these files, say so and use the dashboard.
- If your own permission settings refuse a command, give the user the exact command to run. Leave other routes to the same write alone.
105 changes: 105 additions & 0 deletions skills/sanity-kb-setup/assets/kb-setup-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Knowledge Base setup: {client}

Generated from {repo} on {date}. Sections 1 to 3 can be created with the Sanity CLI (see the skill's `references/cli.md`) or copied into the Context dashboard. Section 6 is dashboard-only.

- **Organisation:** {org id}
- **Knowledge Base id:** {filled in after creation}

## 1. New knowledge base

- **Title:** {short title. Letters, numbers and spaces only; some tools reject `&`}
- **Purpose:**
```
Answer questions from {who asks} about {client}.
Lead with: {three or four subjects}.
Leave out: {what should not be indexed at all. Never list content whose claims need checking}.
```

## 2. Sources

### Dataset

- **Project:** {project id}
- **Dataset:** {dataset name}
- **Query:**
```groq
{query}
```
- **Documents matched:** {count, or "unknown" and why}
- **Fallback query** if the dashboard rejects `pt::text()` or conditional projections:
```groq
{same filter, raw fields only}
```

### Files

| File | Path | Authority for |
|---|---|---|

Files never re-sync. Delete and re-upload on every change.

### Website

- **URL:** {public URL, or "none" and why}

## 3. Build

Build with no instructions, so they can't hide a conflict. A build still misses some conflicts, so read the entries afterwards: `npx sanity context build <kb-id> --watch`, or **Build entries** in the dashboard.

## 4. Expected outline

- {topic} [core | peripheral]

## 5. After the first build

- Compare the real outline with section 4. A missing topic means a missing source or a purpose that doesn't reach it.
- Read the entries. For each disagreement in section 10, note which claim the entry kept and whether the build raised an issue.
- A person picks the winner of each conflict. Resolve those picks, which creates the instructions. Don't write instructions by hand first.
- Correct the losing claims in the source documents, publish, refresh, then rebuild.

## 6. MCP endpoint

- **Title:** {title}
- **Name:** {lowercase-with-hyphens. Cannot be changed later}
- **Instructions:** leave empty
- **Content source:** this Knowledge Base only. Do not add the dataset.
- **URL once created:** `https://api.sanity.io/v1/context/organizations/{org-id}/mcp/{name}`

## 7. Type roles

| Type | Role | Included | Reason |
|---|---|---|---|

## 8. Fields left out

| Type | Field | Reason | Could hide a conflict? |
|---|---|---|---|

## 9. Decisions for a person

- **Homepage:** {included or excluded, and why}
- {other judgement calls}

## 10. Where conflicts are likely

- {facts stated in more than one place, with each location}

## 11. Build results

- **Built:** {lastChangedAt}
- **Open issues:** {count}
- **Outline compared with section 4:** {missing or unexpected topics}

| Fact | Knowledge Base says | Other claim | Entries | Severity |
|---|---|---|---|---|

- **Expected in section 10 but not raised:** {conflicts the build missed, to check on the live page}

## 12. Resolutions

- **Chosen by:** {person} on {date}

| Fact | Winner | Kept or accepted | Source documents corrected |
|---|---|---|---|

- **After rebuild:** {open conflicts, suggestions and gaps left}
117 changes: 117 additions & 0 deletions skills/sanity-kb-setup/references/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# API examples

`@sanity/client` reads entries, lists issues and resolves them through `client.context`, which the CLI can't. These methods are missing from Sanity's public docs, so expect them to change. Every call below ran against a real Knowledge Base on 2026-09-17.

Write a short throwaway script from these examples, save it in the user's Sanity project folder, run it with `node`, and delete it.

## Get a client

This borrows the project's Sanity CLI, so it uses the `npx sanity login` session and needs no token.

```js
import { createRequire } from 'node:module'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'

const KB_ID = 'kb...'
const API_VERSION = 'v2026-08-25'

const fromProject = createRequire(join(process.cwd(), 'package.json'))
let cliCore
for (const pkg of ['@sanity/cli/package.json', 'sanity/package.json']) {
try {
const fromCli = createRequire(fromProject.resolve(pkg))
cliCore = await import(pathToFileURL(fromCli.resolve('@sanity/cli-core')).href)
break
} catch {}
}
const { getGlobalCliClient, getProjectCliClient } = cliCore

const kb = await (await getGlobalCliClient({ apiVersion: API_VERSION, requireUser: true }))
.context.knowledgeBases.get(KB_ID)

const client = await getGlobalCliClient({
apiVersion: API_VERSION,
requireUser: true,
resource: { id: kb.publicId, type: 'knowledge-base' },
context: { organizationId: kb.organizationId },
})
```

End the script by setting `process.exitCode`. On Windows, `process.exit()` after network calls can crash Node while sockets close.

## Read the entries

```js
const outline = await client.context.entries.list()
// [{ _id, path, title, tldr, status }], status is 'filled' once written

const entry = await client.context.entries.get({ path: 'delivery' })
// entry.body is Markdown, entry.citations lists its sources
```

This works before any MCP endpoint exists.

## List issues

`scripts/kb-issues.mjs <kb-id> --status open` makes this call and formats the result.

```js
const issues = await client.context.issues.list({ status: 'open' }) // 'open' | 'accepted' | 'rejected'
// issue._id, issue._createdAt, issue.status, and issue.content with:
// kind ('conflict' | 'update_required' | 'gap'), severity, claimKey,
// currentClaim, alternativeClaim, involvedScopes, scopePath, issue, suggestedFix
```

`accepted` means resolved. `rejected` means dismissed.

## Resolve, dismiss, reopen

```js
const issue = await client.context.issues.get({ issueId })
if (issue.content.kind !== 'conflict') throw new Error('only conflicts can be resolved')

// Print the winner and compare it with the user's pick before the call.
await client.context.issues.resolve({ issueId, resolution: 'keep_existing' }) // currentClaim wins, pick A
await client.context.issues.resolve({ issueId, resolution: 'accept_new' }) // alternativeClaim wins, pick B

await client.context.issues.dismiss({ issueId }) // close a stale issue, a suggestion or a gap
await client.context.issues.reopen({ issueId }) // undo a resolve, deletes the instruction it created
```

## Guarded edits

A guarded edit carries the revision you reviewed, so it can't overwrite a newer change. The example targets a published document. Run it for real only when the user approved a direct publish of that exact change.

```js
const data = await getProjectCliClient({
apiVersion: 'v2025-08-15',
projectId: '<project-id>',
dataset: '<dataset>',
requireUser: true,
useCdn: false,
perspective: 'raw', // so drafts.<id> is visible
})

const doc = await data.getDocument('faq-main') // note doc._rev and the field's current value
const draft = await data.getDocument('drafts.faq-main') // someone's unpublished edits, if not null

await data
.transaction()
.patch('faq-main', (p) =>
p.ifRevisionId(doc._rev).set({ 'items[_key=="ret"].answer': 'You have 14 days from delivery...' }),
)
.commit({ dryRun: true }) // drop dryRun once the user has approved the change
```

- `ifRevisionId` fails the commit with a 409 if anyone edited the document after you read it. Nothing is written. Read it again and show the user the new value.
- `commit({ dryRun: true })` validates the whole transaction without writing.
- Address array items by `_key`, never by index.
- In draft mode, edit only the draft. Never patch the published document.

| Mode | Do |
|---|---|
| Draft, and no draft exists | Create `drafts.<id>` from the published document with `transaction.create(...)`, then patch the draft |
| Draft, and one exists | Patch the draft with its own `_rev`. Tell the user it holds other unpublished edits, and keep them |
| Approved direct publish, and a draft holds the losing claim | Put the draft's correction in the approved change list, and patch both documents in one transaction, each with its own reviewed revision. Keep the draft's unrelated edits |
| Direct publish where the draft's correction isn't approved | The user reconciles the draft in Studio first. Publishing that draft later would restore the losing claim |
Loading
Loading