How Précis-MCP works¶
One page on the moving parts and how they fit, before the per-topic guides go deep. The mental model: you bring data and a description of your financial model; the server turns MCP tool calls into governed, read-only queries against them. Everything else is detail on one of those three clauses.
┌┄┄┄ your warehouse / file drops
┆ │
┆ │ ingestion: extract → validate → atomic swap instance/ (your model)
┆ ▼ ├─ live/ table DDL
┆ ClickHouse ├─ semantic/ SQL views
┆ ┌──────────────────────────────┐ ├─ catalogue/ metrics YAML
┆ │ live.* your data │ provisioned from ─────►├─ integrations/ sources+bindings
┆ │ semantic.* your meaning │ └─ scenarios.yml dataset registry
┆ └──────────────▲───────────────┘
┆ federated read │ reads semantic.* only
┆ (long-tail detail, │
┆ queried in place) │
└┄┄┄┄┄┄┄┄┄┄┄┄► metric engine ◄── catalogue (what exists, how it's computed)
▲
read-only tools (run_statement, run_metric, inspect_rows, discovery)
▲
┌──────────────┴───────────────┐
│ /mcp — OAuth 2.1, multi-user │ PostgreSQL: users, profiles,
│ /mcp — dev key, single-user │ load audit, load locks
└──────────────▲───────────────┘
MCP client (Claude, ChatGPT, your agent)
The instance directory — your model as configuration¶
Everything specific to your business lives in one directory, instance/,
separate from the installed package: the table DDL (live/), the SQL views
that define what your data means (semantic/), the YAML catalogue of
metrics, dimensions, and statements (catalogue/), the ingestion
configuration (integrations/), and the dataset registry
(scenarios.yml). The repository ships a complete demo instance showing the
shape; deployments mount their own over it. If you remember one thing: code
is the engine, instance/ is the model — adapting Précis-MCP to your
business means editing instance/, not Python.
ClickHouse — the read layer¶
All querying happens against ClickHouse, in three databases the provisioner
creates from your instance/: live (your data), staging (ingestion's
landing zone, swapped atomically into live), and semantic (the views the
engine reads — its only query surface). You can run the bundled ClickHouse
or bring your own cluster, empty or with demo data —
data modes — and the
schema contract defines
exactly what a ready cluster contains.
Semantic layer and catalogue — meaning, then metrics¶
Your model is described in two deliberate layers. The semantic layer (SQL views) says what the data means — what a P&L row is, which accounts are revenue. The catalogue (YAML) defines the metrics — and the statements and dimensions built on them — over those views: what each one is, how it's computed (aggregation, derived formulas), and how it's formatted. That definition is also the contract clients query against, so a metric exists for them only once it's catalogued. The same name must appear in both (view column → catalogue reference → field the client receives), and the engine validates the whole chain at startup, refusing to serve an inconsistent model. The walkthrough is Catalogue & semantic model; the change contract is Adding metrics & dimensions.
The metric engine¶
Every reporting call runs one pipeline: resolve the request against the
catalogue (expand statements into metrics, derived metrics into their
inputs), apply the caller's permission scope, retrieve (ClickHouse SQL
against semantic.* — or Ibis expressions for a federated domain that reads
a warehouse table in place), then transform and format. Because every client
goes through the same catalogue definitions, two people asking the same
question get the same number.
Federated reads — the long tail stays in your warehouse¶
The intended split: the datasets that answer most questions — the GL, the
aggregated facts your statements read — are landed in ClickHouse, where
the engine has its full vocabulary: every aggregation, avg and closing
rollups, multi-grain totals, hierarchy filters. For the long tail — very
detailed, rarely-queried data that is too large or too governance-sensitive
to copy (a revenue subledger, raw worklogs) — a catalogue domain can declare
backend_kind: ibis, and the engine queries the table in place on your
warehouse, through the same Source declaration (and credentials) that
ingestion uses.
The trade is deliberate, and the catalogue validator enforces it:
- Sum only. Federated metrics must be
aggregation: sumwithrollup_method: sum; totals are rolled up additively from one detail read.avgandclosingneed the data landed. - Filters only on modelled dimensions. Filters and security scope
resolve against the master data in ClickHouse and are applied to the
foreign table as
IN (…)predicates — so the foreign view must expose the native dimension key columns. Columns that exist only on the source can be exposed assource_inline: trueaxes: group-by only, never filterable or scopable. - Never a cross-store join. One query against one backend per request; master data stays canonical in ClickHouse.
- Read-only, and always
versioned: false.
When a long-tail dataset starts carrying mainline reporting, the move is to
land it: the Source already exists, so it's one binding and a DDL file —
and the engine's full functionality applies. The how-to is in
Adding metrics & dimensions.
Scenarios — which dataset a number comes from¶
A scenario identifies the dataset behind a figure — actuals, a budget, a
forecast. It's a column in your data, registered in instance/scenarios.yml
with a kind (ACTUAL / BUDGET / FORECAST); clients pick one or compare
several at query time, and the engine adds a comparison vocabulary (variance,
shifted views) on top. Scenarios are data, not catalogue entities — adding a
forecast means loading rows that carry its id and seeding its registry row.
Ingestion — optional, and atomic¶
If your data team already populates ClickHouse, you can skip ingestion
entirely and run query-only. Otherwise, you declare sources (a warehouse
connection or file drop) and bindings (one source → one live.* table,
with an extract query and a schedule), and every load runs extract →
validate → swap: rows land in staging, the column shape is checked, and
promotion into live is atomic — queries never see a half-loaded state.
PostgreSQL keeps the audit trail (load_history) and serialises
concurrent loads (a per-binding advisory lock). Reference: Ingestion;
procedure: Onboarding a data source.
Transport and identity — who may read what¶
Two entrypoints serve the same tools: a localhost, single-user dev server
(static key, three deliberate gates — the quickstart) and
the multi-user /mcp endpoint (OAuth 2.1 + PKCE). Sign-in posture is one
variable, PRECIS_AUTH_MODE: a dev key (A), the bundled Keycloak,
optionally brokered to your corporate IdP
(B), or your own OIDC IdP trusted directly (C) —
Remote access. Authentication only
identifies: what a user may read is granted by their
profile (scenario → role → domain and
dimension scope), enforced inside every query. The advertised tool surface is
the MCP tool reference.
What this package deliberately is not¶
Précis-MCP is the read-only access surface of the Précis platform. It never writes to your data, and the agent runtime, planning and write-back, charts, Excel export, and report building live in the full platform, not here. The extension seams those features plug into are documented in Adding read tools — in an open deployment they are simply unregistered.
Where each concern lives¶
| To change… | Edit… | Guide |
|---|---|---|
| Metrics, statements, dimensions | instance/catalogue/ |
Catalogue & semantic model, change contract |
| What your data means | instance/semantic/ (+ re-run the provisioner) |
Catalogue & semantic model |
| Which datasets exist | instance/scenarios.yml |
Schema contract |
| How data gets in | instance/integrations/ + credential env vars |
Ingestion |
| Who may read what | profiles, via the admin CLI | User profiles & permissions |
| How users sign in | PRECIS_AUTH_MODE + mode variables |
Remote access |
| Where ClickHouse runs | PRECIS_DATA_MODE / CH* variables |
Data modes |
| Any single knob | environment variables | Reference |
Vocabulary¶
| Term | Meaning |
|---|---|
| Scenario | A dataset a number comes from: actuals, a budget, a forecast. Registered in scenarios.yml, carried as a column in fact data. |
| Domain | A group of metrics sharing one source view (one catalogue file each). |
| Metric | A base metric aggregates one view column (with optional where: filter); a derived metric is a formula over other metrics. |
| Statement | An ordered list of metric keys rendered as a financial table (P&L, summary). |
| Dimension | Something you slice by. Leaf dimensions own a master table (cost_centre); derived dimensions are parent levels computed from a leaf's attributes (department). |
| Federated domain | A domain whose source view is read in place on your warehouse through Ibis, instead of from ClickHouse — for long-tail detail; sum-only, filters on modelled dimensions only. |
| Source / Binding | Ingestion config: a source is one physical origin of data; a binding ties it to one live.* table with an extract query and schedule. |
| Profile | A named permission set — scenario patterns → role → domain/dimension scope — assigned to users. |
| Instance directory | instance/ — every file that describes your model, mounted into the deployment. |