Skip to main content

MCP server (AI agents)

The manager embeds an MCP (Model Context Protocol) server at POST /mcp on the REST port (default :20900), so AI agents such as Claude Code, Claude Desktop, or Cursor can discover schemas, run SQL with full RBAC/RLS/CLS enforcement, use DuckLake time travel, and (for admin credentials) operate pools and nodes.

The transport is stateless Streamable HTTP: each POST carries one JSON-RPC message and the response is plain JSON. There is no SSE, no server push, and no session id, so any HA replica answers any request. GET /mcp returns 405.

MCP gives an agent tools on the manager; the operator skill gives it the operational playbook for the qod CLI. They combine well.

Authentication

/mcp accepts exactly two credentials in the Authorization: Bearer <token> header:

CredentialPrincipal
Personal access token (qod_pat_...)The owning user, with that user's live tenant, role, and grants - narrowed by any scope minted onto the token
The static API key (QOD_API_KEY)Superuser-equivalent; only when the key is set non-empty

Session JWTs, passwords, and missing headers are all 401. /mcp never admits unauthenticated requests.

Create a PAT from the profile page in the UI, or with the CLI:

qod auth pat create --name claude-code
# the token is printed ONCE; store it now

By default a PAT carries its owner's full permissions; the scope flags described under Scoped tokens and delegation can mint it narrower. A token can be revoked at any time (qod auth pat revoke --id <id>, or the profile page); revoking cascades to every token minted from it. A revoked or expired token stays visible in the listing until you discard it with qod auth pat delete --id <id> (or the profile page's Delete button); a live token must be revoked before it can be deleted. Tenant-scoped principals get their tenant inferred from the PAT owner; superuser and static-key callers pass an explicit tenant argument on tools that need one.

Scoped tokens and delegation

A token can be minted narrower than its owner, so an agent holds a credential it cannot exceed even when fully compromised by an injected instruction. Every axis is optional on qod auth pat create (REST: the same field names on POST /api/auth/pat/create):

AxisCLI flagMeaning
roles--role (repeatable)Restrict the token to these roles
databases--database (repeatable)Restrict the token to these databases
pools--pool (repeatable)Restrict the token to these pools
tools--tool (repeatable)Restrict the MCP tool surface the token may call
verbCeiling--verb-ceilingCap write power: RO, RW, DDL or ALL
dropAdmin--drop-adminStrip the owner's superuser/admin standing from this token
stmtTimeoutMs--stmt-timeout-msPer-statement timeout, milliseconds
maxRows--max-rowsRow cap on this token's queries

An axis left out is unrestricted on that axis (the token inherits the owner's reach); an axis sent as an empty array restricts the token to nothing on that axis. Scope always intersects the owner's grants and can never widen them - a mint request that tries to widen any axis is refused, naming the axis. Row-level and column-level policies that apply to the owner apply to every token they mint, untouched.

Tokens mint tokens. A PAT presented on the REST API may create a further-scoped child of itself, list its own descendants, and revoke or delete within its own subtree only - never itself, a sibling, its parent, or any other token of its owner. An axis narrowed at mint time can only narrow further down the chain, a child's expiry is clamped to its parent's, and chains are depth-capped (QOD_PAT_MAX_DEPTH, default 8). Revoking a token revokes its whole subtree in the same statement, so a stolen token cannot be rolled forward past its own revocation by minting a successor first.

A typical agent credential - read-only, one database, bounded per statement:

qod auth pat create --name analyst-agent \
--database acme_tpch --verb-ceiling RO \
--stmt-timeout-ms 30000 --max-rows 10000

Client configuration

Claude Code:

claude mcp add --transport http qod http://localhost:20900/mcp \
--header "Authorization: Bearer qod_pat_..."

Claude Desktop or any client that takes a JSON server entry:

{
"url": "https://your-manager:20900/mcp",
"headers": { "Authorization": "Bearer qod_pat_..." }
}

Tools: data tier (every authenticated principal)

ToolArgumentsReturns
run_sqlsql, database, pool?, max_rows?Columns and rows as JSON, a truncated flag, rows affected for writes
list_databasessuperuser: tenant?Tenant databases with kind and pools
list_tablesdatabase, schema?Schemas and tables
describe_tabledatabase, schema, tableColumns and types plus a few sample rows
table_historydatabase, schema, table, limit?Snapshot history with change verbs
list_snapshotsdatabase, limit?Snapshots and tags, for time-travel queries (AT (VERSION => n))
my_usagenoneOwn usage counters and recent statements (PAT principals only)

run_sql executes through the same in-process path as the FlightSQL edge: statement validation (ACL), classification, routing, then the node. RBAC verbs decide whether writes are allowed, RLS/CLS apply, and suspended pools wake on the first statement exactly as they do for FlightSQL clients. Results are capped server-side by QOD_MCP_MAX_ROWS (default 500); the tool's max_rows argument can only lower the cap, and truncated results carry truncated: true so the agent aggregates or filters instead of paginating blindly.

Tools: admin tier (admin or superuser principals)

ToolNotes
list_pools, get_pool_statusNodes, health, served counts, suspended flag, autoscale band
scale_poolBand refusals (outside_band) surface as tool errors with the reason
suspend_pool, resume_poolScale-to-zero and wake
restart_node, quarantine_node, unquarantine_nodeNode lifecycle
active_statements, kill_statementInspect and kill running statements
run_maintenance, maintenance_runsTrigger and inspect managed maintenance
create_tag, protect_tagProtect only; there is no unprotect and no tag delete
audit_searchFiltered read over the audit log

tools/list is computed per principal: a role=user PAT sees the data tier only; a tenant admin sees both tiers scoped to their tenant; superuser and static-key callers see both cross-tenant. tools/call re-checks the tier server-side. Tool calls land in the audit trail as the acting user.

Deny-list

Some operations exist in no tier and have no code path from /mcp, regardless of credential:

  • Protection-weakening operations: tag unprotect, tag delete, lockdown off, any guardrail loosening
  • Irreversible destruction: tenant delete, database delete or purge, user delete, manifest import
  • Credential and secret operations: password set/reset, PAT management tools, federated secrets
  • RBAC mutations: grants, revokes, memberships, role/group/user create or update

No MCP tool exposes PAT management. Delegation happens over the REST API instead: a PAT presented there may mint and revoke only within its own subtree (see Scoped tokens and delegation), while full PAT management - across all of a user's tokens - requires a logged-in session (UI or CLI).

Errors

Protocol failures (bad token, malformed JSON-RPC, unknown method) come back as HTTP 401 or JSON-RPC error objects. Everything that happens inside a tool (an ACL denial, a SQL error, an outside_band refusal, a "pool is resuming" timeout) returns a normal tools/call result with isError: true and a message written for the agent to act on. Internal exceptions are sanitized to a generic message with a correlation id in the server log.

Configuration

KeyDefaultEnvMeaning
quack-on-demand.mcp.enabledtrueQOD_MCP_ENABLEDServe POST /mcp at all
quack-on-demand.mcp.maxRows500QOD_MCP_MAX_ROWSHard cap on rows returned by run_sql

Statement execution inherits the edge's existing timeouts.

Troubleshooting

  • 401 on every call: the bearer is not a live PAT (revoked, expired, owner disabled) or is a session JWT, which /mcp refuses by design. Mint a fresh PAT.
  • A tool is missing from tools/list: the credential's tier does not include it; admin tools need an admin-owned PAT or the static key.
  • run_sql returns an ACL error: the message names the table and missing verb; grant the owning role RO/RW/DDL as needed.
  • "pool is resuming": the target pool was suspended and is waking; retry in a few seconds.