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.

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.
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.
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.

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.