diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 7420346..dfe2c9a 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -81,11 +81,14 @@ jobs: - branch_merge YAML cat > /tmp/og/omnigraph.yaml <<'YAML' - policy: - file: /tmp/og/policy.yaml graphs: e2e: uri: /tmp/og/repo.omni + # Per-graph policy: server 0.6.1+ rejects a top-level `policy.file` + # alongside a named graph (it would be silently ignored), so the + # policy must live under the graph it applies to. + policy: + file: /tmp/og/policy.yaml cli: graph: e2e branch: main diff --git a/package.json b/package.json index 4c2d692..44d2dc2 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "packageManager": "pnpm@9.15.0", "omnigraph": { - "serverVersion": "0.6.0" + "serverVersion": "0.6.1" }, "scripts": { "sync-spec": "tsx scripts/sync-spec.ts", diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 94a32f3..4e0f743 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@modernrelay/omnigraph-mcp", - "version": "0.6.0", + "version": "0.6.1", "description": "MCP server exposing an Omnigraph database to LLM clients (Tools + Resources, stdio transport).", "license": "MIT", "repository": { diff --git a/packages/mcp/src/version.gen.ts b/packages/mcp/src/version.gen.ts index 2713dc0..2b0f70d 100644 --- a/packages/mcp/src/version.gen.ts +++ b/packages/mcp/src/version.gen.ts @@ -4,4 +4,4 @@ /** * The MCP server package version reported in the MCP initialize response. */ -export const MCP_PACKAGE_VERSION = "0.6.0"; +export const MCP_PACKAGE_VERSION = "0.6.1"; diff --git a/packages/mcp/test/server.test.ts b/packages/mcp/test/server.test.ts index 600cfed..7e08533 100644 --- a/packages/mcp/test/server.test.ts +++ b/packages/mcp/test/server.test.ts @@ -83,7 +83,7 @@ describe('omnigraph-mcp server', () => { const { client } = await setup(); const info = client.getServerVersion(); expect(info?.name).toBe('omnigraph-mcp'); - expect(info?.version).toBe('0.6.0'); + expect(info?.version).toBe('0.6.1'); }); it('lists every expected tool', async () => { @@ -133,7 +133,7 @@ describe('omnigraph-mcp server', () => { const parsed = JSON.parse(block.text); expect(parsed.status).toBe('ok'); expect(parsed.version).toBe('0.3.0'); - expect(parsed.sdkServerVersion).toBe('0.6.0'); + expect(parsed.sdkServerVersion).toBe('0.6.1'); }); it('calls the read tool and preserves opaque param keys', async () => { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 3d18cc1..7c0ff94 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@modernrelay/omnigraph", - "version": "0.6.0", + "version": "0.6.1", "description": "TypeScript SDK for the Omnigraph HTTP API.", "license": "MIT", "repository": { diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index 6a93775..d2241dc 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -5,6 +5,7 @@ import { BranchesResource } from './resources/branches'; import type { CallOptions } from './internals'; import { CommitsResource } from './resources/commits'; import { GraphsResource } from './resources/graphs'; +import { QueriesResource } from './resources/queries'; import { SchemaResource } from './resources/schema'; import type { Change, @@ -85,6 +86,7 @@ export default class Omnigraph { readonly branches: BranchesResource; readonly commits: CommitsResource; readonly graphs: GraphsResource; + readonly queries: QueriesResource; readonly schema: SchemaResource; private readonly t: Transport; @@ -96,6 +98,7 @@ export default class Omnigraph { this.branches = new BranchesResource(this.t); this.commits = new CommitsResource(this.t); this.graphs = new GraphsResource(this.t); + this.queries = new QueriesResource(this.t); this.schema = new SchemaResource(this.t); } diff --git a/packages/sdk/src/generated/index.ts b/packages/sdk/src/generated/index.ts index 81e663e..67d6b15 100644 --- a/packages/sdk/src/generated/index.ts +++ b/packages/sdk/src/generated/index.ts @@ -69,6 +69,13 @@ export { type IngestResponse, type IngestResponses, type IngestTableOutput, + type InvokeQueryData, + type InvokeQueryError, + type InvokeQueryErrors, + type InvokeQueryResponse, + type InvokeQueryResponses, + type InvokeStoredQueryRequest, + type InvokeStoredQueryResponse, type ListBranchesData, type ListBranchesError, type ListBranchesErrors, @@ -84,6 +91,11 @@ export { type ListGraphsErrors, type ListGraphsResponse, type ListGraphsResponses, + type ListQueriesData, + type ListQueriesError, + type ListQueriesErrors, + type ListQueriesResponse, + type ListQueriesResponses, LoadMode, type ManifestConflictOutput, type MergeBranchesData, @@ -98,6 +110,10 @@ export { type MutateErrors, type MutateResponse, type MutateResponses, + type ParamDescriptor, + ParamKind, + type QueriesCatalogOutput, + type QueryCatalogEntry, type QueryData, type QueryError, type QueryErrors, diff --git a/packages/sdk/src/generated/types.gen.ts b/packages/sdk/src/generated/types.gen.ts index 4d6eb9b..d55ce05 100644 --- a/packages/sdk/src/generated/types.gen.ts +++ b/packages/sdk/src/generated/types.gen.ts @@ -201,6 +201,37 @@ export type IngestTableOutput = { table_key: string; }; +/** + * Body for `POST /queries/{name}` — invokes the server-side stored query + * named in the path. The query source and name come from the registry, + * never the body; only the runtime inputs are supplied here. + */ +export type InvokeStoredQueryRequest = { + /** + * Branch to run against. Defaults to `main`; for a stored mutation the + * write targets this branch. + */ + branch?: string | null; + /** + * JSON object whose keys match the stored query's declared parameters. + */ + params?: unknown; + /** + * Snapshot id to read from (read queries only — rejected for a stored + * mutation). Mutually exclusive with `branch`. + */ + snapshot?: string | null; +}; + +/** + * Response for `POST /queries/{name}`: the read envelope for a stored + * read, or the mutation envelope for a stored mutation. Serialized + * **untagged**, so the wire shape is exactly [`ReadOutput`] or + * [`ChangeOutput`] — classification follows the stored query, not a + * wrapper field. + */ +export type InvokeStoredQueryResponse = ReadOutput | ChangeOutput; + /** * Shadow enum for documenting [`LoadMode`] in the OpenAPI schema. */ @@ -247,6 +278,83 @@ export type MergeConflictOutput = { table_key: string; }; +/** + * One declared parameter of a stored query, projected for the catalog. + */ +export type ParamDescriptor = { + item_kind?: null | ParamKind; + kind: ParamKind; + name: string; + /** + * `false` → the caller must supply it; `true` → optional. + */ + nullable: boolean; + /** + * Dimension when `kind == vector`. + */ + vector_dim?: number | null; +}; + +/** + * The kind of a stored-query parameter, decomposed so a client (e.g. an + * MCP server) can build a typed input schema with a closed `match` and + * never re-parse omnigraph's type spelling. `bigint`/`date`/`datetime`/ + * `blob` are carried as JSON strings on the wire: a 64-bit integer past + * 2^53 loses precision as a JSON number, and Date/DateTime are ISO + * strings, Blob a blob-URI string. + */ +export const ParamKind = { + STRING: "string", + BOOL: "bool", + INT: "int", + BIGINT: "bigint", + FLOAT: "float", + DATE: "date", + DATETIME: "datetime", + BLOB: "blob", + VECTOR: "vector", + LIST: "list", +} as const; + +/** + * The kind of a stored-query parameter, decomposed so a client (e.g. an + * MCP server) can build a typed input schema with a closed `match` and + * never re-parse omnigraph's type spelling. `bigint`/`date`/`datetime`/ + * `blob` are carried as JSON strings on the wire: a 64-bit integer past + * 2^53 loses precision as a JSON number, and Date/DateTime are ISO + * strings, Blob a blob-URI string. + */ +export type ParamKind = (typeof ParamKind)[keyof typeof ParamKind]; + +/** + * Response for `GET /queries`: the `mcp.expose` subset of a graph's + * stored-query registry, each with typed parameters. + */ +export type QueriesCatalogOutput = { + queries: Array; +}; + +/** + * One entry in the stored-query catalog (`GET /queries`). + */ +export type QueryCatalogEntry = { + description?: string | null; + instruction?: string | null; + /** + * `true` for a stored mutation → an MCP read-only hint of `false`. + */ + mutation: boolean; + /** + * Registry key / invoke path segment (`POST /queries/{name}`). + */ + name: string; + params: Array; + /** + * MCP tool id (the `tool_name` override, else `name`). + */ + tool_name: string; +}; + /** * Inline read-query request for `POST /query`. * @@ -789,6 +897,91 @@ export type MutateResponses = { export type MutateResponse = MutateResponses[keyof MutateResponses]; +export type ListQueriesData = { + body?: never; + path?: never; + query?: never; + url: "/queries"; +}; + +export type ListQueriesErrors = { + /** + * Unauthorized + */ + 401: ErrorOutput; + /** + * Forbidden + */ + 403: ErrorOutput; +}; + +export type ListQueriesError = ListQueriesErrors[keyof ListQueriesErrors]; + +export type ListQueriesResponses = { + /** + * Stored-query catalog (the mcp.expose subset, with typed params) + */ + 200: QueriesCatalogOutput; +}; + +export type ListQueriesResponse = + ListQueriesResponses[keyof ListQueriesResponses]; + +export type InvokeQueryData = { + body?: null | InvokeStoredQueryRequest; + path: { + /** + * Stored query name (the registry key) + */ + name: string; + }; + query?: never; + url: "/queries/{name}"; +}; + +export type InvokeQueryErrors = { + /** + * Bad request (param type error; snapshot on a stored mutation) + */ + 400: ErrorOutput; + /** + * Unauthorized + */ + 401: ErrorOutput; + /** + * Forbidden (the inner `change` gate for a stored mutation) + */ + 403: ErrorOutput; + /** + * Unknown stored query, or `invoke_query` denied — indistinguishable to a caller without the grant + */ + 404: ErrorOutput; + /** + * Merge conflict + */ + 409: ErrorOutput; + /** + * Per-actor admission cap exceeded; honor `Retry-After` header + */ + 429: ErrorOutput; + /** + * Policy evaluation error (a denial is reported as 404, not 500) + */ + 500: ErrorOutput; +}; + +export type InvokeQueryError = InvokeQueryErrors[keyof InvokeQueryErrors]; + +export type InvokeQueryResponses = { + /** + * Read envelope (ReadOutput) or mutation envelope (ChangeOutput), serialized untagged + */ + 200: InvokeStoredQueryResponse; +}; + +export type InvokeQueryResponse = + InvokeQueryResponses[keyof InvokeQueryResponses]; + export type QueryData = { body: QueryRequest; path?: never; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index e8bc281..f11b3b1 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -60,6 +60,12 @@ export type { ReadTarget, Snapshot, SnapshotTable, + // Stored queries + Queries, + QueryCatalogEntry, + ParamDescriptor, + InvokeQuery, + InvokeQueryInput, // Conflict / errors / shared MergeConflict, ManifestConflict, @@ -76,4 +82,5 @@ export { ErrorCode, LoadMode, MergeConflictKindOutput, + ParamKind, } from './types'; diff --git a/packages/sdk/src/resources/queries.ts b/packages/sdk/src/resources/queries.ts new file mode 100644 index 0000000..9e4bd72 --- /dev/null +++ b/packages/sdk/src/resources/queries.ts @@ -0,0 +1,53 @@ +import type { Transport } from '../transport'; +import type { InvokeQuery, InvokeQueryInput, Queries } from '../types'; +import type { CallOptions } from '../internals'; + +// Mirror client.ts: `params` keys are caller-controlled (matched by name to +// `$var` in the stored query) and a stored *read* returns opaque +// `rows`/`columns` — both must survive the camel<->snake boundary verbatim. +const OPAQUE_PARAMS = new Set(['params']); +const OPAQUE_READ_RESPONSE = new Set(['rows', 'columns']); + +/** + * The graph's server-side stored queries (the `queries:` registry). Distinct + * from {@link Omnigraph.query}, which runs ad-hoc GQ source sent in the + * request body — these operate on queries curated on the server by name. + */ +export class QueriesResource { + constructor(private readonly t: Transport) {} + + /** + * List the graph's exposed stored queries as a typed tool catalog — the + * `mcp.expose == true` subset of the registry, each with its typed + * parameters. Read-only; the catalog is graph-wide (branch independent). + */ + list(opts: CallOptions = {}): Promise { + return this.t.request('GET', '/queries', { signal: opts.signal }); + } + + /** + * Invoke a curated server-side stored query by `name`. The source comes + * from the registry, not the caller — pass only runtime inputs (`params`, + * `branch`, `snapshot`). Returns a read envelope for a stored read or a + * mutation envelope for a stored mutation (untagged: `Read | Change`). + * + * A `name` the caller lacks `invoke_query` for is indistinguishable from an + * unknown one — both surface as `NotFoundError`. + */ + invoke( + name: string, + input: InvokeQueryInput = {}, + opts: CallOptions = {}, + ): Promise { + return this.t.request( + 'POST', + `/queries/${encodeURIComponent(name)}`, + { + body: input, + signal: opts.signal, + opaqueBodyKeys: OPAQUE_PARAMS, + opaqueResponseKeys: OPAQUE_READ_RESPONSE, + }, + ); + } +} diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 26c6e43..c624977 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -24,10 +24,15 @@ import type { IngestOutput, IngestRequest, IngestTableOutput, + InvokeStoredQueryRequest, + InvokeStoredQueryResponse, LoadMode, ManifestConflictOutput, MergeConflictKindOutput, MergeConflictOutput, + ParamDescriptor as ParamDescriptorOutput, + QueriesCatalogOutput, + QueryCatalogEntry as QueryCatalogEntryOutput, QueryRequest, ReadOutput, ReadRequest, @@ -64,6 +69,13 @@ export type Ingest = Camelize; export type IngestTable = Camelize; export type Read = Camelize; export type ReadTarget = Camelize; +// Stored queries (GET /queries, POST /queries/{name}). +export type Queries = Camelize; +export type QueryCatalogEntry = Camelize; +export type ParamDescriptor = Camelize; +// A stored query is read or mutation; the wire response is untagged +// (ReadOutput | ChangeOutput), so this camelizes to `Read | Change`. +export type InvokeQuery = Camelize; export type Schema = Camelize; export type SchemaApply = Camelize; export type Snapshot = Camelize; @@ -93,6 +105,7 @@ export type ChangeInput = export type ExportInput = Camelize; export type IngestInput = Camelize; export type QueryInput = Camelize; +export type InvokeQueryInput = Camelize; export type ReadInput = Camelize; export type SchemaApplyInput = Camelize; @@ -104,6 +117,7 @@ export { BranchMergeOutcome, LoadMode, MergeConflictKindOutput, + ParamKind, } from './generated/types.gen'; // CamelErrorOutput is the camelCased version surfaced on OmnigraphError.body. diff --git a/packages/sdk/src/version.gen.ts b/packages/sdk/src/version.gen.ts index c81fb87..327b92d 100644 --- a/packages/sdk/src/version.gen.ts +++ b/packages/sdk/src/version.gen.ts @@ -6,4 +6,4 @@ * The SDK targets the corresponding OpenAPI spec exactly; behaviour against * a different server major.minor is undefined. */ -export const SERVER_VERSION = "0.6.0"; +export const SERVER_VERSION = "0.6.1"; diff --git a/packages/sdk/test/queries.test.ts b/packages/sdk/test/queries.test.ts new file mode 100644 index 0000000..9f5bb54 --- /dev/null +++ b/packages/sdk/test/queries.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from 'vitest'; +import Omnigraph from '../src'; +import { stubFetch } from './helpers'; + +describe('queries resource (stored queries)', () => { + it('list sends GET /queries and camelizes the catalog', async () => { + const { fetch, calls } = stubFetch({ + body: { + queries: [ + { + name: 'find_inactive', + tool_name: 'find_inactive', + mutation: false, + description: 'inactive users', + instruction: null, + params: [ + { name: 'days', kind: 'int', nullable: false, item_kind: null, vector_dim: null }, + ], + }, + ], + }, + }); + const og = new Omnigraph({ baseUrl: 'http://x', fetch }); + const r = await og.queries.list(); + expect(calls[0]?.method).toBe('GET'); + expect(calls[0]?.url).toBe('http://x/queries'); + expect(r.queries[0]?.toolName).toBe('find_inactive'); + expect(r.queries[0]?.mutation).toBe(false); + // Typed-catalog fields are camelized (not opaque). + expect(r.queries[0]?.params[0]?.itemKind).toBeNull(); + }); + + it('invoke escapes the name in the path and keeps params/rows opaque', async () => { + const { fetch, calls } = stubFetch({ + body: { + query_name: 'find_inactive', + row_count: 1, + columns: ['$u.name'], + rows: [{ '$u.name': 'Alice' }], + }, + }); + const og = new Omnigraph({ baseUrl: 'http://x', token: 't', fetch }); + const r = await og.queries.invoke('find inactive', { + params: { min_days: 30 }, + branch: 'main', + }); + expect(calls[0]?.method).toBe('POST'); + expect(calls[0]?.url).toBe('http://x/queries/find%20inactive'); + const body = JSON.parse(calls[0]?.body ?? '{}'); + // `params` is opaque: the snake_case key must reach the wire unchanged. + expect(body.params).toEqual({ min_days: 30 }); + expect(body.branch).toBe('main'); + // A stored read returns a Read envelope; rows/columns are opaque (verbatim). + const read = r as { rowCount: number; rows: Array> }; + expect(read.rowCount).toBe(1); + expect(read.rows[0]?.['$u.name']).toBe('Alice'); + }); +}); diff --git a/spec/openapi.json b/spec/openapi.json index d1fa337..aced64d 100644 --- a/spec/openapi.json +++ b/spec/openapi.json @@ -7,7 +7,7 @@ "name": "MIT", "identifier": "MIT" }, - "version": "0.6.0" + "version": "0.6.1" }, "paths": { "/branches": { @@ -829,6 +829,177 @@ ] } }, + "/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": [ + "queries" + ], + "summary": "Invoke a curated, server-side stored query by name.", + "description": "The query source comes from the graph's `queries:` registry, not the\nrequest body — callers send only runtime inputs (`params`, `branch`,\n`snapshot`). Gated by the `invoke_query` Cedar action at the boundary;\na stored *mutation* additionally passes the engine's `change` gate\n(double-gated). An actor **without** `invoke_query` cannot tell a denied\nquery from a missing one — both return the same 404, so the catalog\ncan't be probed without the grant. Once `invoke_query` is held, the\ninner `read`/`change` gate may surface a 403 for an existing query the\nactor can't run (the intended double-gate signal).", + "operationId": "invoke_query", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Stored query name (the registry key)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/InvokeStoredQueryRequest" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Read envelope (ReadOutput) or mutation envelope (ChangeOutput), serialized untagged", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvokeStoredQueryResponse" + } + } + } + }, + "400": { + "description": "Bad request (param type error; snapshot on a stored mutation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "403": { + "description": "Forbidden (the inner `change` gate for a stored mutation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "404": { + "description": "Unknown stored query, or `invoke_query` denied — indistinguishable to a caller without the grant", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "409": { + "description": "Merge conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "429": { + "description": "Per-actor admission cap exceeded; honor `Retry-After` header", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + }, + "500": { + "description": "Policy evaluation error (a denial is reported as 404, not 500)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorOutput" + } + } + } + } + }, + "security": [ + { + "bearer_token": [] + } + ] + } + }, "/query": { "post": { "tags": [ @@ -1628,6 +1799,40 @@ } } }, + "InvokeStoredQueryRequest": { + "type": "object", + "description": "Body for `POST /queries/{name}` — invokes the server-side stored query\nnamed in the path. The query source and name come from the registry,\nnever the body; only the runtime inputs are supplied here.", + "properties": { + "branch": { + "type": [ + "string", + "null" + ], + "description": "Branch to run against. Defaults to `main`; for a stored mutation the\nwrite targets this branch." + }, + "params": { + "description": "JSON object whose keys match the stored query's declared parameters." + }, + "snapshot": { + "type": [ + "string", + "null" + ], + "description": "Snapshot id to read from (read queries only — rejected for a stored\nmutation). Mutually exclusive with `branch`." + } + } + }, + "InvokeStoredQueryResponse": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReadOutput" + }, + { + "$ref": "#/components/schemas/ChangeOutput" + } + ], + "description": "Response for `POST /queries/{name}`: the read envelope for a stored\nread, or the mutation envelope for a stored mutation. Serialized\n**untagged**, so the wire shape is exactly [`ReadOutput`] or\n[`ChangeOutput`] — classification follows the stored query, not a\nwrapper field." + }, "LoadMode": { "type": "string", "description": "Shadow enum for documenting [`LoadMode`] in the OpenAPI schema.", @@ -1698,6 +1903,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.",