columna-server 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. columna_server-0.1.0/.gitignore +23 -0
  2. columna_server-0.1.0/CHANGELOG.md +25 -0
  3. columna_server-0.1.0/PKG-INFO +83 -0
  4. columna_server-0.1.0/README.md +59 -0
  5. columna_server-0.1.0/demos/agent_nonexistent_measure_transcript.md +31 -0
  6. columna_server-0.1.0/demos/agent_transcript.md +76 -0
  7. columna_server-0.1.0/demos/mcp_claude_desktop.md +124 -0
  8. columna_server-0.1.0/pyproject.toml +56 -0
  9. columna_server-0.1.0/src/columna_server/__init__.py +2 -0
  10. columna_server-0.1.0/src/columna_server/__main__.py +6 -0
  11. columna_server-0.1.0/src/columna_server/agent/__init__.py +10 -0
  12. columna_server-0.1.0/src/columna_server/agent/conversation.py +14 -0
  13. columna_server-0.1.0/src/columna_server/agent/loop.py +220 -0
  14. columna_server-0.1.0/src/columna_server/agent/mcp_client.py +80 -0
  15. columna_server-0.1.0/src/columna_server/agent/providers.py +115 -0
  16. columna_server-0.1.0/src/columna_server/agent/runner.py +39 -0
  17. columna_server-0.1.0/src/columna_server/agent/system_prompt.md +66 -0
  18. columna_server-0.1.0/src/columna_server/cli.py +136 -0
  19. columna_server-0.1.0/src/columna_server/demo/README.md +7 -0
  20. columna_server-0.1.0/src/columna_server/demo/benchmark/data.toml +10 -0
  21. columna_server-0.1.0/src/columna_server/demo/benchmark/manifold.cml +61 -0
  22. columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/calendar.parquet +0 -0
  23. columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/categories.parquet +0 -0
  24. columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/customers.parquet +0 -0
  25. columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/daily_revenue_summary.parquet +0 -0
  26. columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/engagement_scores.parquet +0 -0
  27. columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/eom_inventory.parquet +0 -0
  28. columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/monthly_avg_order_value.parquet +0 -0
  29. columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/monthly_store_inventory.parquet +0 -0
  30. columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/monthly_unique_visitors.parquet +0 -0
  31. columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/product_categories.parquet +0 -0
  32. columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/products.parquet +0 -0
  33. columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/stores.parquet +0 -0
  34. columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/support_tickets.parquet +0 -0
  35. columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/transactions.parquet +0 -0
  36. columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/warehouse_notes.parquet +0 -0
  37. columna_server-0.1.0/src/columna_server/demo.py +84 -0
  38. columna_server-0.1.0/src/columna_server/frameql.py +106 -0
  39. columna_server-0.1.0/src/columna_server/server.py +53 -0
  40. columna_server-0.1.0/src/columna_server/store.py +124 -0
  41. columna_server-0.1.0/src/columna_server/tools.py +143 -0
  42. columna_server-0.1.0/tests/conftest.py +53 -0
  43. columna_server-0.1.0/tests/fixtures/manifolds/benchmark/data.toml +10 -0
  44. columna_server-0.1.0/tests/fixtures/manifolds/benchmark/manifold.cml +61 -0
  45. columna_server-0.1.0/tests/test_agent.py +211 -0
  46. columna_server-0.1.0/tests/test_demo.py +39 -0
  47. columna_server-0.1.0/tests/test_demo_data_drift.py +50 -0
  48. columna_server-0.1.0/tests/test_frameql_and_auth.py +57 -0
  49. columna_server-0.1.0/tests/test_mcp_server.py +175 -0
@@ -0,0 +1,23 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+ .env
11
+
12
+ # Test / tooling caches
13
+ .pytest_cache/
14
+ .ruff_cache/
15
+ .coverage
16
+ htmlcov/
17
+
18
+ # OS / editor
19
+ .DS_Store
20
+ *.swp
21
+
22
+ # Raw handoff bundle (provenance, not part of the deliverable)
23
+ attachments/
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ All notable changes to **columna-server** are recorded here
4
+ ([Keep a Changelog](https://keepachangelog.com/)).
5
+
6
+ ## [0.1.0] — the MCP server, the packaged demo, and the NL agent
7
+
8
+ First release. The Columna MCP server and its two front doors, over one contract (ADR-032 D8 — the
9
+ four moods as data; `contract_version` `"1"`).
10
+
11
+ - **MCP server** (`columna-server mcp --manifolds <dir>`): five read-only tools —
12
+ `list_manifolds`, `describe_manifold`, `describe_measure`, `query`, `explain` — over a Manifold
13
+ store (`<id>/manifold.cml` + `data.toml`, parsed once at startup). stdio canonical;
14
+ `--http` gated by `COLUMNA_MCP_TOKEN` (constant-time `hmac.compare_digest`). No SQL, no writes.
15
+ The envelope grammar is parsed here; every expression is delegated to columna-core (one dialect).
16
+ - **Packaged demo** (`columna-server demo [--play]`): the benchmark Manifold over a small bundled
17
+ warehouse (byte-identical to the core fixtures, drift-guarded), runnable with no path args.
18
+ `--play` prints the real clarify → refuse → disclose wire transcript.
19
+ - **Natural-language agent** (`columna-server agent`, `[agent]` extra): a true MCP client — spawns
20
+ the server over stdio and speaks the protocol, never importing the engine. Natural language
21
+ becomes a *proposed* Frame-QL query; the four moods drive the conversation (clarifies relayed,
22
+ never auto-picked; every numeral verbatim from the wire — grounding is structural). Provider layer
23
+ (`anthropic` default via `COLUMNA_AGENT_MODEL`; `scripted` for tests).
24
+
25
+ [0.1.0]: https://github.com/datumwise/columna
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: columna-server
3
+ Version: 0.1.0
4
+ Summary: Columna MCP server — a library of Manifolds exposed to AI agents over MCP, one contract (the four moods as data).
5
+ Author: Datumwise
6
+ License-Expression: Apache-2.0
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: License :: OSI Approved :: Apache Software License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: columna-core>=0.7.8
12
+ Requires-Dist: duckdb<2.0,>=1.0
13
+ Requires-Dist: mcp>=1.0
14
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
15
+ Provides-Extra: agent
16
+ Requires-Dist: anthropic>=0.40; extra == 'agent'
17
+ Provides-Extra: http
18
+ Requires-Dist: starlette>=0.37; extra == 'http'
19
+ Requires-Dist: uvicorn>=0.23; extra == 'http'
20
+ Provides-Extra: test
21
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
22
+ Requires-Dist: pytest>=8.0; extra == 'test'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # columna-server
26
+
27
+ The **Columna MCP server** (ADR-032 D8): a library of Manifolds exposed to AI agents over MCP, with
28
+ **one contract** — every tool returns the same outcome/disclosure structure the Python API returns.
29
+ This is the wedge product: the first metrics MCP server that can say *"it depends."*
30
+
31
+ ## Run
32
+
33
+ ```bash
34
+ pip install columna-core columna-server
35
+ columna-server demo --play # the packaged demo: clarify -> refuse -> disclose
36
+ columna-server demo # serve the packaged demo over MCP stdio (no path args)
37
+ columna-server mcp --manifolds <dir> # serve your own manifolds dir over stdio
38
+ columna-server mcp --manifolds <dir> --http :8000 # streamable-http, gated by COLUMNA_MCP_TOKEN
39
+
40
+ # Natural-language agent (a true MCP client over the server); needs the [agent] extra + a key:
41
+ pip install "columna-server[agent]"
42
+ ANTHROPIC_API_KEY=... columna-server agent # chat REPL over the packaged demo
43
+ ```
44
+
45
+ The **agent** turns natural language into a *proposed* Frame-QL query, spawns the server over stdio,
46
+ and lets the four moods drive the conversation — it never touches the engine in-process. The model
47
+ proposes, the planner disposes, the human decides (clarifies are relayed, never auto-picked; every
48
+ number comes verbatim from the wire). See [`demos/agent_transcript.md`](demos/agent_transcript.md).
49
+ Model via `COLUMNA_AGENT_MODEL` (default `claude-opus-4-8`).
50
+
51
+ **Richer run.** The packaged demo ships a small (~330 KB) warehouse. To run the same benchmark
52
+ Manifold over the full 4.7 MB warehouse (299,934 transactions), point `--manifolds` at a directory
53
+ whose `data.toml` warehouse path is the repo's `packages/columna-core/demos/warehouse`.
54
+
55
+ > **From source (contributors):** `git clone https://github.com/datumwise/columna && cd columna`,
56
+ > then `pip install -e packages/columna-core -e "packages/columna-server[test]"` and
57
+ > `pytest packages/columna-server -q`.
58
+
59
+ `<dir>` holds `<id>/manifold.cml` + `data.toml` (connector type + data path). Manifolds are parsed
60
+ and connected once at startup.
61
+
62
+ ## Tools (five, read-only — no SQL, no writes)
63
+
64
+ | tool | touches data? | returns |
65
+ |---|---|---|
66
+ | `list_manifolds()` | no | id, name, description, measure count, universes |
67
+ | `describe_manifold(manifold_id)` | no | dimensions/levels, edges (+ lineage), universes (+ predicate), measure index |
68
+ | `describe_measure(manifold_id, measure)` | no | family triple, per-member anchors, dtype, v-anchor `{universe, grain}`, m-anchor, provenance |
69
+ | `query(manifold_id, frameql, universe?)` | yes | the wire contract (serve/disclose/clarify/refuse/error) |
70
+ | `explain(manifold_id, frameql, universe?)` | **no** (`fetches_delta: 0`) | the would-be outcome + disclosures |
71
+
72
+ `frameql` is `"<columns> @ <anchor>"` (e.g. `"rate: revenue / level.last @ store, day"`); the
73
+ envelope is parsed here and every expression is delegated to columna-core — one expression dialect.
74
+
75
+ ## The contract
76
+
77
+ Outcomes are data (the four moods), and disclosures are structured `{code, materiality, severity,
78
+ category, detail, remedy, source, rel_error}` via `columna_core.disclosure_wire` — the same
79
+ serialization every surface shares. Clarify alternatives are mechanically substitutable (a universe
80
+ pin carries `apply: {"universe": U}`).
81
+
82
+ See [`demos/mcp_claude_desktop.md`](demos/mcp_claude_desktop.md) for a Claude Desktop config and a
83
+ real clarify → answer transcript.
@@ -0,0 +1,59 @@
1
+ # columna-server
2
+
3
+ The **Columna MCP server** (ADR-032 D8): a library of Manifolds exposed to AI agents over MCP, with
4
+ **one contract** — every tool returns the same outcome/disclosure structure the Python API returns.
5
+ This is the wedge product: the first metrics MCP server that can say *"it depends."*
6
+
7
+ ## Run
8
+
9
+ ```bash
10
+ pip install columna-core columna-server
11
+ columna-server demo --play # the packaged demo: clarify -> refuse -> disclose
12
+ columna-server demo # serve the packaged demo over MCP stdio (no path args)
13
+ columna-server mcp --manifolds <dir> # serve your own manifolds dir over stdio
14
+ columna-server mcp --manifolds <dir> --http :8000 # streamable-http, gated by COLUMNA_MCP_TOKEN
15
+
16
+ # Natural-language agent (a true MCP client over the server); needs the [agent] extra + a key:
17
+ pip install "columna-server[agent]"
18
+ ANTHROPIC_API_KEY=... columna-server agent # chat REPL over the packaged demo
19
+ ```
20
+
21
+ The **agent** turns natural language into a *proposed* Frame-QL query, spawns the server over stdio,
22
+ and lets the four moods drive the conversation — it never touches the engine in-process. The model
23
+ proposes, the planner disposes, the human decides (clarifies are relayed, never auto-picked; every
24
+ number comes verbatim from the wire). See [`demos/agent_transcript.md`](demos/agent_transcript.md).
25
+ Model via `COLUMNA_AGENT_MODEL` (default `claude-opus-4-8`).
26
+
27
+ **Richer run.** The packaged demo ships a small (~330 KB) warehouse. To run the same benchmark
28
+ Manifold over the full 4.7 MB warehouse (299,934 transactions), point `--manifolds` at a directory
29
+ whose `data.toml` warehouse path is the repo's `packages/columna-core/demos/warehouse`.
30
+
31
+ > **From source (contributors):** `git clone https://github.com/datumwise/columna && cd columna`,
32
+ > then `pip install -e packages/columna-core -e "packages/columna-server[test]"` and
33
+ > `pytest packages/columna-server -q`.
34
+
35
+ `<dir>` holds `<id>/manifold.cml` + `data.toml` (connector type + data path). Manifolds are parsed
36
+ and connected once at startup.
37
+
38
+ ## Tools (five, read-only — no SQL, no writes)
39
+
40
+ | tool | touches data? | returns |
41
+ |---|---|---|
42
+ | `list_manifolds()` | no | id, name, description, measure count, universes |
43
+ | `describe_manifold(manifold_id)` | no | dimensions/levels, edges (+ lineage), universes (+ predicate), measure index |
44
+ | `describe_measure(manifold_id, measure)` | no | family triple, per-member anchors, dtype, v-anchor `{universe, grain}`, m-anchor, provenance |
45
+ | `query(manifold_id, frameql, universe?)` | yes | the wire contract (serve/disclose/clarify/refuse/error) |
46
+ | `explain(manifold_id, frameql, universe?)` | **no** (`fetches_delta: 0`) | the would-be outcome + disclosures |
47
+
48
+ `frameql` is `"<columns> @ <anchor>"` (e.g. `"rate: revenue / level.last @ store, day"`); the
49
+ envelope is parsed here and every expression is delegated to columna-core — one expression dialect.
50
+
51
+ ## The contract
52
+
53
+ Outcomes are data (the four moods), and disclosures are structured `{code, materiality, severity,
54
+ category, detail, remedy, source, rel_error}` via `columna_core.disclosure_wire` — the same
55
+ serialization every surface shares. Clarify alternatives are mechanically substitutable (a universe
56
+ pin carries `apply: {"universe": U}`).
57
+
58
+ See [`demos/mcp_claude_desktop.md`](demos/mcp_claude_desktop.md) for a Claude Desktop config and a
59
+ real clarify → answer transcript.
@@ -0,0 +1,31 @@
1
+ # The agent on a metric that doesn't exist — a verbatim capture
2
+
3
+ Real, unedited output from **`claude-opus-4-8`** driving `columna-server agent` over the packaged
4
+ demo Manifold (captured live this session). It shows the honest-naming property: asked for a
5
+ plausible-but-fake metric, the agent does **not** invent a measure — it asks which of the manifold's
6
+ real measures is meant.
7
+
8
+ The demo Manifold's measures are `revenue`, `orders`, `visitors`, `level` (family `sum`/`last`, on
9
+ the `store_days` universe), `med_amount`, and `region_label`. There is **no** `sell_through_rate`
10
+ measure — that name is only a *column label* one might give the ratio `revenue / level.last`.
11
+
12
+ ---
13
+
14
+ **you:** now the sell-through rate by store and day
15
+
16
+ **agent** *(verbatim, an `ASK` — no query proposed, no number invented)*:
17
+ > I don't have a `sell_through_rate` measure in this manifold. I do have `revenue`, `orders`,
18
+ > `visitors`, `level` (sum/last, on store_days), and `med_amount`. Which of these would you like by
19
+ > store and day?
20
+
21
+ ---
22
+
23
+ The model proposed neither a fabricated `sell_through_rate` measure nor a guessed number — it asked.
24
+ And the backstop holds even if a model *did* fabricate one: a query naming an unknown measure is
25
+ rejected by the engine (unknown column) and the system presents it as an invalid query, so no fake
26
+ figure can ever reach the human. Both behaviors are covered by tests
27
+ (`test_live_asks_on_a_plausible_but_fake_metric` and
28
+ `test_engine_and_formatter_backstop_a_fabricated_measure`).
29
+
30
+ > *Provenance: captured from a live `@pytest.mark.llm` run against `claude-opus-4-8` on 2026-07-09.
31
+ > The API key used for that run has since been rotated; a fresh re-run needs a new `ANTHROPIC_API_KEY`.*
@@ -0,0 +1,76 @@
1
+ # The Columna query agent — the wedge in natural language
2
+
3
+ `columna-server agent` is Columna's own natural-language front door, and it is **a true MCP client**:
4
+ it spawns `columna-server` over stdio and speaks the protocol — it never touches the engine
5
+ in-process. Natural language becomes a *proposed* Frame-QL query; the planner disposes; the four
6
+ moods drive the conversation. The model proposes, the layer checks, the human decides.
7
+
8
+ Run it against the packaged demo (needs `ANTHROPIC_API_KEY` and the agent extra —
9
+ `pip install -e "packages/columna-server[agent]"`):
10
+
11
+ ```bash
12
+ columna-server agent # packaged demo Manifold, no path args
13
+ ```
14
+
15
+ Below is a real conversation (the transcript the hermetic tests assert against, value rows elided).
16
+ It walks through **all four moods** — serve → clarify → refuse → disclose — in one human exchange.
17
+ Every number shown comes verbatim from the server's wire JSON; the agent never computes or restates
18
+ a figure, so it can never state a wrong one.
19
+
20
+ ---
21
+
22
+ **you:** what's revenue by region?
23
+
24
+ **agent** *(serve)*:
25
+ > Here is the answer:
26
+ > revenue: region="south"=771240.4099999999; region="north"=601075.3299999996; region="east"=615704.5300000003; region="west"=224371.58999999994
27
+
28
+ The agent proposed `QUERY: revenue @ region`; the server served it; the agent presents the values
29
+ exactly as the wire returned them.
30
+
31
+ ---
32
+
33
+ **you:** what's the sell-through rate by store and day?
34
+
35
+ **agent** *(clarify — relayed to you, never auto-picked)*:
36
+ > That question is ambiguous: ratio combines measures over different populations ['store_days',
37
+ > 'transactions'] … which population should the rate be taken over?
38
+ > Please choose:
39
+ > [1] express both numerator and denominator within universe 'store_days'
40
+ > [2] express both numerator and denominator within universe 'transactions'
41
+ > Reply with the number of your choice.
42
+
43
+ `revenue / level.last` spans two populations, so the rate has no single value. The server clarifies;
44
+ the agent relays the alternatives and **waits for your choice**. It does not guess a population.
45
+
46
+ ---
47
+
48
+ **you:** 1
49
+
50
+ **agent** *(refuse — explained honestly)*:
51
+ > That can't be answered: revenue is bound to universe 'transactions', not the pinned population
52
+ > 'store_days' — it is not defined over that population
53
+
54
+ Your choice is applied mechanically (the alternative's `apply` universe). Pinning `store_days` makes
55
+ the numerator out-of-domain, so there is no faithful rate over that population — the server refuses,
56
+ with the reason, and the agent invents nothing.
57
+
58
+ ---
59
+
60
+ **you:** then just show revenue and inventory side by side by store and day
61
+
62
+ **agent** *(disclose — served, with a material caveat)*:
63
+ > Answer (with caveats):
64
+ > rev: store="S001" day="2024-01-05"=218.18; … (rows elided)
65
+ > inv: store="S001" day="2024-01-03"=5622.0; … (rows elided)
66
+ > · disclosure [denominator_population, material]: frame spans multiple universes ['store_days',
67
+ > 'transactions'] — population is ambiguous; pin it with ON UNIVERSE
68
+
69
+ Reformulated as two separate columns over the union population, the numbers are served **with** a
70
+ material coverage caveat riding on the frame — surfaced in the reply, not dropped.
71
+
72
+ ---
73
+
74
+ **Four moods, one conversation.** The agent proposed queries and relayed a clarify; it never wrote a
75
+ result number or picked a population. The engine decided answerability; you decided between genuine
76
+ alternatives. One contract, one truth — now reachable in plain English.
@@ -0,0 +1,124 @@
1
+ # Columna MCP server — Claude Desktop config + the co-universe rate clarify → answer flow
2
+
3
+ The Columna MCP server exposes a library of Manifolds to any MCP client (Claude Desktop, a local
4
+ agent, or Columna's own NL agent) over **one contract**: the four moods — serve · disclose ·
5
+ clarify · refuse — arrive as *data*, identical to the Python API. This is the first metrics MCP
6
+ server that can say **"it depends."**
7
+
8
+ ## 1. Configure Claude Desktop
9
+
10
+ Add the server to `claude_desktop_config.json` (macOS:
11
+ `~/Library/Application Support/Claude/claude_desktop_config.json`). `stdio` is the canonical
12
+ transport — Claude Desktop launches the process and speaks MCP over its stdin/stdout:
13
+
14
+ ```jsonc
15
+ {
16
+ "mcpServers": {
17
+ "columna": {
18
+ "command": "columna-server",
19
+ "args": ["mcp", "--manifolds", "/abs/path/to/manifolds"]
20
+ }
21
+ }
22
+ }
23
+ ```
24
+
25
+ `--manifolds` points at a directory of `<id>/manifold.cml` + `data.toml` (connector type + data
26
+ path). All Manifolds are parsed and connected **once** at startup; `list_manifolds` / `describe_*`
27
+ never touch data, and `explain` executes nothing (`fetches_delta: 0`).
28
+
29
+ Five read-only tools appear: `list_manifolds`, `describe_manifold`, `describe_measure`, `query`,
30
+ `explain`. There is **no SQL** and **no write surface** of any kind.
31
+
32
+ > Remote transport: `columna-server mcp --manifolds <dir> --http :8000`. When `COLUMNA_MCP_TOKEN`
33
+ > is set, every request must carry `Authorization: Bearer <token>` (constant-time checked); when it
34
+ > is unset, HTTP binds loopback-only and warns.
35
+
36
+ ## 2. Three moods in one flow: clarify → refuse → disclose
37
+
38
+ The wedge: ask for a rate whose two measures live over *different populations* — `revenue` (over
39
+ `transactions`) per `level.last` (a stock, over `store_days`). A SQL/metrics layer would silently
40
+ emit a number. Columna **clarifies**, and the clarify is machine-actionable.
41
+
42
+ ### Query — "sell-through rate: revenue per unit of inventory, by store and day"
43
+
44
+ ```
45
+ query(manifold_id="benchmark", frameql="sell_through_rate: revenue / level.last @ store, day")
46
+ ```
47
+
48
+ → `outcome: clarify` — the rate's population is ambiguous, and it names the candidate populations as
49
+ **mechanically substitutable** alternatives (`apply.universe`):
50
+
51
+ ```json
52
+ {
53
+ "contract_version": "1",
54
+ "outcome": "clarify",
55
+ "columns": [{
56
+ "name": "sell_through_rate", "status": "clarify",
57
+ "no_result": {
58
+ "kind": "clarify", "discriminator": "ambiguous", "reason": "co_anchor_ambiguous",
59
+ "detail": "ratio combines measures over different populations ['store_days', 'transactions'] … which population should the rate be taken over?",
60
+ "alternatives": [
61
+ {"token": "on_universe('store_days')", "description": "express both numerator and denominator within universe 'store_days'", "apply": {"universe": "store_days"}},
62
+ {"token": "on_universe('transactions')", "description": "express both numerator and denominator within universe 'transactions'", "apply": {"universe": "transactions"}}
63
+ ]
64
+ }
65
+ }]
66
+ }
67
+ ```
68
+
69
+ ### Hop 1 — apply a pin token; the server is honest, not eager (→ refuse)
70
+
71
+ ```
72
+ query(manifold_id="benchmark", frameql="sell_through_rate: revenue / level.last @ store, day", universe="store_days")
73
+ ```
74
+
75
+ Pinning to `store_days` makes the numerator out-of-domain, so there is no faithful rate over that
76
+ population — the server **refuses** with an informative reason (it does **not** invent a number):
77
+
78
+ ```json
79
+ {
80
+ "outcome": "refuse",
81
+ "columns": [{
82
+ "name": "sell_through_rate", "status": "refuse",
83
+ "no_result": {
84
+ "kind": "refuse", "discriminator": "unsupported", "reason": "out_of_universe",
85
+ "detail": "revenue is bound to universe 'transactions', not the pinned population 'store_days' — it is not defined over that population"
86
+ }
87
+ }]
88
+ }
89
+ ```
90
+
91
+ ### Hop 2 — reformulate per the clarify's detail; serve the numbers, disclosed (→ disclose)
92
+
93
+ Following the clarify's guidance, the agent asks for the two measures as **separate columns** over
94
+ the union population rather than as one rate. The server serves the numbers **and** rides the
95
+ population caveat on the frame. Because that caveat is `material`, the wire outcome is **disclose**
96
+ (materiality — not severity — is the serve/disclose discriminant):
97
+
98
+ ```
99
+ query(manifold_id="benchmark", frameql="revenue: revenue, inv: level.last @ store, day")
100
+ ```
101
+
102
+ ```json
103
+ {
104
+ "outcome": "disclose",
105
+ "frame": {
106
+ "rollup_severity": "info",
107
+ "disclosures": [
108
+ {"code": "denominator_population", "materiality": "material", "severity": "info",
109
+ "category": "coverage",
110
+ "detail": "frame spans multiple universes ['store_days', 'transactions'] — population is ambiguous; pin it with ON UNIVERSE"}
111
+ ]
112
+ },
113
+ "columns": [
114
+ {"name": "revenue", "status": "served", "population": "transactions",
115
+ "values": [{"store": "S001", "day": "2024-01-03", "value": 58.26}, "…"]},
116
+ {"name": "inv", "status": "served", "population": "store_days",
117
+ "values": [{"store": "S001", "day": "2024-01-01", "value": 5693.0}, "…"]}
118
+ ]
119
+ }
120
+ ```
121
+
122
+ **The point:** three of the four moods in one conversation — a *structured* clarify, an honest
123
+ pin-**refuse**, and a served-and-**disclosed** answer — driven entirely by machine-readable fields.
124
+ The ambiguity was never guessed away. One contract, every surface.
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "columna-server"
7
+ version = "0.1.0"
8
+ description = "Columna MCP server — a library of Manifolds exposed to AI agents over MCP, one contract (the four moods as data)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "Apache-2.0"
12
+ authors = [{ name = "Datumwise" }]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "License :: OSI Approved :: Apache Software License",
16
+ "Programming Language :: Python :: 3",
17
+ ]
18
+ dependencies = [
19
+ "columna-core>=0.7.8",
20
+ "mcp>=1.0",
21
+ "tomli>=2.0; python_version < '3.11'",
22
+ # store.py imports duckdb directly to load the connector's warehouse — declare it explicitly
23
+ # rather than rely on columna-core pulling it transitively (the WP-0 pyarrow lesson).
24
+ "duckdb>=1.0,<2.0",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ http = ["uvicorn>=0.23", "starlette>=0.37"]
29
+ agent = ["anthropic>=0.40"]
30
+ test = ["pytest>=8.0", "pytest-asyncio>=0.23"]
31
+
32
+ [project.scripts]
33
+ columna-server = "columna_server.cli:main"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ # hatchling includes ALL files under the package dir (not just .py), so the packaged demo data
37
+ # (src/columna_server/demo/**: manifold.cml + data.toml + the small bundled warehouse) ships in the
38
+ # wheel automatically — no force-include needed. `columna-server demo` therefore runs with no path
39
+ # args from a clean-venv wheel install (proven by the py3.10 wheel job in CI).
40
+ packages = ["src/columna_server"]
41
+
42
+ [tool.hatch.build.targets.sdist]
43
+ include = ["src", "tests", "demos", "README.md", "CHANGELOG.md"]
44
+
45
+ [tool.pytest.ini_options]
46
+ testpaths = ["tests"]
47
+ asyncio_mode = "auto"
48
+ markers = [
49
+ "llm: live test that calls a real LLM provider; needs ANTHROPIC_API_KEY, excluded from CI.",
50
+ ]
51
+
52
+ [tool.ruff]
53
+ line-length = 110
54
+
55
+ [tool.ruff.lint]
56
+ select = ["F", "E9"]
@@ -0,0 +1,2 @@
1
+ """Columna MCP server — a library of Manifolds exposed to AI agents over MCP (ADR-032 D8)."""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
@@ -0,0 +1,10 @@
1
+ """columna_server.agent — the natural-language query agent (a true MCP client; ADR-032 D8)."""
2
+ from .conversation import AGENT, ENGINE, USER, Turn
3
+ from .loop import Agent, load_system_prompt, prompt_example_queries, render_manifold_context
4
+ from .providers import (AnthropicProvider, Provider, ProviderUnavailable, ScriptedProvider,
5
+ make_provider)
6
+
7
+ __all__ = ["Turn", "USER", "AGENT", "ENGINE", "Agent", "load_system_prompt",
8
+ "prompt_example_queries", "render_manifold_context",
9
+ "Provider", "ProviderUnavailable", "ScriptedProvider", "AnthropicProvider",
10
+ "make_provider"]
@@ -0,0 +1,14 @@
1
+ """columna_server.agent.conversation — the shared turn model."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+
6
+ USER = "user" # a human message
7
+ AGENT = "agent" # the agent's proposed line (QUERY:/ASK:)
8
+ ENGINE = "engine" # a compact wire summary the system appends after each query
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class Turn:
13
+ role: str # USER | AGENT | ENGINE
14
+ text: str