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.
- columna_server-0.1.0/.gitignore +23 -0
- columna_server-0.1.0/CHANGELOG.md +25 -0
- columna_server-0.1.0/PKG-INFO +83 -0
- columna_server-0.1.0/README.md +59 -0
- columna_server-0.1.0/demos/agent_nonexistent_measure_transcript.md +31 -0
- columna_server-0.1.0/demos/agent_transcript.md +76 -0
- columna_server-0.1.0/demos/mcp_claude_desktop.md +124 -0
- columna_server-0.1.0/pyproject.toml +56 -0
- columna_server-0.1.0/src/columna_server/__init__.py +2 -0
- columna_server-0.1.0/src/columna_server/__main__.py +6 -0
- columna_server-0.1.0/src/columna_server/agent/__init__.py +10 -0
- columna_server-0.1.0/src/columna_server/agent/conversation.py +14 -0
- columna_server-0.1.0/src/columna_server/agent/loop.py +220 -0
- columna_server-0.1.0/src/columna_server/agent/mcp_client.py +80 -0
- columna_server-0.1.0/src/columna_server/agent/providers.py +115 -0
- columna_server-0.1.0/src/columna_server/agent/runner.py +39 -0
- columna_server-0.1.0/src/columna_server/agent/system_prompt.md +66 -0
- columna_server-0.1.0/src/columna_server/cli.py +136 -0
- columna_server-0.1.0/src/columna_server/demo/README.md +7 -0
- columna_server-0.1.0/src/columna_server/demo/benchmark/data.toml +10 -0
- columna_server-0.1.0/src/columna_server/demo/benchmark/manifold.cml +61 -0
- columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/calendar.parquet +0 -0
- columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/categories.parquet +0 -0
- columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/customers.parquet +0 -0
- columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/daily_revenue_summary.parquet +0 -0
- columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/engagement_scores.parquet +0 -0
- columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/eom_inventory.parquet +0 -0
- columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/monthly_avg_order_value.parquet +0 -0
- columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/monthly_store_inventory.parquet +0 -0
- columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/monthly_unique_visitors.parquet +0 -0
- columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/product_categories.parquet +0 -0
- columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/products.parquet +0 -0
- columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/stores.parquet +0 -0
- columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/support_tickets.parquet +0 -0
- columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/transactions.parquet +0 -0
- columna_server-0.1.0/src/columna_server/demo/benchmark/warehouse/warehouse_notes.parquet +0 -0
- columna_server-0.1.0/src/columna_server/demo.py +84 -0
- columna_server-0.1.0/src/columna_server/frameql.py +106 -0
- columna_server-0.1.0/src/columna_server/server.py +53 -0
- columna_server-0.1.0/src/columna_server/store.py +124 -0
- columna_server-0.1.0/src/columna_server/tools.py +143 -0
- columna_server-0.1.0/tests/conftest.py +53 -0
- columna_server-0.1.0/tests/fixtures/manifolds/benchmark/data.toml +10 -0
- columna_server-0.1.0/tests/fixtures/manifolds/benchmark/manifold.cml +61 -0
- columna_server-0.1.0/tests/test_agent.py +211 -0
- columna_server-0.1.0/tests/test_demo.py +39 -0
- columna_server-0.1.0/tests/test_demo_data_drift.py +50 -0
- columna_server-0.1.0/tests/test_frameql_and_auth.py +57 -0
- 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,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
|