Skip to main content
The MCP server (canonic/mcp/server.py) registers 13 tools. Each does transport translation only: parse arguments, call the same core service the CLI calls, serialize the result. No resolution, compilation, or execution logic lives in the MCP layer itself.

Session / contract

contract_info

Returns the serving contract version this daemon implements. Call at session start to confirm compatibility.

negotiate_contract(contract_major: int)

Declares the contract-schema major version your client was built against. The daemon accepts iff contract_major == server major version. Otherwise it fails fast with a clear message rather than serving mismatched behavior.

Discovery

get_overview(domain: str | None = None)

The recommended first call: active metrics grouped by domain with plain-language sample questions, so an agent (or a human) can see what’s askable before drilling into list_metrics or query. Pass domain to narrow to one owning-source group.

list_metrics()

Lists all active canonical metrics this project defines, plus a deduplicated catalog of every dimension queryable against them. Each metric’s dimensions list holds canonical names only: look those up in the top-level dimensions catalog for the human-readable label and source. Use canonical names directly in query() calls.

describe_metric(name: str)

Returns grain, dimensions, measures, and freshness for one metric.

resolve_metric(name: str, context: str | None = None)

Resolves a metric name or alias to its canonical binding. Returns the binding on success, or a structured error when the name is ambiguous or unresolved.

Query execution

compile_query(query: dict)

Compiles a semantic query to dialect-correct SQL + metadata without executing it. query accepts:
  • metrics (list[str])
  • dimensions (list[str]): canonical name values as returned by describe_metric, not natural-language terms
  • filters (list[str]): SQL WHERE predicates, e.g. ["segment = 'smb'", "status = 'active'"]
  • via (list[str]): routes join paths through specific intermediate sources, required when multiple join paths exist between the metric source and a dimension source
  • limit (int | null)
On an ambiguous_join_path error, inspect the returned candidates (each has a via list and a human-readable route) and re-issue with that via value. On an unreachable error for a dimension, check candidates for the correct canonical name and re-issue.

query(query: dict)

Same query shape as compile_query, but compiles and executes read-only. Returns rows plus compiler metadata: resolved bindings, guardrails fired, freshness, and trust_score (tier plus reasons, see Trust score). This is the main path: with --json/over MCP, the payload is identical to canonic query --json.

run_sql(sql: str, connection: str | None = None)

Executes a read-only SQL SELECT on a named connection (or the project default). Rejects non-SELECT statements with READ_ONLY_VIOLATION. Use only when no metric/dimension in list_metrics() covers the question: query/compile_query route joins through the resolved join graph and apply guardrails that a hand-written join across fact tables won’t get, and can silently multiply values like revenue.

Knowledge

search_knowledge(query: str, user: str | None = None, limit: int = 5)

Searches the project’s knowledge pages for business context: definitions, caveats, and policies. Returns ranked hits plus any caveats auto-surfaced because a hit references their bound semantic entity. Call this before answering any question about what a metric means or how it’s calculated: this project’s definitions may differ from textbook ones. Returns empty hits when the project has no knowledge pages. Each hit’s score is a rank-fusion value (RRF), not a confidence or relevance percentage. Use hit order, not the raw number, to judge how well a result matches. See canonic knowledge search for the exact formula.

read_knowledge_page(page: str, user: str | None = None)

Retrieves the full content of a knowledge page by its id (page slug). Use after search_knowledge() to read the complete definition, caveat, or policy: the rendered body has live {{ sl:entity.expr }} definitions substituted, plus metadata, drift review flags, staleness warnings, and linked references.

Reports

A report (reports/*.yaml) names an ordered sequence of already-existing calls: no new execution semantics, no new authority. run_report is a deterministic loop over query(), optionally attaching a rendered knowledge page per section. See Report schema for the file format.

list_reports(domain: str | None = None)

A directory listing of every committed report: id, title, description, owner, domain. No execution, no per-section detail. Pass domain to narrow to reports declaring a matching domain field. Call this when a user asks for a report by name (e.g. “the customer report”) rather than an ad-hoc question, then call run_report() with the matching id.

run_report(report_id: str, as_of: str | None = None, user: str | None = None)

Runs every section of a committed report through query(), in declared order, and returns an ordered array of section results. Each entry carries the section’s title and either its unmodified QueryResult (with an optional attached narrative, the rendered body of the section’s narrative_from knowledge page) or a structured {code, message, candidates?} error in place of a result. A failing section does not abort the report: the other sections still return normal results, and the call as a whole never raises for a per-section failure. An unknown report_id reuses the existing unresolved error code: there is no dedicated “report not found” code. as_of is an optional ISO-8601 reference point for finality-watermark evaluation, applied to every section that doesn’t set its own.