Add GET /queries stored-query catalog endpoint

List a graph's mcp.expose stored queries as a typed tool catalog so a client
(the MCP server) can register them as tools without fetching .gq source.
Each entry carries name, MCP tool_name, description/instruction, a
read/mutate flag, and decomposed typed params (kind enum: string|bool|int|
bigint|float|date|datetime|blob|vector|list, plus item_kind for lists and
vector_dim) — so the consumer builds an input schema with a closed match and
never re-parses omnigraph type spelling. I64/U64 are bigint (string on the
wire): a JSON number loses precision past 2^53 and the engine already accepts
decimal strings.

Read-gated (works in default-deny; the catalog is graph-wide, authorized
against main). NOT Cedar-filtered per query yet — a reader can list a query
whose invoke_query they lack (documented gap until per-query authz lands);
invocation stays invoke_query-gated + deny==404.

- api: QueriesCatalogOutput / QueryCatalogEntry / ParamDescriptor / ParamKind
  + query_catalog_entry (reuses PropType::from_param_type_name; scalar_kind is
  exhaustive, so a new ScalarType is a compile error here until catalogued).
- GET /queries route in per_graph_protected (→ /graphs/{id}/queries in multi
  mode); OpenAPI regenerated; path allowlists updated.
- Tests: projection unit (every kind, list, vector, nullable, mutation,
  empty) + handler (exposed-only filter, read-gate probe-oracle, empty
  registry).
This commit is contained in:
Ragnor Comerford 2026-05-31 13:09:03 +02:00
parent 6cad21cb6a
commit a087ac4476
No known key found for this signature in database
6 changed files with 481 additions and 1 deletions

View file

@ -829,6 +829,53 @@
]
}
},
"/queries": {
"get": {
"tags": [
"queries"
],
"summary": "List the graph's exposed stored queries as a typed tool catalog.",
"description": "Returns the `mcp.expose == true` subset of the `queries:` registry, each\nwith its MCP tool name, read/mutate flag, description/instruction, and\ntyped parameters — enough for a client to register them as tools without\nfetching `.gq` source. Read-gated; the catalog is graph-wide (branch\nindependent — `read` is authorized against `main`). **Not** Cedar-filtered\nper query yet, so it can list a query whose `invoke_query` the caller\nlacks (a known gap until per-query authorization lands).",
"operationId": "list_queries",
"responses": {
"200": {
"description": "Stored-query catalog (the mcp.expose subset, with typed params)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/QueriesCatalogOutput"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
}
},
"security": [
{
"bearer_token": []
}
]
}
},
"/queries/{name}": {
"post": {
"tags": [
@ -1840,6 +1887,120 @@
}
}
},
"ParamDescriptor": {
"type": "object",
"description": "One declared parameter of a stored query, projected for the catalog.",
"required": [
"name",
"kind",
"nullable"
],
"properties": {
"item_kind": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/ParamKind",
"description": "Element kind when `kind == list` (always a scalar — the grammar\nforbids lists of vectors or nested lists)."
}
]
},
"kind": {
"$ref": "#/components/schemas/ParamKind"
},
"name": {
"type": "string"
},
"nullable": {
"type": "boolean",
"description": "`false` → the caller must supply it; `true` → optional."
},
"vector_dim": {
"type": [
"integer",
"null"
],
"format": "int32",
"description": "Dimension when `kind == vector`.",
"minimum": 0
}
}
},
"ParamKind": {
"type": "string",
"description": "The kind of a stored-query parameter, decomposed so a client (e.g. an\nMCP server) can build a typed input schema with a closed `match` and\nnever re-parse omnigraph's type spelling. `bigint`/`date`/`datetime`/\n`blob` are carried as JSON strings on the wire: a 64-bit integer past\n2^53 loses precision as a JSON number, and Date/DateTime are ISO\nstrings, Blob a blob-URI string.",
"enum": [
"string",
"bool",
"int",
"bigint",
"float",
"date",
"datetime",
"blob",
"vector",
"list"
]
},
"QueriesCatalogOutput": {
"type": "object",
"description": "Response for `GET /queries`: the `mcp.expose` subset of a graph's\nstored-query registry, each with typed parameters.",
"required": [
"queries"
],
"properties": {
"queries": {
"type": "array",
"items": {
"$ref": "#/components/schemas/QueryCatalogEntry"
}
}
}
},
"QueryCatalogEntry": {
"type": "object",
"description": "One entry in the stored-query catalog (`GET /queries`).",
"required": [
"name",
"tool_name",
"mutation",
"params"
],
"properties": {
"description": {
"type": [
"string",
"null"
]
},
"instruction": {
"type": [
"string",
"null"
]
},
"mutation": {
"type": "boolean",
"description": "`true` for a stored mutation → an MCP read-only hint of `false`."
},
"name": {
"type": "string",
"description": "Registry key / invoke path segment (`POST /queries/{name}`)."
},
"params": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ParamDescriptor"
}
},
"tool_name": {
"type": "string",
"description": "MCP tool id (the `tool_name` override, else `name`)."
}
}
},
"QueryRequest": {
"type": "object",
"description": "Inline read-query request for `POST /query`.\n\nFriendlier-named alternative to [`ReadRequest`] for ad-hoc reads and\nAI-agent integration. Mutations are rejected with 400 — use `POST\n/mutate` (or its deprecated alias `POST /change`) for write queries.\nField names are deliberately short (`query`, `name`) to match the GQ\nkeyword and the CLI `-e` flag.",