Skip to main content
canonic exposes its capabilities to agent clients through a local, on-demand MCP server: no always-on hosted service. Verified with Claude Code, Cursor, and Codex.

1. Start the daemon

The daemon binds locally and reads your committed context (semantics, knowledge, contracts):
See canonic mcp for the full flag reference, including --transport http for a background daemon on a fixed host/port.

2. Register canonic in your client’s MCP config

MCP clients typically spawn the server with an arbitrary working directory (not your project folder), so pass --project explicitly rather than relying on cwd detection:
If canonic isn’t installed globally (see Installation), point the client at uvx instead so it fetches canonic on demand:
Pin a version ("args": ["canonic==0.5.1", "mcp", "start", ...]) if you want reproducible daemon versions instead of always resolving the latest release from PyPI. See your client’s own docs for the exact config file location. Claude Code, Cursor, and Codex each load standard MCP configuration. If you started the daemon with --transport http (see canonic mcp), point your client at the HTTP endpoint instead of spawning a process. With a bearer token configured (mcp.auth.tokens):
Adjust the host/port to match the --host/--port flags used to start the daemon. The token must match one resolved from mcp.auth.tokens/--token-ref on the daemon side. With OAuth configured (mcp.auth.oauth) instead, an MCP client that speaks OAuth 2.1 discovers the daemon’s auth requirements itself and prompts for login (proxy mode) or expects you to supply an IdP-issued JWT (jwt mode): no Authorization header to hand-configure, though the exact flow depends on your client’s own OAuth support. See the remote/enterprise deployment section below for both.

Remote/enterprise deployment

--transport http is meant for a daemon running centrally (e.g. in a data center) with clients connecting from local machines over the network, not just local loopback. Local binding is not a security boundary once the daemon is network-reachable, so --transport http refuses to start without at least one auth mechanism configured: a bearer token, OAuth 2.1, or both (they compose: a static token for a CI pipeline and OAuth for interactive human users on the same daemon is a supported setup, not just tolerated). This is also why canonic mcp start --tenant <id> (the local-development principal override, see Tenancy & access control) is refused outright on --transport http: each HTTP request already derives its own principal from its verified token, so a single flag-supplied tenant applying to every caller would undo per-request isolation. Bearer tokens: one per client, revoked by editing canonic.yaml:
Each client authenticates with its own token (Authorization: Bearer <token>), and the resolved client_id is attributed on every query/run_sql answer event in .canonic/events.jsonl, so usage is traceable per user, not just per daemon instance. The HTTP daemon also serves unauthenticated /livez and /readyz probe endpoints for load balancers and Kubernetes. See Health endpoints.

Binding tenant/role claims to a token

When the project has a tenancy and/or role policy configured (contracts/policies/tenancy.yaml / roles.yaml), a token also needs to carry the claims those policies name (claim: merchant_id, claim: roles, or whatever the policy declares). A static bearer token has no IdP to fetch claims from at request time, so it carries them inline via claims:
Each of these two tokens now resolves to a distinct Principal (same daemon, same warehouse, isolated rows) with every compiled query for merchant-4711-agent carrying orders.merchant_id = '4711' and nothing else. With OAuth instead of static tokens, the claims arrive in the verified JWT itself. Use mcp.auth.oauth.claim_mapping to rename a namespaced IdP claim key (e.g. https://example.com/merchant_id) to the policy’s own claim name:
A claim absent from claim_mapping is looked up under its own name unchanged. See Tenancy & access control for how the resulting Principal flows into the compiler, and config schema for the full field list. OAuth 2.1: for SSO-integrated organizations that want per-user identity tied to their own IdP and centralized revocation instead of a shared secret to distribute and rotate:
mode: proxy presents a DCR-compliant OAuth server to MCP clients and relays the login to your IdP (Authorization Code + PKCE), the mode most deployments use, since most enterprise IdPs don’t support MCP’s Dynamic Client Registration natively. mode: jwt is simpler: the IdP hands the client a JWT directly and the daemon only verifies its signature against the IdP’s published JWKS, no proxy or redirect handling. Either way, the resolved client_id is attributed on every answer event the same way a token’s client_id is. See the config schema reference for the full field list. canonic mcp status reports which mechanism(s) are active on a running daemon (e.g. token, oauth-proxy), useful for confirming a deployment matches its intended config. TLS termination is expected to happen at a reverse proxy/ingress in front of the daemon, not in the daemon itself. For a runnable end-to-end example with a real IdP, see Marketplace with Keycloak: a local Keycloak plus a dockerized daemon, with role and tenant claims flowing from the login into the query. Headless/agentic clients: mode: proxy also accepts an IdP-signed identity assertion (SEP-990 ID-JAG) in place of the interactive Authorization Code + PKCE flow, for a client that already holds an established identity from the IdP and cannot open a browser to log in:
Under a tenancy or role policy, the IdP must sign the tenancy and role claims into the assertion itself, since canonic reads the principal from the verified assertion’s claims (after claim_mapping) and from nothing else. The answer is scoped to the employee named in the assertion, not to the agent, and .canonic/events.jsonl records both: "user": {"subject": "<employee>", "acted_via": "<agent client_id>"}. Prefer this over a static mcp.auth.tokens entry for an agentic client that already participates in your IdP: identity stays centrally revocable and per-caller, instead of a shared secret to distribute and rotate by hand. Static tokens remain fully supported for pipelines or clients with no IdP identity of their own, and the two compose on the same daemon, same as tokens and oauth do generally. Signature and replay verification of the assertion itself is FastMCP’s, not canonic’s, to reimplement. CI pipelines and scheduled jobs: prefer the OAuth 2.0 client-credentials grant against your IdP over a static mcp.auth.tokens entry. The job requests a short-lived token from the IdP before calling canonic (FastMCP’s client-side ClientCredentialsOAuthProvider implements this), so the credential is revocable at the IdP and never written to canonic.yaml. The daemon verifies the resulting token like any other bearer token, so no server-side setting changes. Static tokens remain supported.
In proxy mode, check whether your IdP issues opaque (non-JWT) access tokens: Google and GitHub both do, and some Okta setups too. OIDCProxy verifies the upstream access token by default (verify_id_token: false), and an opaque one fails that outright. Set verify_id_token: true to verify the OIDC id_token instead, which is always a standard JWT. This also decides what client_id looks like in .canonic/events.jsonl: the access token’s client_id/azp/sub claim by default, often an opaque subject id, versus the id_token’s, which reliably carries sub/email and is what you want if per-user attribution in logs is the point of switching to OAuth in the first place.
Upgrading a proxy-mode deployment from a canonic release built on FastMCP 3 to one built on FastMCP 4 is a one-time re-authorization event, if and only if your issuer_url differs from the daemon’s public base URL. FastMCP 4 stamps issuer_url into the iss claim of the tokens it mints, so tokens minted before the upgrade no longer validate and every client, including its refresh token, has to log in again. Deployments where the two URLs are equal are unaffected. Announce the window before restarting, and confirm afterwards with canonic mcp status, which reports the FastMCP version the running daemon serves.
In proxy mode, the daemon keeps its OAuth client registrations and signing key in memory by default. Restarting the daemon (canonic mcp stop && canonic mcp start) drops any Dynamic Client Registration state and invalidates previously minted tokens. Clients transparently re-register and re-authenticate, but an in-flight session is disrupted. jwt mode has no such state: it only verifies tokens against the IdP’s JWKS.

Background tasks for long-running queries

query, run_sql and run_report can run as background tasks on --transport http, so a client that supports the MCP tasks extension can poll a slow query instead of holding the connection open for its full duration:
This is additive, not a replacement: a client that doesn’t declare the tasks capability still gets the same synchronous response as before, on the same tool. Requires the canonic[tasks] extra (pip install "canonic[tasks]"). The daemon refuses to start with a clear error if mcp.tasks.enabled: true is set and the extra isn’t installed. Not available under --transport stdio, a local subprocess has no reason to hold a request open in the first place. See config schema for the full field list.

Listing cache hints

A conforming client may cache list_metrics/describe_metric/tool listings for mcp.cache_ttl_seconds (default 300) before re-fetching, cutting round trips for a client that re-lists frequently:
The hint is scoped private always, never shared across callers, since a shared cache would leak one caller’s listing to another once tenancy or role-scoped tools are in play. Tool call results (query, run_sql, run_report answers) are untouched, only listings. Honoring the hint is opt-in and modern-era-only on the client side, so a legacy client or one that doesn’t ask for caching is unaffected either way.

Environment variables for credentials

If a connection’s credentials_ref points at env:SOME_VAR (e.g. a database password), that variable must be readable by the spawned canonic process. GUI-launched MCP clients (Claude Desktop, Cursor, etc.) start the process with a minimal environment. They do not source your shell profile (~/.zshrc, ~/.bash_profile), so an export that works in your terminal will not reach the daemon. Pass the variable explicitly via the config’s env field instead:
A missing or empty variable surfaces as an internal_error at query time, not at startup. If you see one, check the connection’s credentials_ref in canonic.yaml and confirm the named variable is set in the MCP config’s env block.

Follow-up suggestions

Add --suggestions to mcp start to have query responses include a metadata.related field: unused dimensions on the resolved metric and sibling metrics on the same source, so the agent can see what else is queryable without an extra describe_metric round trip. Off by default, descriptive only (no ranking or recommendation), same additive pattern as guardrails_fired/freshness.

3. Your agent now has these tools

See Tools reference for the full list and payload shapes. Every answer-producing tool returns the metadata band alongside the result (resolved definition, guardrails fired, freshness, final/provisional), so the agent can caveat honestly. On ambiguity or a blocked guardrail, the tool returns the candidates or the rationale instead of a guess, and the agent is expected to relay that rather than fabricate an answer.