leona-mcp 0.3.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.
@@ -0,0 +1,10 @@
1
+ # This package is published (owner ruling ai-ops#432), and hatchling copies the NEAREST
2
+ # .gitignore into the source distribution and builds with its patterns alone. Without
3
+ # this file that was the repository root's, commentary and all, inside a public sdist.
4
+ # Keep the patterns a build must never ship; leona-client's tests/test_client_distribution.py
5
+ # checks the sdist.
6
+ __pycache__/
7
+ *.py[cod]
8
+ *.egg-info/
9
+ build/
10
+ dist/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eshaan Mistry
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,508 @@
1
+ Metadata-Version: 2.5
2
+ Name: leona-mcp
3
+ Version: 0.3.0
4
+ Summary: An MCP server over the public Quantum Atlas at leonaqt.com, and, with a token, verified runs
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.12
8
+ Requires-Dist: anyio<5,>=4.15.1
9
+ Requires-Dist: leona-client>=0.3.0
10
+ Requires-Dist: mcp<2,>=1.27
11
+ Requires-Dist: pydantic<3,>=2.11
12
+ Description-Content-Type: text/markdown
13
+
14
+ # leona-mcp
15
+
16
+ <!-- mcp-name: io.github.Leona-Quantum/leona-mcp -->
17
+
18
+ An MCP server that lets an AI assistant look things up in the
19
+ [Quantum Atlas](https://leonaqt.com/repository), Leona Quantum's public catalog of
20
+ quantum algorithms, gates, states and benchmark circuits — and, with a personal
21
+ access token, start a verified run, read its result, plan an algorithm and cost it from
22
+ its sources, estimate physical resources, call a published Qapp, have a circuit
23
+ checked, and list, read, edit and re-run your own Leona notebooks, all as the token's
24
+ own account.
25
+
26
+ Three tools need nothing at all: they read one public API endpoint, the same
27
+ anonymous catalog listing the website uses. You need no account or key, and they
28
+ cannot spend money. Twelve more need a personal access token (minted on leonaqt.com,
29
+ Account → Access tokens) in the `LEONA_API_TOKEN` environment variable — never as a
30
+ tool argument, and it is never logged or echoed in an error. Without one, those twelve
31
+ tools answer with a message telling you to set it; the three read-only tools are
32
+ unaffected. The server runs on your machine and talks to your MCP client over stdio.
33
+
34
+ ## Tools
35
+
36
+ | Tool | Needs a token | What it does |
37
+ |---|---|---|
38
+ | `search_methods` | No | Filters the Atlas by free text, problem area, a qubit limit, a depth limit, and NISQ or fault-tolerant, using the same rules as the site's method finder. Results are unranked candidates, sorted by slug. |
39
+ | `get_method` | No | One record in full: description, explanation, the cost it states, its speedup class and whether that was checked against the primary paper, its verification tier, the literature it cites, and the OpenQASM 3 of its circuit when it has one. |
40
+ | `list_problem_areas` | No | The problem areas the Atlas uses, with a definition and a record count for each. |
41
+ | `run_verified` | Yes (`run` scope) | Starts a Nala run — the same route the website's Run box calls — as your account, and polls for up to `wait_s` seconds for a verified result. |
42
+ | `get_run` | Yes | Reads one of your own runs by id, including its verification record. |
43
+ | `list_my_runs` | Yes | Lists your own runs, most recent first. |
44
+ | `plan_workflow` | Yes (any scope) | Plans an algorithm the way [leonaqt.com/repository/plan](https://leonaqt.com/repository/plan) does and returns the same numbers: the pipeline of Atlas blocks for a problem (factoring, search, ground-state energy, linear systems, MaxCut, amplitude and phase estimation, Hamiltonian simulation, elliptic-curve keys, differential equations), each stage's alternatives, and the cost lines evaluated from published formulas at your sizes. Every line carries its kind (exact, a bound, leading order, a paper's numerical estimate, a scaling with no constant, ...) and its source, and `summary` quotes each number with both. `estimate_point` is ready to pass to `estimate_resources`. Arithmetic only: nothing is run or stored. |
45
+ | `estimate_resources` | Yes | Turns a logical cost you state into physical qubits and runtime under a named assumption set. |
46
+ | `run_qapp` | Yes (`run` scope) | Calls a published Qapp with input values, through the same route and the same sandboxed execution as the Qapp's own page, and polls for its result. |
47
+ | `check_circuit` | Yes (`run` scope) | Checks a small OpenQASM 3 circuit (the tool description gives the widest per kind, from the contract's own limits) against one property: an output state, a unitary, an ideal measured distribution, or an energy. Returns pass, fail or inconclusive with a diagnosis, a plain `passed` boolean, a one-paragraph `summary`, and its teeth: how many deliberately broken copies of the circuit the check caught. An optional `structure` says how the circuit must be built (gate budgets, gates it may, may not or must use, measurements only at the end), so a circuit that loads its answer with `prepare_state` instead of building it fails. To check a circuit family rather than one circuit, send `circuits` (the circuit built at each of up to 8 sizes) with a reference written with `n`, such as `ghz(n)`: each size is judged with its own teeth, and the check passes only if every size does. Judged by Leona's own code, one circuit at a time (a busy server answers 503; call again in a few seconds); nothing is stored. |
48
+ | `list_notebooks` | Yes | Lists your own notebooks (id, title, current version, latest status, updated), newest first, paginated with `cursor`. |
49
+ | `pull_notebook` | Yes | Reads one notebook version's `.nb.py` SOURCE — jupytext's percent-format authoring text, not compiled `.ipynb` JSON — the form a model edits far more reliably. Omit `version` for the current one. |
50
+ | `push_notebook` | Yes (`run` scope) | Saves edited `.nb.py` source as a new version of an existing notebook — the same route the web editor and `leona-notebooks push --to` use. Runs it in Leona's sandbox by default and waits for a per-cell report; `execute=false` saves a draft with no run. |
51
+ | `notebook_status` | Yes | Reads a notebook's latest version: status, a plain `ok`, and each cell's outcome (including, on error, the exception and a traceback tail). Poll this after `push_notebook`/`run_notebook` if `wait_s` ran out. |
52
+ | `run_notebook` | Yes (`run` scope) | Re-runs a notebook's current version with no edits, and waits for a per-cell report. To run edited source, use `push_notebook` instead. |
53
+
54
+ Everything the first three tools return comes from the published Atlas record, with
55
+ a link back to its page on leonaqt.com. When a record does not carry a field, the
56
+ answer says "not stated in the record" instead of guessing. A record is a claim from
57
+ the sources it cites, not a result this server checked.
58
+
59
+ **A run's `status` can be `"succeeded"` while its verification did not pass.** Read
60
+ `verifier_decision` and `verification_summary` — every run tool's response also
61
+ carries a plain `verified` boolean, true only when `verifier_decision` is `"pass"` —
62
+ before telling anyone a result is verified.
63
+
64
+ **A `check_circuit` pass means "checked against" what `checked_against` names, and
65
+ nothing more.** Quote it that way, never as "verified": the reference can itself be the
66
+ wrong target. Its teeth say whether the check could have failed at all; a check that
67
+ caught none of the broken copies could not tell a broken circuit from yours.
68
+
69
+ **A notebook version's `status` can be `"ready"` while `ok` is `false`.** `ok` comes
70
+ from the stored run report, not from `status`: a version is `ready` the moment
71
+ execution finishes, whether or not a cell raised. Read `ok` and `cells` — never just
72
+ `status` — before telling anyone a notebook ran clean.
73
+
74
+ **Editing a notebook never needs `.ipynb`.** `pull_notebook` returns the `.nb.py`
75
+ percent-format source — a plain-text file of a YAML header, markdown cells and code
76
+ cells under `# %%` markers — and `push_notebook` takes that same text back. Neither
77
+ tool touches compiled `.ipynb` JSON, which is why they exist beside `leona-notebooks
78
+ pull`/`push` rather than duplicating them: a model edits Python far more reliably than
79
+ JSON with escaped newlines inside string literals.
80
+
81
+ ## Example: "write a 3-qubit QFT and check it with Leona"
82
+
83
+ A Claude Code user with the server connected and a `run`-scope token set
84
+ (`claude mcp add --transport stdio --env LEONA_API_TOKEN=lq_pat_... leona-atlas -- uvx
85
+ --from "leona-mcp==0.3.0"
86
+ leona-mcp`) asks their agent to write a 3-qubit QFT and check it with Leona. Bitstrings
87
+ and Pauli strings use Qiskit's order, q0 rightmost, and `qft(3)` is Qiskit's QFT
88
+ including its final swaps. The requests and answers below come from a real run:
89
+ `services/api/tests/test_check_circuit_route.py` produces them through the MCP tool,
90
+ `leona_client`, the API route and the check engine, and fails if this README drifts from
91
+ what they answer.
92
+
93
+ <!-- check-circuit-example:start (generated by test_check_circuit_route.py) -->
94
+
95
+ The agent's first attempt leaves out the final swap. It calls:
96
+
97
+ ```json
98
+ {
99
+ "tool": "check_circuit",
100
+ "arguments": {
101
+ "qasm": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[3] q;\nh q[2];\ncp(pi/2) q[1], q[2];\ncp(pi/4) q[0], q[2];\nh q[1];\ncp(pi/2) q[0], q[1];\nh q[0];\n",
102
+ "kind": "unitary",
103
+ "reference": "qft(3)"
104
+ }
105
+ }
106
+ ```
107
+
108
+ Leona answers:
109
+
110
+ ```json
111
+ {
112
+ "status": "fail",
113
+ "basis": "circuit",
114
+ "checked_against": "Qiskit's QFT on 3 qubits, exact unitary up to global phase",
115
+ "measure": "largest entry of the difference, after removing global phase, 7.07e-01 (needs ≤ 1.00e-06)",
116
+ "detail": "It matches up to a reversal of the qubit order on one side. A QFT written without its final swaps does exactly this.",
117
+ "qubits": 3,
118
+ "subject_fingerprint": "c5fcd4e70621fe09aca5a0421af4d7be5f9e7f4ef3da09d3ae784bcbc4d66880",
119
+ "subject_qasm": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[3] q;\nh q[2];\ncp(pi/2) q[1], q[2];\ncp(pi/4) q[0], q[2];\nh q[1];\ncp(pi/2) q[0], q[1];\nh q[0];\n",
120
+ "teeth": {
121
+ "status": "not_measured",
122
+ "reason": "Broken copies are tried only on a check that passes. This one failed, so there was nothing to test.",
123
+ "mutants": 0,
124
+ "equivalent": 0,
125
+ "caught": 0,
126
+ "survivors": [],
127
+ "could_not_run": 0,
128
+ "shortcut": null
129
+ },
130
+ "per_size": null,
131
+ "counts": null,
132
+ "structure": null,
133
+ "behaviour_status": null,
134
+ "behaviour_detail": "",
135
+ "passed": false,
136
+ "summary": "Checked against Qiskit's QFT on 3 qubits, exact unitary up to global phase: fail (largest entry of the difference, after removing global phase, 7.07e-01 (needs ≤ 1.00e-06)). It matches up to a reversal of the qubit order on one side. A QFT written without its final swaps does exactly this. Whether this check can catch a broken circuit was not measured: Broken copies are tried only on a check that passes. This one failed, so there was nothing to test.",
137
+ "teeth_note": "Whether this check can catch a broken circuit was not measured: Broken copies are tried only on a check that passes. This one failed, so there was nothing to test."
138
+ }
139
+ ```
140
+
141
+ The agent adds `swap q[0], q[2];` at the end and calls again:
142
+
143
+ ```json
144
+ {
145
+ "tool": "check_circuit",
146
+ "arguments": {
147
+ "qasm": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[3] q;\nh q[2];\ncp(pi/2) q[1], q[2];\ncp(pi/4) q[0], q[2];\nh q[1];\ncp(pi/2) q[0], q[1];\nh q[0];\nswap q[0], q[2];\n",
148
+ "kind": "unitary",
149
+ "reference": "qft(3)"
150
+ }
151
+ }
152
+ ```
153
+
154
+ Leona answers:
155
+
156
+ ```json
157
+ {
158
+ "status": "pass",
159
+ "basis": "circuit",
160
+ "checked_against": "Qiskit's QFT on 3 qubits, exact unitary up to global phase",
161
+ "measure": "largest entry of the difference, after removing global phase, 1.28e-15 (needs ≤ 1.00e-06)",
162
+ "detail": "",
163
+ "qubits": 3,
164
+ "subject_fingerprint": "077aef414f965daa2894271ba1cdea0a7c6090f70779a8f91a3c94ef0ab53509",
165
+ "subject_qasm": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[3] q;\nh q[2];\ncp(pi/2) q[1], q[2];\ncp(pi/4) q[0], q[2];\nh q[1];\ncp(pi/2) q[0], q[1];\nh q[0];\nswap q[0], q[2];\n",
166
+ "teeth": {
167
+ "status": "measured",
168
+ "reason": "",
169
+ "mutants": 11,
170
+ "equivalent": 0,
171
+ "caught": 11,
172
+ "survivors": [],
173
+ "could_not_run": 0,
174
+ "shortcut": null
175
+ },
176
+ "per_size": null,
177
+ "counts": null,
178
+ "structure": null,
179
+ "behaviour_status": null,
180
+ "behaviour_detail": "",
181
+ "passed": true,
182
+ "summary": "Checked against Qiskit's QFT on 3 qubits, exact unitary up to global phase: pass (largest entry of the difference, after removing global phase, 1.28e-15 (needs ≤ 1.00e-06)). Leona also made 11 deliberately broken copies of the circuit that change its output, and the check caught all 11."
183
+ }
184
+ ```
185
+
186
+ <!-- check-circuit-example:end -->
187
+
188
+ ## Example: "how big a machine does RSA-2048 need, by Leona's sources?"
189
+
190
+ The same user asks their agent what factoring a 2048-bit RSA modulus costs. Any token
191
+ will do here; planning and estimating are arithmetic. The requests and answers below come
192
+ from a real run: `services/api/tests/test_plan_route.py` makes both calls through the MCP
193
+ tools, `leona_client`, the API routes, the planner port and the estimator, and fails if
194
+ this README drifts from what they answer. The planner's numbers are the ones
195
+ `/repository/plan` shows for the same input: the port is held equal to the TypeScript
196
+ planner on about 900 inputs (`packages/py/planner/tests/test_planner_parity.py`).
197
+
198
+ <!-- plan-workflow-example:start (generated by test_plan_route.py) -->
199
+
200
+ The agent calls:
201
+
202
+ ```json
203
+ {
204
+ "tool": "plan_workflow",
205
+ "arguments": {
206
+ "problem": "factoring",
207
+ "params": {
208
+ "bits": 2048
209
+ }
210
+ }
211
+ }
212
+ ```
213
+
214
+ Leona answers (the summary, the cost lines, and the point to cost; the full answer also carries every stage, its alternatives and the methods' stated costs, the sources with their quotes, and the published whole-machine figures):
215
+
216
+ ```json
217
+ {
218
+ "summary": "Leona's workflow planner, for Factor an integer (RSA) at bits = 2,048 (given): Recover the period of a periodic function by Period finding in a finite cyclic group (the planner's default). Costs: Logical qubits (Gidney–Ekerå 2019): 6,190 logical qubits [leading-order; Gidney & Ekerå 2019 (arxiv:1905.09749), abstract]; Toffoli gates (Gidney–Ekerå 2019): 2.62e9 Toffoli gates [leading-order; Gidney & Ekerå 2019 (arxiv:1905.09749), abstract]; Measurement depth (Gidney–Ekerå 2019): 2.14e9 measurement layers [leading-order; Gidney & Ekerå 2019 (arxiv:1905.09749), abstract]; Logical qubits (Gidney 2025): 1,399 logical qubits [published; Gidney 2025 (arxiv:2505.15917), Table 5]; Toffoli gates per factoring (Gidney 2025): 6.5e9 Toffoli gates [published; Gidney 2025 (arxiv:2505.15917), Table 5]. Published at this size: Noisy qubits, 8 hours (Gidney–Ekerå 2019): 2e7 physical qubits [published; Gidney & Ekerå 2019 (arxiv:1905.09749), title and abstract]; Noisy qubits, under a week (Gidney 2025): < 1,000,000 physical qubits [published; Gidney 2025 (arxiv:2505.15917), abstract]. Figures here are rounded for reading; the lines carry exact values. For physical qubits and runtime, pass estimate_point to estimate_resources.",
219
+ "lines": [
220
+ {
221
+ "id": "ge2021-qubits",
222
+ "label": "Logical qubits (Gidney–Ekerå 2019)",
223
+ "value": 6190,
224
+ "unit": "logical qubits",
225
+ "kind": "leading-order",
226
+ "source": "ge2021-logical",
227
+ "formula": "3n + 0.002·n·lg n"
228
+ },
229
+ {
230
+ "id": "ge2021-toffolis",
231
+ "label": "Toffoli gates (Gidney–Ekerå 2019)",
232
+ "value": 2624225017.856,
233
+ "unit": "Toffoli gates",
234
+ "kind": "leading-order",
235
+ "source": "ge2021-logical",
236
+ "formula": "0.3n³ + 0.0005·n³·lg n"
237
+ },
238
+ {
239
+ "id": "ge2021-depth",
240
+ "label": "Measurement depth (Gidney–Ekerå 2019)",
241
+ "value": 2143289344,
242
+ "unit": "measurement layers",
243
+ "kind": "leading-order",
244
+ "source": "ge2021-logical",
245
+ "formula": "500n² + n²·lg n"
246
+ },
247
+ {
248
+ "id": "g2025-qubits",
249
+ "label": "Logical qubits (Gidney 2025)",
250
+ "value": 1399,
251
+ "unit": "logical qubits",
252
+ "kind": "published",
253
+ "source": "gidney2025-table5",
254
+ "formula": "Table 5, n = 2048"
255
+ },
256
+ {
257
+ "id": "g2025-toffolis",
258
+ "label": "Toffoli gates per factoring (Gidney 2025)",
259
+ "value": 6500000000,
260
+ "unit": "Toffoli gates",
261
+ "kind": "published",
262
+ "source": "gidney2025-table5",
263
+ "formula": "Table 5, n = 2048"
264
+ }
265
+ ],
266
+ "logical": {
267
+ "logical_qubits": "ge2021-qubits",
268
+ "toffolis": "ge2021-toffolis",
269
+ "t_gates": null,
270
+ "queries": null,
271
+ "serial_depth": "ge2021-depth"
272
+ },
273
+ "estimate_point": {
274
+ "label": "factoring (Leona planner)",
275
+ "parameter_value": 2048,
276
+ "logical_qubits": 6190,
277
+ "toffoli_count": 2624225018,
278
+ "t_count": 0,
279
+ "non_clifford_depth": 2143289344
280
+ }
281
+ }
282
+ ```
283
+
284
+ The agent passes `estimate_point` on:
285
+
286
+ ```json
287
+ {
288
+ "tool": "estimate_resources",
289
+ "arguments": {
290
+ "points": [
291
+ {
292
+ "label": "factoring (Leona planner)",
293
+ "parameter_value": 2048,
294
+ "logical_qubits": 6190,
295
+ "toffoli_count": 2624225018,
296
+ "t_count": 0,
297
+ "non_clifford_depth": 2143289344
298
+ }
299
+ ]
300
+ }
301
+ }
302
+ ```
303
+
304
+ Leona answers (the costed point; the full answer adds the frontier and each assumption set's citation):
305
+
306
+ ```json
307
+ {
308
+ "assumptions": "gidney-2025@v2",
309
+ "points": [
310
+ {
311
+ "label": "factoring (Leona planner)",
312
+ "refused": null,
313
+ "distance": {
314
+ "code_distance": 31,
315
+ "logical_operations": 132690604193744,
316
+ "required_error_per_operation": 7.536328635144969e-17,
317
+ "achieved_error_per_operation": 1.0000000000000008e-17,
318
+ "physical_per_logical": 2048
319
+ },
320
+ "fastest": {
321
+ "footprint": {
322
+ "data_patch_qubits": 12677120,
323
+ "routing_qubits": 12677120,
324
+ "factory_qubits": 417792,
325
+ "total_physical_qubits": 25772032
326
+ },
327
+ "runtime": {
328
+ "magic_states": 20993800144,
329
+ "factory_count": 17,
330
+ "throughput_seconds": 21410.58882332941,
331
+ "reaction_limited_seconds": 21432.893440000003,
332
+ "seconds": 21432.893440000003,
333
+ "binding_term": "reaction",
334
+ "factory_crossover": 17
335
+ }
336
+ },
337
+ "smallest": {
338
+ "footprint": {
339
+ "data_patch_qubits": 12677120,
340
+ "routing_qubits": 12677120,
341
+ "factory_qubits": 24576,
342
+ "total_physical_qubits": 25378816
343
+ },
344
+ "runtime": {
345
+ "magic_states": 20993800144,
346
+ "factory_count": 1,
347
+ "throughput_seconds": 363980.0099965999,
348
+ "reaction_limited_seconds": 21432.893440000003,
349
+ "seconds": 363980.0099965999,
350
+ "binding_term": "throughput",
351
+ "factory_crossover": 17
352
+ }
353
+ }
354
+ }
355
+ ]
356
+ }
357
+ ```
358
+
359
+ <!-- plan-workflow-example:end -->
360
+
361
+ No tool here submits a hardware job. Submitting to hardware needs a token with the
362
+ separate `hardware` scope (ai-ops 376), which the `run` scope does not include, and even
363
+ with that scope this server has no tool that submits.
364
+
365
+ ## Example: "pull my coin-flip notebook, fix the bug, and run it"
366
+
367
+ Connect the server with a `run`-scope token (below), then just ask, in your own words:
368
+ a Claude Code user with the server connected asks their agent to fix a bug in one of
369
+ their notebooks. The agent finds it (`list_notebooks`), reads its source
370
+ (`pull_notebook`, `version: null` — the current version), edits the `.nb.py` text
371
+ directly, and saves the fix as a new version, run and reported in one call
372
+ (`push_notebook`):
373
+
374
+ ```json
375
+ {"tool": "list_notebooks", "arguments": {"limit": 5}}
376
+ ```
377
+
378
+ ```json
379
+ {"tool": "pull_notebook", "arguments": {"notebook_id": "01996f3a-...-b21c"}}
380
+ ```
381
+
382
+ ```json
383
+ {
384
+ "tool": "push_notebook",
385
+ "arguments": {
386
+ "notebook_id": "01996f3a-...-b21c",
387
+ "source": "# ---\n# title: Coin flip\n# kind: lesson\n# ---\n\n# %% role=run\nfrom qiskit import QuantumCircuit\nqc = QuantumCircuit(1, 1)\nqc.h(0)\nqc.measure(0, 0)\n",
388
+ "message": "measure the qubit before reading it out"
389
+ }
390
+ }
391
+ ```
392
+
393
+ `push_notebook` waits for the run and answers with the per-cell report directly — no
394
+ separate poll needed unless `wait_s` runs out, in which case `notebook_status` reads
395
+ the same shape once it finishes.
396
+
397
+ ## Install
398
+
399
+ Requires Python 3.12 or later and [uv](https://docs.astral.sh/uv/). `uvx` fetches
400
+ the supported 0.3.0 server and its public dependencies from PyPI; Git access is not
401
+ required.
402
+
403
+ ```bash
404
+ uvx --from "leona-mcp==0.3.0" leona-mcp --version
405
+ ```
406
+
407
+ For a regular Python environment, use `python -m pip install "leona-mcp==0.3.0"`,
408
+ then run `leona-mcp`. The server depends on the public `leona-client` package;
409
+ Leona's check engine, contracts, sandbox and planner stay on the service.
410
+
411
+ ## Connect a client
412
+
413
+ The stdio examples below launch the server directly. Atlas tools need no token;
414
+ acting tools need `LEONA_API_TOKEN` with the scope noted above.
415
+
416
+ The examples name the server `leona-atlas`. Any name works.
417
+
418
+ ### Claude Code
419
+
420
+ ```bash
421
+ claude mcp add --transport stdio leona-atlas -- \
422
+ uvx --from "leona-mcp==0.3.0" leona-mcp
423
+ ```
424
+
425
+ Options go before the name and everything after `--` is the command. Add
426
+ `--scope user` to make it available in every project, or `--scope project` to write it
427
+ to the project's `.mcp.json` for everyone who clones it. The default scope is `local`.
428
+
429
+ ### Cursor
430
+
431
+ Add this to `~/.cursor/mcp.json`, or to `.cursor/mcp.json` in a project:
432
+
433
+ ```json
434
+ {
435
+ "mcpServers": {
436
+ "leona-atlas": {
437
+ "type": "stdio",
438
+ "command": "uvx",
439
+ "args": [
440
+ "--from",
441
+ "leona-mcp==0.3.0",
442
+ "leona-mcp"
443
+ ]
444
+ }
445
+ }
446
+ }
447
+ ```
448
+
449
+ ### Claude Desktop
450
+
451
+ Open the config file (on macOS
452
+ `~/Library/Application Support/Claude/claude_desktop_config.json`, on Windows
453
+ `%APPDATA%\Claude\claude_desktop_config.json`), add the server, and restart the app:
454
+
455
+ ```json
456
+ {
457
+ "mcpServers": {
458
+ "leona-atlas": {
459
+ "command": "uvx",
460
+ "args": [
461
+ "--from",
462
+ "leona-mcp==0.3.0",
463
+ "leona-mcp"
464
+ ]
465
+ }
466
+ }
467
+ }
468
+ ```
469
+
470
+ Desktop apps often start without your shell's `PATH`. If the server does not start,
471
+ replace `"uvx"` with the full path that `which uvx` prints (for example
472
+ `/Users/you/.local/bin/uvx`).
473
+
474
+ ## Settings
475
+
476
+ | Variable | Default | Meaning |
477
+ |---|---|---|
478
+ | `LEONA_API_URL` | `https://majorana-api-nikekeixtq-uw.a.run.app` | The Leona API to read. Point it at `http://localhost:8000` to read a local API. |
479
+ | `LEONA_API_TOKEN` | unset | A personal access token, for `run_verified`/`get_run`/`list_my_runs`/`plan_workflow`/`estimate_resources`/`run_qapp`/`check_circuit`/`list_notebooks`/`pull_notebook`/`push_notebook`/`notebook_status`/`run_notebook`. Leave unset to use only the three read-only Atlas tools. |
480
+
481
+ In Claude Code, pass either with `--env NAME=value` before the server name. In the
482
+ JSON configs, add `"env": {"LEONA_API_URL": "...", "LEONA_API_TOKEN": "..."}` beside
483
+ `args`. Personal access tokens are a Leona feature still being rolled out
484
+ (proposal 7 Phase B); if `run_verified` answers that tokens are not available yet,
485
+ that is the account-wide switch, not this server.
486
+
487
+ The server reads the whole Atlas once (a few megabytes, in pages of 100) and reuses it
488
+ for ten minutes. HTTP calls time out after 30 seconds. If the listing it gets back is
489
+ incomplete or changes while it is being read, the tool call fails with a message
490
+ saying so, rather than answering from part of the Atlas.
491
+
492
+ ## Develop
493
+
494
+ From the repository root:
495
+
496
+ ```bash
497
+ uv sync --all-packages
498
+ uv run pytest packages/py/mcp -q
499
+ uv run leona-mcp # speaks MCP on stdin and stdout; Ctrl-C to stop
500
+ ```
501
+
502
+ The Atlas filtering rules and the HTTP client live in `leona_client`
503
+ (`packages/py/client`), shared with the `%nala` Jupyter magic and the
504
+ `leona-notebooks` CLI — this package now holds only the MCP protocol layer. The
505
+ filtering rules are themselves copies of TypeScript in `apps/web/lib/repository/`,
506
+ which is the source of truth; `packages/py/client/tests/test_mirrors.py` fails when a
507
+ copied vocabulary stops matching it. See `AGENTS.md` in this directory, and in
508
+ `packages/py/client`, before changing a rule.