Docs / Protocols
MCP surface
Generate an MCP server from Postgres — four governed tools that make the API self-describing to AI agents, permission-projected so an agent only ever learns what the caller can see.
gqlith generates an MCP server from your Postgres schema — a Streamable HTTP endpoint with four permanent, governed tools that make the API self-describing to any MCP-capable agent. The agent doesn’t guess at your schema: it asks, and the answer is projected through the caller’s permissions.
Enabled by setting targets.mcpServer: streamable-http. Default endpoint: POST /mcp (JSON-RPC 2.0). Protocol version: 2025-11-25.
POST /mcp
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2025-11-25
Authorization: Bearer <jwt>
The four tools
| Tool | Purpose | Writes? |
|---|---|---|
describe_schema | Schema guide for one entity, or a compact index of every visible entity | No |
filter_help | FiltrQL syntax, operators, and entity-specific examples built from your real schema | No |
graphql_query | Executes a read-only GraphQL document | No |
graphql_mutation | Executes a write GraphQL document | Yes |
The read/write split is enforced at the wire: a mutation { … } document sent to graphql_query is rejected before any handler runs, and the two tools take different parameter names (query vs mutation) so a wrong-tool call fails fast as an input-validation error.
Permission-projected discovery
describe_schema and filter_help never reveal entities, fields, relations, example documents or filterable fields the caller’s role cannot access. A generated example is dropped entirely if it would so much as name something the role can’t see.
describe_schema() with no argument returns a conventions block (pagination, filtering, ordering, nested-mutation semantics, aggregate shapes, limits, scalar wire formats) plus an index of every visible entity. describe_schema('recipes') returns:
- Every field with its type, nullability, enum values, and a semantic annotation —
required-on-create,server-assigned,generated,default-backed. - Relations with cardinality and the exact nested-mutation operators available on each (
nestedOps), so nested writes are discoverable, not guessed. - Operations tagged with the tool that runs them (
createRecipe → graphql_mutation), matching the real API exactly. - Ready-to-run example documents — create, nested create, update, upsert, delete, find, filter + paginate, aggregates — each tagged with the tool to send it to.
Running documents
{
"method": "tools/call",
"params": {
"name": "graphql_query",
"arguments": {
"query": "{ recipes(limit: 10, filterql: \"status = 'published' AND servings >= 4\", orderBy: [createdAt_DESC]) { edges { node { id title servings } } } }"
}
}
}
filterql is FiltrQL — the same grammar the REST ?filter= parameter uses; filter_help('recipes') teaches it to the agent with copy-pasteable examples from your actual columns. RBAC, row filters, tenancy, OCC, rate limiting, depth/complexity limits and audit all apply — the tools are a governed front door to the same pipeline as /graphql, never a bypass.
Nested mutations sent through graphql_mutation run in one atomic transaction: if a child row fails, the parent rolls back too.
Resources, not just tools
The server also exposes MCP resources: entity://<entityKey> (the per-entity descriptor) and gqlith://schema/<entity> (the entity’s runtime schema, which clients can subscribe to — it notifies when a tenant’s custom fields change). With storage enabled, file reads attach an MCP resource_link at gqlith://files/{fileId}: reading it runs the same authorization gate as every download, and an agent that isn’t allowed to see the file gets a refusal — never a signed URL by accident.
Locking discovery down
Schema discovery is governed by the top-level schemaDiscovery { raw, projected } config — two independent risk classes, each a tri-state gate (enabled / disabled / role-restricted):
projected(default enabled) governsdescribe_schema/filter_helpand theentity:///gqlith://schema/resources. Safe to leave on: it only ever shows what the caller can already reach.raw(default disabled) governs the full-SDL surface (schema://sdl, GraphQL__schema).
Error navigation
Errors return guidance the agent can act on without leaving the conversation: an unknown field points at describe_schema('<entity>'); a filter parse error points at filter_help('<entity>').
Limits today
- Subscriptions are not available over MCP — they remain a GraphQL/SSE feature.
- Query and mutation results are capped at
maxResultBytes(default 1 MiB) with a notice; narrow the selection or split the document. - FiltrQL autocomplete (
filter_complete) is not shipped yet;filter_helpis the syntax reference.