Skip to content

MCP Saved-Query Tools

Saved-query tools are JSON-defined, parameterized queries scoped to a collection. They appear as first-class MCP tools alongside core tools like query_collection — AI agents pick them by name, pass arguments, and receive filtered, formatted results. No PHP is needed; the definition lives in the collection’s schema editor.

Use saved-query tools when a particular query is reused or when you want to give an AI agent a stable, named entry point. For ad-hoc filter composition, query_collection is the better choice.

Open the schema editor for the collection you want to expose. On the MCP tab, find the Custom MCP Tools field — it renders as a deck of tool rows. Click Add and fill in the id, description, filters, etc. Behind the scenes the deck stores tools keyed by their id:

{
"featured_posts": {
"id": "featured_posts",
"description": "Return featured blog posts, newest first.",
"filters": {
"featured": { "value": true }
},
"sort": "date:desc",
"limit": 10
}
}

The deck key matches the tool’s id — that’s the canonical identifier; the inner id field is what you enter in the form. Save the schema. The tool featured_posts is live on /mcp immediately — no rebuild, no restart.

Each tool entry is an object with the following fields. Only id and description are required.

FieldRequiredTypeNotes
idyesstringSnake_case ^[a-z][a-z0-9_]*$. Becomes the wire-level tool name when registered. The registered name (base + any mcp.toolPrefix) must be ≤ 64 characters; the save-time validator enforces the full limit. Globally unique across all tools.
descriptionyesstringMin 1, max 1024 characters. Describe what the tool returns — not how an agent should use it.
paramsnoobjectTyped caller parameters (see Parameterized tools). Omit for fixed-filter tools.
filtersnoobjectField-name → {value, operator?} map. Filter values may reference {{params.X}} placeholders.
sortnostringREST-style sort spec, e.g. date:desc or priority:asc,date:desc.
limitnointegerMax results per call. Default 20, capped at 50.
offsetnointegerSkip the first N matching results. Default 0.
includenostringREST-style include filter — escape hatch for filter shapes the filters object can’t express.
excludenostringREST-style exclude filter — escape hatch.
formatnoenumOutput format for rich-text fields: markdown (default), html, or text.

The mcp-tool.json JSON Schema enforces maxLength: 64 on the base id field only — it cannot dynamically account for the site’s configured mcp.toolPrefix. The save-time validator performs the full prefix-inclusive check: strlen(toolPrefix + "_" + id) ≤ 64. If a site has a 10-character prefix configured, base ids must be ≤ 54 characters. The error message will state both the prefix in use and the resulting length.

A saved-query tool returns the same paginated envelope as query_collection — both are built from the same CollectionQueryResultFormatter:

{
"items": [ { "id": "post-1", "url": "/blog/post-1", "...": "..." } ],
"total": 37,
"limit": 10,
"offset": 0,
"has_more": true
}
  • total — total matching records, after persona filtering (drafts excluded for public callers).
  • limit / offset — the tool definition’s own limit and offset values (see Tool definition reference). Saved-query tools have no caller-supplied pagination parameter — limit and offset are fixed by the definition so the tool stays a curated, bounded answer.
  • has_moretrue when total exceeds offset + limit, i.e. the tool’s fixed window doesn’t cover every matching record.

If an agent needs to page past a saved-query tool’s fixed window, it should call query_collection directly — the general-purpose tool that accepts limit/offset — rather than expecting the saved-query tool to move its own window. has_more: true is a signal to reach for query_collection, not an invitation to re-call the saved-query tool with different arguments it doesn’t accept.

Fixed-filter tools take no caller arguments. They are preset queries with a stable result shape — useful for “give me all draft posts” or “return the three most recent announcements”:

{
"draft_posts": {
"id": "draft_posts",
"description": "Return blog posts pending editorial review, oldest first.",
"filters": {
"status": { "value": "draft" }
},
"sort": "date:asc",
"limit": 20
},
"recent_announcements": {
"id": "recent_announcements",
"description": "The three most recent site announcements.",
"sort": "date:desc",
"limit": 3
}
}

Add a params block to accept caller-supplied arguments. Reference them in filter values with {{params.X}} placeholders.

{
"id": "find_listings_by_city",
"description": "Search active real-estate listings by city and optional maximum price.",
"params": {
"city": {
"type": "string",
"description": "City name (case-insensitive substring match).",
"required": true
},
"max_price": {
"type": "number",
"description": "Maximum listing price in USD.",
"minimum": 0
}
},
"filters": {
"status": { "value": "active" },
"city": { "operator": "contains", "value": "{{params.city}}" },
"price": { "operator": "lte", "value": "{{params.max_price}}" }
},
"sort": "price:asc",
"limit": 20
}
  • {{params.X}} is the only recognized placeholder syntax. Other {{...}} strings in filter values are treated as literals.
  • When a placeholder replaces the entire filter value (e.g. "value": "{{params.city}}"), the resolved value is type-coerced to match the param’s declared type.
  • When a placeholder is embedded in a longer string (e.g. "value": "category-{{params.slug}}"), the resolved segment is cast to string and the parts are concatenated.
  • If an optional param is not supplied by the caller, the filter containing its placeholder is excluded from the query. The tool still runs against the remaining filters.
PropertyRequiredTypeNotes
typeyesenumstring, number, integer, boolean
descriptionnostringShown to AI agents in the generated inputSchema
requirednobooleanDefault false. Required params are enforced by the SDK before dispatch.
defaultnoanyDefault value used when the param is absent
enumnoarrayRestrict to a specific set of values
minimumnonumberNumeric lower bound
maximumnonumberNumeric upper bound
formatnostringJSON Schema format hint (e.g. date, uri)

Param names must match ^[a-z][a-z0-9_]*$.

Specify an operator alongside value in any filter entry. The default is eq.

OperatorMeaning
eqEqual (default)
neNot equal
ltLess than
lteLess than or equal
gtGreater than
gteGreater than or equal
containsSubstring match (case-insensitive for string fields)
startsStarts with
endsEnds with
inValue is in a pipe-separated list (a|b|c)
notinValue is not in a pipe-separated list
"filters": {
"tag": { "operator": "contains", "value": "featured" },
"rating": { "operator": "gte", "value": 4 },
"status": { "operator": "notin", "value": "draft|archived" }
}

The ordered operators (lt, lte, gt, gte) compare numbers numerically and dates chronologically — so a tool can take an ISO date param and filter a date field against it:

"params": { "before": { "type": "string", "format": "date", "description": "Cutoff date." } },
"filters": { "reviewed": { "operator": "lte", "value": "{{params.before}}" } }

A bare 2026-07-26 cutoff works against full ISO-8601 stored values. Values that are neither both numbers nor both dates have no defined order and match nothing.

A saved-query tool inherits its access level from the parent collection’s MCP Access setting (admin or public). There is no per-tool override.

When the public persona calls the tool, T3’s standard safety filters run after the tool’s filters: draft objects are excluded, and fields marked expose: false in the collection’s MCP field settings are stripped from every response. Saved-query tools cannot bypass these filters.

Collision detection runs at two points:

Save-time (warning, non-blocking). When you save the schema, each tool name is checked against currently registered core and extension tools. A collision triggers a warning banner in the admin — it does not block the save. The colliding tool will be silently skipped from tools/list at runtime until the name is changed.

Server-build-time (strict deny). When the MCP server initializes, SchemaToolRegistrar walks all collections. If the same tool name appears in two different collections, both tools are dropped from tools/list. Core and extension tools always win over schema tools. Rename one of the conflicting tools to resolve the issue.

If a tool is missing from tools/list despite being defined in the schema, check the admin’s warnings UI for collision notices and review the application log for entries from SchemaToolRegistrar.

Saved-query toolquery_collection
DefinitionJSON in schema editorCalled ad-hoc by AI agent
Agent selects byTool nameTool name + inline args
Best forStable, reused queriesAd-hoc filter composition
Caller can change filtersNo (fixed + declared params only)Yes
PaginationFixed limit/offset from the definition; has_more signals to switch toolsCaller-supplied limit/offset
Requires PHPNoNo

Use a saved-query tool when you want a predictable, named entry point. Use query_collection when the agent needs to compose filters freely.

Tool id format. Only lowercase letters, digits, and underscores. Must start with a letter. Max 64 characters including any mcp.toolPrefix the operator has configured.

Total tool count. Most MCP clients degrade in usability above roughly 50 tools. If you define many saved-query tools across multiple collections, watch the cumulative count in the admin’s MCP status panel.

{{params.X}} is the only placeholder syntax. Filter values containing other {{...}} strings are passed through as literals. Future macro syntax will use a different prefix.

Referencing an undeclared param. If a filter value contains {{params.X}} but X is not declared in the params block, the tool fails at runtime with a validation error. The admin’s warnings UI will surface the issue on next save.

Do not write LLM instructions into the description. “Always call this tool first” reads as prompt injection at directory review. Lead with what the tool returns.

  • MCP Server — personas, transport, core tool catalog, collection MCP settings.
  • Extending MCP — registering tools and resources from PHP extensions, progress notifications, error handling, persona-aware handlers.