Every read in Noon, from either the MCP client or a page render, goes through one tool: query. There is no per-collection read tool and no GraphQL-style resource endpoint. You send one JSON document naming a collection, the fields you want, and an optional filter, sort, and cursor.
The document
{
"collection": "string",
"select": ["field", "..."],
"where": { "field": "value" },
"orderBy": [{ "field": "field", "dir": "asc" }],
"limit": 20,
"cursor": "opaque string from a previous response"
}
collection and select are required. Everything else is optional. select is an array of field names, or validated JSON sub-paths for the fields that allow them (see below).
A worked example against the built-in pages collection, reading published pages back in creation order:
{
"collection": "pages",
"select": ["id", "slug", "title", "updatedAt"],
"where": { "isPublished": true },
"orderBy": [{ "field": "createdAt", "dir": "desc" }],
"limit": 10
}
The response is { items, next_cursor? }. Each item carries exactly the fields you asked for, decoded to their normal JSON types (a timestamp field comes back as a number of epoch milliseconds). There are no counts, no metadata, and no extra columns riding along.
Built-in collections
These are the collections registered by Noon itself. None of them need a plugin installed.
| Collection | What it holds |
|---|---|
pages | Pages: slug, title, layout, lifecycle columns |
posts | Posts: slug, title, lifecycle columns, plus frontmatter as a select-only sub-path |
templates | Region templates |
content_types | Your content type definitions, including their delivery policy |
content_entities | Entities of your user-defined content types |
content_entity_terms | Raw taxonomy assignment rows (entity, term, taxonomy triples) |
site_settings | The site settings singleton |
assets | The media library |
due_work | Delivery/schedule state: scheduled publishes and webhook deliveries |
audit_log | The audit trail. Readable by a root-scoped token only |
Every plugin you install can register its own collections. Examples from Noon's own plugins: geo_rules needs the geo plugin, seo_jsonld needs the SEO plugin, vote_polls and vote_options need the vote plugin, products and orders need the shop plugin, theme_document needs the theme plugin, word_count_stats needs the word-count plugin.
Some collections are queryable by name but deliberately left off describe_schema's advertised list, because their rows carry secrets or personal data. The two you are most likely to want are connections, which returns connection metadata and never a secret, and form_submissions, which returns what visitors sent through a form. If you already know a collection's name, querying it still works and is still scope-checked; describe_schema just will not suggest it to you. For every other collection, describe_schema is the accurate, current list: it reflects exactly what is registered right now, including anything a plugin added.
The where grammar
where is an object mapping a field name to either a bare value (equality) or an operator object:
{
"where": {
"isPublished": true,
"title": { "op": "contains", "value": "recipe" }
}
}
| Form | Meaning |
|---|---|
{"field": value} | Shorthand for {"field": {"op": "eq", "value": value}} |
{"field": {"op": "eq", "value": v}} | Equal to v |
{"field": {"op": "ne", "value": v}} | Not equal to v |
{"field": {"op": "in", "value": [v1, v2, ...]}} | Equal to any value in the array |
{"field": {"op": "gt", "value": v}} | Greater than v |
{"field": {"op": "gte", "value": v}} | Greater than or equal to v |
{"field": {"op": "lt", "value": v}} | Less than v |
{"field": {"op": "lte", "value": v}} | Less than or equal to v |
{"field": {"op": "contains", "value": v}} | Substring match |
in requires an array value. contains requires a string value; using it on a number or boolean field is refused. Only fields a collection actually declares as filterable accept where at all: an unregistered field name is refused rather than silently ignored, and describe_schema tells you which fields on a given collection are filterable.
Sorting, limits, and pagination
orderBy is an array of {field, dir} terms, evaluated in order. Omit it and a collection uses its own default order. Only fields the collection has marked sortable (generally, indexed columns) can be named.
limit bounds how many items come back. Ask for fewer and you get fewer; ask for more than the cap and the request is refused, not silently clamped.
Pagination is cursor-based, not offset-based. When a result is truncated, the response carries next_cursor. Pass that string back as cursor on your next call, with the same collection, where, and orderBy, to continue where you left off:
{
"collection": "pages",
"select": ["id", "slug", "title"],
"where": { "isPublished": true },
"orderBy": [{ "field": "createdAt", "dir": "desc" }],
"cursor": "eyJjIjoicGFnZXMiLCJvIjpbXSwiayI6WzE3MjM..."
}
A cursor is bound to the exact collection and the exact canonical order it was issued for. Reusing one against a different collection, or against the same collection with a different orderBy, is refused rather than silently reinterpreted.
Nested JSON sub-paths
Some collections expose a JSON column for select beyond its declared top-level fields, at any validated sub-path beneath it. For example, on pages you can select layout.blocks.0.type even though only a couple of exact paths under layout are registered as filterable. A sub-path is dot-separated, each segment is either an object key or a numeric array index, and the whole path is capped at 6 segments deep. where/orderBy, unlike select, only accept the exact paths a collection has explicitly registered as filterable, never an arbitrary sub-path.
Limits
| Limit | Value |
|---|---|
Max fields in select | 32 |
Max conditions in where | 16 |
Max elements in an in array | 64 |
Max limit | 100 |
Default limit when omitted | 20 |
Max terms in orderBy | 4 |
Max depth of a select sub-path | 6 segments |
| Max length of a field name / path string | 256 characters |
Max length of a scalar value string (and of each in array element) | 1024 characters |
Max length of a cursor string | 4096 characters |
| Max size of the whole query document you send | 65,536 bytes |
Discovering what is actually queryable
This page describes the query engine's grammar, which is stable. The list of collections, and each collection's fields and operators, is not: it grows as you define content types and install plugins. Call describe_schema to get the live, current answer rather than relying on a list written down here.