Sanity Context
Ninety reads its rules two ways. This page uses the second: the hosted, read-only MCP endpoint, which is how an agent would see the same content. Everything below was fetched from that endpoint when this page was rendered — it is not a description of the integration.
https://api.sanity.io/v1/context/organizations/ovihgdwkx/mcp/ninety
Tools this endpoint serves
- initial_context
**Call this first**, unless its output was already provided to you (for example in your system prompt). It initializes your session. Returns: - Schema with document types and their fields - Relationships between types (e.g., `author->`, `categories[]->`) - Document counts per type and relation Use this to understand what's queryable before using `groq_query`. The schema tells you which types and fields exist for filtering.
- groq_query
Query the dataset using GROQ. ## Context management Your context window is limited. Be intentional with it. **ALWAYS use projections.** Every query must specify which fields to return: ```groq *[_type == "article"][0...5]{ _id, title, summary } // Good *[_type == "article"][0...5] // Bad - full documents ``` **Especially for text/semantic searches** which match across document types: ```groq *[@ match text::query("setup")][0...10]{ _id, _type, title } // Good *[@ match text::query("setup")][0...10] // Bad - context overflow ``` ## Array Field Outlines Array fields containing objects (e.g. Portable Text `body`, `content`) are returned as **structural outlines** instead of full content. The outline shows headings, references, and custom block types, not the actual text. To read the actual content, use the `array_field_reader` tool with mode `range` on the document ID and field name shown in the outline. ## Type Names and Fields **CRITICAL:** Use exact type names and field names as shown in the schema. Names are case-sensitive and may use unexpected formats (hyphens, dots, etc). Don't assume conventional casing. Always check the schema first to get correct names. ## Projections ```groq {_id, _type, title, publishedAt} // Select fields {"displayTitle": title, "date": publishedAt} // Rename fields (keys must be quoted) {"commentCount": count(comments)} // Count arrays ``` ### Nested Fields and References in Projections Dotted paths and dereferences (`->`) in projections require a quoted key name: ```groq // WRONG - causes "Cannot determine property key" error { _id, slug.current, author->name } // CORRECT { _id, "slug": slug.current, "author": author->name } { _id, slug { current }, author { name } } ``` ## Basic Filters ```groq *[_type == "post"] // All of type *[_type == "post" && status == "published"] // With conditions *[_type == "post" && defined(publishedAt)] // Exclude nulls *[_type == "post" && title match "search"] // Text match (use literals, not $params) *["tag-id" in tags[]._ref] // Array contains *[dateTime(publishedAt) > dateTime("2024-01-01T00:00:00Z")] // Dates (camelCase!) ``` ## Ordering & Slicing ```groq *[_type == "post"] | order(publishedAt desc) // Sort descending *[_type == "post"] | order(priority desc, _updatedAt desc) // Multiple sort *[_type == "post"][0...10] // First 10 (exclusive end) *[_type == "post"][0] // Single document *[_type == "post"] | score(title match "silver") | order(_score desc) // Rank by relevance ``` The only GROQ pipe functions are `order()` and `score()`. See **Text Search** below for `score()` syntax and rules. ## References ```groq author->name // Single reference authors[]->{name, bio} // Array of references authors[defined(@->bio)]->{name, bio} // Filter before deref *[_type == "post" && references("author-id")] // Find by reference ``` ## Aggregations ```groq {"total": count(*[_type == "post"])} // Count documents *[_type == "post"]{"tagCount": count(tags)} // Count per document ``` ## Plain Text from Portable Text `pt::text(field)` extracts plain text from Portable Text arrays. Returns `null` for non-PT values. ```groq *[_type == "post"]{ _id, "plainBody": pt::text(body) } ``` ## Text Search Prefer scoring over filtering when searching by keywords. Filtering with `match` uses AND logic: all terms must be present or you get zero results. Scoring with `| score()` uses OR logic: partial matches are ranked by relevance, so you get results even when not every term appears. ```groq // Score and rank by relevance (OR, partial matches work) *[_type == "article"] | score([title, description] match text::query("climate change policy")) [_score > 0] | order(_score desc)[0...10]{ _id, title, _score } // Filter only (AND, all words must match) *[_type == "article" && @ match text::query("climate change")]{ _id, title }[0...10] ``` ### `text::query()` Syntax `text::query()` does exact keyword matching (not fuzzy). Think about what words appear in the content, not just the user's phrasing. | Syntax | Meaning | Example | |--------|---------|---------| | `word1 word2` | AND in filter, OR in score | `"battery life"` | | `"exact phrase"` | Phrase, words in order | `"\"how to connect\""` | | `word*` | Prefix, matches word + anything | `"connect*"` matches connect, connecting, connection | | `-word` | Exclude, must not contain | `"setup -wifi"` | - No fuzzy matching, so misspellings won't match (`batry` won't find `battery`) - No stemming, so use `product*` to match "products" ### Scoring with `| score()` Rules: - `text::query()` must be used with `match`; `| score(text::query("x"))` is an error. Write `| score(@ match text::query("x"))`. - Always add `[_score > 0]` after `| score()` to exclude non-matching documents. - `score()` is a pipe function, so always `| score(...)`, never inside `[...]` or `{...}`. - `score()` must come before slicing; `*[...][0...5] | score(...)` is wrong. Slice after: `| score(...) | order(_score desc)[0...5]`. Use `[field]` bracket syntax to scope scoring to specific fields. Without brackets, `text::query()` searches all text fields (including Portable Text, with no `pt::text()` needed). ```groq // Scoped to specific fields *[_type == "article"] | score([title, description] match text::query("summer fashion")) [_score > 0] | order(_score desc)[0...10] // Phrase + exclude + prefix *[_type == "article"] | score([title] match text::query('"summer collection" -discontinued fash*')) [_score > 0] | order(_score desc)[0...10] // Structural filter + scored text search *[_type == "article" && category == "news"] | score(@ match text::query("battery life charging")) [_score > 0] | order(_score desc)[0...10]{ _id, title, _score } ``` Filter in `*[...]` before `| score()` to narrow the candidate set. Access the score via `{ title, _score }` in projections. ### Boosting `boost(expression, weight)` inside `| score()` multiplies a signal's score contribution by `weight`: ```groq // Weight title matches higher than body *[_type == "article"] | score(boost([title] match text::query("summer"), 3), boost([description] match text::query("summer"), 1)) [_score > 0] | order(_score desc)[0...10] // Combine text relevance with structural signals *[_type == "article"] | score([title] match text::query("summer"), boost(category == "featured", 3), boost(publishedAt > "2024-06-01", 2)) [_score > 0] | order(_score desc)[0...10] ``` ### Filtering with `match` Use `match` as a filter when you need strict yes/no inclusion: e.g., checking whether a term is present, or narrowing results before applying other ranking: ```groq *[_type == "article" && title match text::query("climate change")] *[_type == "article" && @ match text::query("climate change")] // Entire doc ``` For OR with filters: `*[@ match text::query("A") || @ match text::query("B")]` ## Finding Content Without Embeddings Without semantic search, expand your keyword searches to cover variations: ```groq // Think about what words might appear in the actual content *[_type == "article" && @ match text::query("battery life duration hours")] { _id, _type, title, summary }[0...15] // Use prefix matching for word variations *[@ match text::query("connect* setup install*")] { _id, _type, title, name }[0...20] // Combine structural filters with text search, exclude unwanted terms *[_type == "support-article" && category == "troubleshooting" && @ match text::query("wifi wireless network -ethernet")] { _id, title, summary }[0...10] ``` Tips: - Include synonyms and related terms in your query - Use `prefix*` to match word variations (connect, connecting, connection) - Use `-word` to exclude unwanted results - Filter by document type or category to narrow results before text search - Fetch lightweight projections first, then get full content for relevant docs
- schema_explorer
Inspect a schema type's fields and structure. Use when the overview isn't enough, e.g., to check if "product" has a "color" field before filtering, or to understand a reference relationship.
- array_field_reader
Read and navigate array fields on Sanity documents. This tool loads only a specific field from a single document. Use it when `groq_query` returns an outlined field representation and you need the actual content. ## Modes **range**: Read a contiguous slice of items by index. Use this as the default when you need actual content. ```json { "mode": "range", "documentId": "abc", "field": "body", "range": { "startIndex": 0, "endIndex": 20 } } ``` **filter**: Find items matching text, type, marks, or structural criteria. ```json { "mode": "filter", "documentId": "abc", "field": "body", "filter": { "pte": { "styles": ["h1", "h2"] } } } ``` **outline**: Lightweight structural overview (headings, references, custom types). No actual content. ```json { "mode": "outline", "documentId": "abc", "field": "body" } ``` **continue**: Resume reading a previously cropped item using the continuation token from a prior response. ```json { "mode": "continue", "documentId": "abc", "field": "body", "continue": { "blockIndex": 5, "offsetBytes": 8000 } } ``` ## When to use each mode - Start with **range** to read content (default `startIndex: 0`, `endIndex: 20`) - Use **filter** to locate specific items (headings, images, code blocks, text matches) - Use **outline** only when you need a structural overview without actual content - Use **continue** only when a previous response returned `cropped: true` with a `continuationToken` ## Limits - **range/filter**: max 50 items per request, 8KB per item (items exceeding 8KB are cropped with a continuation token) - **outline**: max 100 entries, 80-char text previews
A GROQ query answered through Context
This asks the endpoint about Croatia’s date-banded membership — the fact that makes the same trip cost fourteen days in 2023 and nothing in 2022.
*[_type == "territory" && code == "HR"][0]{name, code, "bands": accessBands[]{window{from, to}, counted, modes}}Context returned 1 document(s).
Initial context, verbatim
What an agent is handed before it asks anything.
# ninety: Context & Schema
## Efficiency
Prefer well-constructed queries over multiple exploratory calls.
## Accuracy
Only state what the data explicitly says. Don't infer, extrapolate, or connect dots that aren't there. Every claim should be grounded in content you retrieved.
If your query results are incomplete:
- Answer with what you found
- Be explicit about what's missing rather than guessing
When uncertain, acknowledge it. Better to say "I'm not certain" than to confidently state something wrong.
## Tools
- **schema_explorer**: Inspect a type's fields in detail. Use when you need to understand field structure before writing a query.
- **groq_query**: Query the dataset with GROQ. Always use projections and slices to control response size.
## Schema
**Schema overview**: document types, their fields, relationships between types, and document counts.
Query this content using GROQ. The schema shows you:
- **References**: follow with `->` for single refs (`author->name`) or `[]->{field}` for arrays (`categories[]->{title}`)
- **Incoming reference counts**: find related content with `count(*[references(^._id)])`
- **Document counts**: understand data distribution before querying
<resource-schema>
- allowance (title: title, depth: 1, count: 1)
- fields: title<string>, code<slug>, windowDays<number>, limitDays<number>, countingBasis<arrival_inclusive|any_touch|departure_inclusive>, consequences<array>, sources<array>
- referenced by: itinerary×2, territory×36, visaRegime×3
- dispute (title: question, depth: 2, count: 1)
- fields: question<string>, subjectKind<presence_kind|territory|allowance|nationality_class>, presenceRule<reference>, territory<reference>, itinerary<reference>, competingClaims<array>, status<open|review|adjudicated|withdrawn>, ruling<object>, precedent<reference>
- references: presenceRule->presenceRule×1
- itinerary (title: title, depth: 2, count: 2)
- fields: title<string>, slug<slug>, holder<object>, allowance<reference>, summary<text>, demo<boolean>
- references: allowance->allowance×2, holder.nationalityClass->nationalityClass×2
- referenced by: trip×17
- nationalityClass (title: label, depth: 1, count: 3)
- fields: label<string>, code<slug>, passports<array>, summary<text>, sources<array>
- referenced by: itinerary×2, visaRegime×3
- permitExemption (title: label, depth: 1, count: 5)
- fields: label<string>, kind<residence_permit|long_stay_visa|free_movement|permanent_residence|pending_application>, exemptsFromAllowance<boolean>, scopeTerritories<array>, conditions<text>, sources<array>
- presenceRule (title: label, depth: 1, count: 6)
- fields: label<string>, code<slug>, kind<cleared_entry|airport_transit|airport_transit_landside|overnight_arrival|same_day|permitted>, counted<boolean>, disputed<boolean>, rationale<text>, sources<array>
- referenced by: dispute×1
- source (title: title, depth: 1, count: 5)
- fields: title<string>, publisher<string>, url<url>, kind<legislation|guidance|calculator|faq|treaty>, retrievedAt<datetime>, notes<text>
- territory (title: name, depth: 1, count: 36)
- fields: name<string>, code<string>, kind<state|carve_out|external>, carveOutOf<reference>, allowances<array>, accessBands<array>
- references: allowances[]->allowance×36, carveOutOf->territory×5
- referenced by: territory×5
- trip (title: label, depth: 1, count: 17)
- fields: label<string>, itinerary<reference>, stays<array>, notes<text>
- references: itinerary->itinerary×17
- visaRegime (title: notes, depth: 2, count: 3)
- fields: nationalityClass<reference>, allowance<reference>, window<object>, visaRequired<boolean>, maxDaysPerEntry<number>, allowedPurposes<tourism|business|family|study|work|transit>, notes<text>, sources<array>
- references: allowance->allowance×3, nationalityClass->nationalityClass×3
</resource-schema>