rapier-runtime 0.5.2__tar.gz → 0.6.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 (101) hide show
  1. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/CHANGELOG.md +39 -0
  2. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/PKG-INFO +2 -2
  3. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/docs/STATUS.md +45 -3
  4. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/docs/mcp-server-scope.md +11 -1
  5. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/pyproject.toml +4 -2
  6. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/site/README.md +1 -1
  7. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/site/index.html +1 -0
  8. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/mcp/server.py +99 -20
  9. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/mcp/tools.py +51 -0
  10. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_mcp.py +39 -6
  11. rapier_runtime-0.6.0/tests/test_mcp_resources.py +98 -0
  12. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/.claude/scripts/loom-deploy-nonprod.sh +0 -0
  13. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/.claude/scripts/loom-deploy-prod.sh +0 -0
  14. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/.claude/scripts/loom-env-check.sh +0 -0
  15. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/.claude/scripts/loom-state-docs.sh +0 -0
  16. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/.githooks/pre-commit +0 -0
  17. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/.github/workflows/secret-scan.yml +0 -0
  18. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/.gitignore +0 -0
  19. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/.gitleaks.toml +0 -0
  20. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/.loom/audits/smuggled-virtue.done +0 -0
  21. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/.loom/project.json +0 -0
  22. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/LICENSE +0 -0
  23. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/README.md +0 -0
  24. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/SECURITY.md +0 -0
  25. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/docs/chatlogs/.gitkeep +0 -0
  26. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/docs/chatlogs/202607301413.checkpoint.handoff-cut-0-5-0-to-pypi.md +0 -0
  27. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/docs/chatlogs/20260802-explainability-reminder.md +0 -0
  28. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/docs/threat-model.md +0 -0
  29. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/manifests/echo.yaml +0 -0
  30. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/manifests/sparring.frame.yaml +0 -0
  31. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/manifests/sparring.full.yaml +0 -0
  32. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/manifests/sparring.proposer.yaml +0 -0
  33. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/manifests/sparring.spar.yaml +0 -0
  34. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/site/coming-soon.html +0 -0
  35. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/site/rapier-logo.png +0 -0
  36. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/__init__.py +0 -0
  37. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/_json.py +0 -0
  38. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/cli.py +0 -0
  39. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/convergence.py +0 -0
  40. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/envelope.py +0 -0
  41. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/ledger.py +0 -0
  42. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/manifest.py +0 -0
  43. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/mcp/__init__.py +0 -0
  44. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/models.py +0 -0
  45. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/onboarding.py +0 -0
  46. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/pipeline.py +0 -0
  47. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/presets.py +0 -0
  48. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/secrets.py +0 -0
  49. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stage.py +0 -0
  50. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/__init__.py +0 -0
  51. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/echo.py +0 -0
  52. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/frame.py +0 -0
  53. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/proposer/__init__.py +0 -0
  54. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/proposer/phase_stages.py +0 -0
  55. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/proposer/phases.py +0 -0
  56. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/proposer/traceability_gate.py +0 -0
  57. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/reconcile.py +0 -0
  58. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/resolver/__init__.py +0 -0
  59. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/resolver/_binding.py +0 -0
  60. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/resolver/_extract.py +0 -0
  61. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/resolver/anchored_fix.py +0 -0
  62. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/resolver/author.py +0 -0
  63. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/resolver/citation_gate.py +0 -0
  64. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/resolver/compose.py +0 -0
  65. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/resolver/cross_review.py +0 -0
  66. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/resolver/definitiveness_gate.py +0 -0
  67. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/__init__.py +0 -0
  68. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/_bootstrap.py +0 -0
  69. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/_llm_shim.py +0 -0
  70. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/_vendor/PROVENANCE.md +0 -0
  71. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/_vendor/cite_check.py +0 -0
  72. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/_vendor/lib_llm.py +0 -0
  73. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/_vendor/spar_cross_review.py +0 -0
  74. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/_vendor/spar_definitiveness_gate.py +0 -0
  75. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/_vendor/spar_verify_gate.py +0 -0
  76. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/_vendor/verify_grounding.py +0 -0
  77. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/rider_record.py +0 -0
  78. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/service.py +0 -0
  79. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_cite_check_evidence_union.py +0 -0
  80. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_cli.py +0 -0
  81. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_convergence.py +0 -0
  82. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_envelope.py +0 -0
  83. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_extract_json.py +0 -0
  84. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_frame_stage.py +0 -0
  85. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_full_ceremony.py +0 -0
  86. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_manifest.py +0 -0
  87. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_models_vendors.py +0 -0
  88. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_onboarding.py +0 -0
  89. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_persistence_default.py +0 -0
  90. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_pipeline.py +0 -0
  91. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_policy.py +0 -0
  92. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_proposer_g2.py +0 -0
  93. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_proposer_stages.py +0 -0
  94. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_proposer_traceability_gate.py +0 -0
  95. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_reconcile.py +0 -0
  96. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_resolver_stages.py +0 -0
  97. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_rider_record.py +0 -0
  98. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_secrets.py +0 -0
  99. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_single_vendor_report.py +0 -0
  100. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_vendor_policy_v4.py +0 -0
  101. {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_verify_service.py +0 -0
@@ -4,6 +4,45 @@ Notable changes to `rapier-runtime`. Earlier releases are summarized in
4
4
  [`docs/STATUS.md`](docs/STATUS.md) and on the
5
5
  [Releases](https://github.com/muddyone/rapier-runtime/releases) page.
6
6
 
7
+ ## 0.6.0 — 2026-10-16
8
+
9
+ ### Changed: the MCP server now runs on the mcp 2.x SDK
10
+
11
+ The `[mcp]` extra now requires `mcp>=2.3,<3` (it was `>=1.2,<2`), and the
12
+ server is built on the SDK's `MCPServer`. Tools, their results and the run
13
+ resources behave as before. Two visible differences:
14
+
15
+ - **Progress shows the stage.** Each stage's progress update now carries the
16
+ stage name as its message. mcp 2.x sends log messages only to clients that
17
+ ask for them, so the progress message is what every client will show.
18
+ - **`pip install -U "rapier-runtime[mcp]"` upgrades the SDK.** An environment
19
+ pinned to mcp 1.x will be moved to 2.x. If a 1.x SDK is installed anyway,
20
+ `rapier mcp` says so and gives the command that fixes it.
21
+
22
+ ## 0.5.3 — 2026-10-16
23
+
24
+ ### Added: saved runs as MCP resources
25
+
26
+ MCP clients can now browse saved runs instead of calling tools:
27
+
28
+ - `rapier://runs`: every saved run, newest first, with its kind, topic and
29
+ verdict (JSON).
30
+ - `rapier://runs/{run_id}`: a run's report, as the CLI printed it (plain text).
31
+ - `rapier://runs/{run_id}/record`: a run's full record, redacted when it was
32
+ saved (JSON).
33
+
34
+ Each saved run also appears in the client's resource list, titled by kind and
35
+ topic, including runs saved after the server started. The `list_runs` and
36
+ `get_run` tools still work.
37
+
38
+ ### Fixed: `rapier mcp` failed on a fresh install
39
+
40
+ The `[mcp]` extra allowed any `mcp>=1.2`, so a new install got the mcp 2.x SDK,
41
+ which renamed the server API this version uses. `rapier mcp` then exited with a
42
+ message telling you to install the extra you already had. The extra is now
43
+ capped at `mcp<2`, and if a 2.x SDK is present anyway, `rapier mcp` says so and
44
+ gives the command that fixes it.
45
+
7
46
  ## 0.5.2 — 2026-10-16
8
47
 
9
48
  ### Fixed: the report no longer calls a same-vendor review independent
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rapier-runtime
3
- Version: 0.5.2
3
+ Version: 0.6.0
4
4
  Summary: A code-orchestrated engine that runs the SPARRING method: grounded, cross-vendor adversarial review for AI-in-the-loop decisions.
5
5
  Project-URL: Homepage, https://rapierruntime.com
6
6
  Project-URL: Repository, https://github.com/muddyone/rapier-runtime
@@ -25,7 +25,7 @@ Requires-Dist: requests>=2.31
25
25
  Provides-Extra: dev
26
26
  Requires-Dist: pytest>=8.0; extra == 'dev'
27
27
  Provides-Extra: mcp
28
- Requires-Dist: mcp>=1.2; extra == 'mcp'
28
+ Requires-Dist: mcp<3,>=2.3; extra == 'mcp'
29
29
  Description-Content-Type: text/markdown
30
30
 
31
31
  # Rapier Runtime
@@ -1,9 +1,43 @@
1
1
  # Rapier — dev status
2
2
 
3
- _Last updated: 2026-10-16 (tag v0.5.1, 230 tests)._ A running "where we are /
3
+ _Last updated: 2026-10-16 (tag v0.5.3, 249 tests with the mcp SDK)._ A running "where we are /
4
4
  what's next" so a new session can continue without reconstructing from git log.
5
5
 
6
6
  ## Done
7
+ - **RELEASED (2026-10-16) — `rapier-runtime` 0.5.3 on PyPI. Saved runs as MCP
8
+ resources, and `rapier mcp` starts again on a fresh install.** Git tag
9
+ `v0.5.3`; https://pypi.org/project/rapier-runtime/0.5.3/. Resources:
10
+ `rapier://runs`, `rapier://runs/{run_id}`, `rapier://runs/{run_id}/record`;
11
+ each saved run is listed individually, including runs saved after the server
12
+ started. The fix: `[mcp]` allowed any `mcp>=1.2`, so new installs got mcp
13
+ 2.3.0, which renamed FastMCP, and 0.5.2's `rapier mcp` failed with a message
14
+ pointing at the wrong cause. Capped at `mcp<2`. Verified from PyPI in a clean
15
+ environment: the install resolved mcp 1.30.0 and the server listed its runs.
16
+ - **RELEASED (2026-10-16) — `rapier-runtime` 0.5.2 on PyPI. The report stops
17
+ calling a same-vendor review independent.** Git tag `v0.5.2`;
18
+ https://pypi.org/project/rapier-runtime/0.5.2/. A live 0.5.1 run with only an
19
+ OpenAI key (author and reviewer both `gpt-6-sol`) showed three defects. The
20
+ report said "an independent, different-vendor reviewer" raised the objections
21
+ while its own trust section said the review was not independent. The progress
22
+ warning blamed a missing OpenAI key that was set. LaTeX arithmetic printed raw.
23
+ All three are fixed (`compose.py`, `cross_review.py`; the vendored reviewer is
24
+ untouched) and re-checked on a second live run. 235 tests. The same day the
25
+ repo gained the Loom secret-scan gate: the GitHub gitleaks workflow, the
26
+ pre-commit hook and `.gitleaks.toml`.
27
+ The live runs confirmed `gpt-6-sol` works, including the framing step at its
28
+ 1,024-token limit. **OpenRouter confirmed end to end (2026-10-16)** with only
29
+ an OpenRouter key set: `frame` (1 call) and a full `spar` (8 calls) all went
30
+ to `openrouter` / `openai/gpt-6-sol` with no empty responses, and the 0.5.2
31
+ single-vendor wording held. Keys: 1Password, Sparring Framework vault, item
32
+ `OpenRouter` (field `rapier-runtime-test` for tests).
33
+ **Gemini default `gemini-3.5-flash-lite` confirmed live (2026-10-16):** one
34
+ `frame` call with only the stored Gemini key returned a complete answer.
35
+ **DeepSeek default `deepseek-flash` confirmed live (2026-10-16):** DeepSeek's
36
+ own model list for the key is `deepseek-flash` and `deepseek-v4-pro`, and one
37
+ `frame` call with only the DeepSeek key returned a complete answer. Key:
38
+ 1Password, Sparring Framework vault, item `DeepSeek`.
39
+ Still never called live: the Groq and Together `openai/gpt-oss-120b`
40
+ defaults (no keys).
7
41
  - **RELEASED (2026-10-16) — `rapier-runtime` 0.5.1 on PyPI. Default models for
8
42
  five vendors replaced.** Git tag `v0.5.1`;
9
43
  https://pypi.org/project/rapier-runtime/0.5.1/. OpenRouter's default
@@ -144,8 +178,16 @@ what's next" so a new session can continue without reconstructing from git log.
144
178
  2. **Paper 2 (the Proposer)** stays parked until the engine is fully shipped.
145
179
  Note: the Proposer study runs *on* this engine — Frame, seeded generation, and
146
180
  the depth knob are the instrument it will exercise.
147
- 3. **Optional polish, if worth it:** expose runs as MCP *resources* rather than
148
- tools; a landing line advertising the front door / `--depth` / `--seed`.
181
+ 3. ~~**Optional polish:** runs as MCP resources; a landing line for the front
182
+ door / `--depth` / `--seed`.~~ **DONE (2026-10-16).** The landing line is live.
183
+ Saved runs are MCP resources (`rapier://runs`, `rapier://runs/{run_id}`,
184
+ `rapier://runs/{run_id}/record`); the `list_runs` / `get_run` tools stay for
185
+ tool-only clients. Released in 0.5.3.
186
+ 4. ~~**Port the MCP server to the mcp 2.x SDK.**~~ **DONE (2026-10-16), not yet
187
+ released.** `MCPServer` replaces `FastMCP`; the `[mcp]` extra is now
188
+ `mcp>=2.3,<3`. 250 tests pass on mcp 2.3.0, and a built wheel was checked over
189
+ real stdio: tools, resources, a live `frame` call with progress, and the new
190
+ run appearing in the next listing. Ships with the next release.
149
191
 
150
192
  _(M4 publish is **DONE** — v0.4.0 on PyPI + the full landing live; see Done above.
151
193
  Loom submodule pin bumped to the v0.4.0 release commit `514df56`.)_
@@ -97,7 +97,17 @@ existing redaction on every tool result. No new secret surface — keys stay in
97
97
  **THE RECORD** section naming the run directory, so a Desktop user can see the
98
98
  audit trail exists and where — governance is only real if the record is visible.
99
99
 
100
- Follow-ups: consider exposing runs as MCP *resources* (currently tools).
100
+ Saved runs are also MCP **resources** (2026-10-16): `rapier://runs` (index,
101
+ JSON), `rapier://runs/{run_id}` (the report, plain text) and
102
+ `rapier://runs/{run_id}/record` (the full redacted envelope, JSON). Each saved
103
+ run is listed as its own resource every time a client asks, including runs saved
104
+ after the server started. The `list_runs` / `get_run` tools remain for clients
105
+ that only call tools.
106
+
107
+ The server is built on the mcp **2.x** SDK (`MCPServer`; ported 2026-10-16), and
108
+ the `[mcp]` extra requires `mcp>=2.3,<3`. Stage progress is sent as the
109
+ `notifications/progress` message, because 2.x delivers log messages only to
110
+ clients that opt in.
101
111
 
102
112
  ## Open decisions
103
113
  1. `rapier mcp` subcommand (recommended) vs a separate `rapier-mcp` entry point.
@@ -9,7 +9,7 @@ build-backend = "hatchling.build"
9
9
 
10
10
  [project]
11
11
  name = "rapier-runtime"
12
- version = "0.5.2"
12
+ version = "0.6.0"
13
13
  description = "A code-orchestrated engine that runs the SPARRING method: grounded, cross-vendor adversarial review for AI-in-the-loop decisions."
14
14
  readme = "README.md"
15
15
  requires-python = ">=3.10"
@@ -50,7 +50,9 @@ Paper = "https://doi.org/10.5281/zenodo.21210264"
50
50
  dev = ["pytest>=8.0"]
51
51
  # The MCP server (`rapier mcp`) — kept out of the core so the tiny runtime stays
52
52
  # `requests` + `pyyaml`. Install with: pip install "rapier-runtime[mcp]".
53
- mcp = ["mcp>=1.2"]
53
+ # Built on the mcp 2.x SDK (MCPServer). Capped below the next major, because 2.0
54
+ # renamed the server API and an uncapped >=1.2 broke `rapier mcp` (0.5.2).
55
+ mcp = ["mcp>=2.3,<3"]
54
56
 
55
57
  [project.scripts]
56
58
  rapier = "rapier.cli:main"
@@ -11,7 +11,7 @@ fonts. `index.html` is **light-only** and serves the logo master
11
11
  Kept as a rollback target; **not** what is deployed.
12
12
  - **`index.html`** — the full MVP landing page. **This is what is live** at
13
13
  rapierruntime.com (swapped in at launch, 2026-07-16; verified byte-identical
14
- 2026-08-28).
14
+ 2026-10-16, after adding the quickstart line for `frame`, `--depth` and `--seed`).
15
15
 
16
16
  ## Status: LAUNCHED 2026-07-16 — the full landing is live
17
17
 
@@ -301,6 +301,7 @@ pip install <b>rapier-runtime</b>
301
301
  rapier <span class="p">spar</span> --request <span class="s">"Should we drop Postgres for DynamoDB?"</span>
302
302
  </div>
303
303
  <p class="qs-note">Bring your own API keys — Rapier calls the AI vendors you configure, and nothing runs on our servers. Want the full four-phase ceremony instead of the resolver? <span class="gold">rapier sparring</span>.</p>
304
+ <p class="qs-note">Not sure if you have a decision or still a question? <span class="gold">rapier frame</span> tells you which, and which command fits: an open question needs options generated first (<span class="gold">sparring</span>); a well-argued decision can go straight to the challenge (<span class="gold">spar</span>). In <span class="gold">sparring</span>, <span class="gold">--depth shallow|standard|deep</span> sets how widely it explores options, and <span class="gold">--seed</span> adds one you already favor &mdash; it gets no head start and survives only on its merits.</p>
304
305
  </div>
305
306
  </section>
306
307
 
@@ -1,4 +1,6 @@
1
- """The ``rapier mcp`` stdio server — frame / proposer / spar / sparring / doctor as MCP tools.
1
+ """The ``rapier mcp`` stdio server — frame / proposer / spar / sparring / doctor as MCP tools,
2
+ and saved runs as MCP resources (``rapier://runs``, ``rapier://runs/{run_id}``,
3
+ ``rapier://runs/{run_id}/record``).
2
4
 
3
5
  Optional: requires the ``mcp`` extra (``pip install "rapier-runtime[mcp]"``). The
4
6
  SDK is imported lazily inside :func:`build_server`, so importing this module (and
@@ -13,15 +15,22 @@ minutes, so the tools **stream per-stage progress** to the client: the engine's
13
15
  run-log lines are bridged from the worker thread to MCP ``info`` + ``report_progress``
14
16
  notifications, so the client shows life instead of a hang.
15
17
 
18
+ Built on the mcp 2.x SDK (``MCPServer``; it was ``FastMCP`` in 1.x).
19
+
16
20
  NOTE: this module deliberately does *not* use ``from __future__ import
17
- annotations`` — FastMCP evaluates each tool's signature, and the lazily-imported
21
+ annotations`` — MCPServer evaluates each tool's signature, and the lazily-imported
18
22
  ``Context`` annotation must be a real class object at definition time, not a
19
23
  string it cannot resolve.
20
24
  """
25
+ import json
21
26
  import os
22
27
  import sys
23
28
  from typing import Any, Callable
24
29
 
30
+ # Cap on runs listed individually in resources/list; every run stays readable by URI
31
+ # and is indexed in rapier://runs.
32
+ _MAX_LISTED_RUNS = 200
33
+
25
34
 
26
35
  async def _run_with_progress(
27
36
  fn: Callable[..., dict],
@@ -32,8 +41,13 @@ async def _run_with_progress(
32
41
  **kwargs: Any,
33
42
  ) -> dict:
34
43
  """Run a blocking tool ``fn(log=…, cancel=…, ledger_root=…, **kwargs)`` in a
35
- worker thread while streaming its log lines to the MCP client as info +
36
- progress notifications, with cooperative cancellation and an optional timeout.
44
+ worker thread while streaming its log lines to the MCP client, with
45
+ cooperative cancellation and an optional timeout.
46
+
47
+ Every line goes out as an ``info`` log message, and each ``stage:`` line also
48
+ ticks progress with that line as the progress message. The mcp 2.x SDK
49
+ deprecates the logging helpers and delivers them only to clients that opt in,
50
+ so the progress message is what keeps every client showing the stage.
37
51
 
38
52
  ``ctx`` may be ``None`` (a unit test with no session), in which case the run
39
53
  proceeds silently. On client cancellation or timeout the cancel flag is set,
@@ -69,11 +83,14 @@ async def _run_with_progress(
69
83
  if msg is sentinel:
70
84
  return
71
85
  if ctx is not None:
72
- await ctx.info(msg)
86
+ try:
87
+ await ctx.info(msg)
88
+ except Exception:
89
+ pass # logging is best-effort; never fail a run over it
73
90
  if msg.startswith("stage:"):
74
91
  done += 1
75
92
  try:
76
- await ctx.report_progress(done, total)
93
+ await ctx.report_progress(done, total, message=msg)
77
94
  except Exception:
78
95
  pass # progress is best-effort; never fail a run over it
79
96
 
@@ -99,22 +116,26 @@ async def _run_with_progress(
99
116
 
100
117
 
101
118
  def build_server():
102
- """Construct the FastMCP server with Rapier's tools.
119
+ """Construct the MCP server with Rapier's tools and run resources.
103
120
 
104
121
  Raises ``ImportError`` if the ``mcp`` extra is not installed (handled by
105
122
  :func:`serve`).
106
123
  """
107
- from mcp.server.fastmcp import Context, FastMCP # optional dependency
124
+ import warnings
125
+
126
+ from mcp.server.mcpserver import Context, MCPServer # optional dependency
127
+ from mcp.shared.exceptions import MCPDeprecationWarning
128
+ from mcp.types import Resource
108
129
 
109
130
  from .. import __version__
110
131
  from ..presets import load_preset
111
132
  from . import tools
112
133
 
113
- server = FastMCP("rapier")
114
- # FastMCP doesn't forward a version to the low-level server, which then reports
115
- # the mcp SDK's own version in the initialize handshake. Set ours so a client
116
- # sees rapier's version, not the SDK's.
117
- server._mcp_server.version = __version__
134
+ # ctx.info is deprecated (SEP-2577) but still reaches clients that opt in to
135
+ # logs; keep sending it without the SDK warning on the server's stderr. The
136
+ # warning is raised inside the SDK, so match its text, not the calling module.
137
+ warnings.filterwarnings("ignore", message="The logging capability is deprecated",
138
+ category=MCPDeprecationWarning)
118
139
  # Run persistence (governance default: ON). Ceremonies are written to a
119
140
  # durable run dir and become retrievable via list_runs / get_run. Precedence:
120
141
  # RAPIER_NO_PERSIST opts out entirely; else RAPIER_MCP_LEDGER pins the location
@@ -126,6 +147,26 @@ def build_server():
126
147
  else:
127
148
  ledger_root = os.environ.get("RAPIER_MCP_LEDGER") or default_runs_root()
128
149
 
150
+ class _RapierServer(MCPServer):
151
+ async def list_resources(self):
152
+ # MCPServer lists only resources registered at startup, so runs saved
153
+ # after the server started would never appear. Add each saved run's
154
+ # report as a concrete resource every time a client asks.
155
+ listed = await super().list_resources()
156
+ index = tools.run_index(ledger_root)
157
+ for run in (index.get("runs") or [])[:_MAX_LISTED_RUNS]:
158
+ listed.append(Resource(
159
+ uri=run["report_uri"],
160
+ name=run["run_id"],
161
+ title=f"{run['kind']}: {run['topic']}" if run["topic"] else run["run_id"],
162
+ description=f"verdict: {run['verdict']}" if run["verdict"] else None,
163
+ mime_type="text/plain",
164
+ ))
165
+ return listed
166
+
167
+ # The version goes in the initialize handshake; without it 2.x reports "".
168
+ server = _RapierServer("rapier", version=__version__)
169
+
129
170
  def _stage_total(name: str, settle: int = 0, verify: str = "gate",
130
171
  seed: list | None = None, depth: str = "standard") -> int:
131
172
  try:
@@ -208,29 +249,67 @@ def build_server():
208
249
  values) and whether cross-vendor review is available."""
209
250
  return tools.doctor()
210
251
 
252
+ # Kept for clients that only call tools; the resources below are the browsable form.
211
253
  @server.tool()
212
254
  def list_runs() -> dict:
213
- """List persisted run ids (requires the server's RAPIER_MCP_LEDGER)."""
255
+ """List persisted run ids. Clients that support resources can browse rapier://runs instead."""
214
256
  return tools.list_runs(ledger_root)
215
257
 
216
258
  @server.tool()
217
259
  def get_run(run_id: str) -> dict:
218
- """Fetch a persisted run's report + verdict by id (requires RAPIER_MCP_LEDGER)."""
260
+ """Fetch a persisted run's report + verdict by id. Also readable as rapier://runs/{run_id}."""
219
261
  return tools.get_run(ledger_root, run_id)
220
262
 
263
+ # ── saved runs as resources ──────────────────────────────────────────────
264
+ @server.resource("rapier://runs", name="runs", mime_type="application/json",
265
+ description="Every saved run, newest first: kind, topic, verdict, and "
266
+ "the URIs of its report and full record.")
267
+ def runs_index() -> str:
268
+ return json.dumps(tools.run_index(ledger_root), indent=2)
269
+
270
+ @server.resource("rapier://runs/{run_id}", name="run-report", mime_type="text/plain",
271
+ description="A saved run's report, as the CLI printed it.")
272
+ def run_report(run_id: str) -> str:
273
+ return tools.run_report(ledger_root, run_id)
274
+
275
+ @server.resource("rapier://runs/{run_id}/record", name="run-record",
276
+ mime_type="application/json",
277
+ description="A saved run's full record (redacted on write): request, "
278
+ "recommendation, trust rider, verdict, trace, meta.")
279
+ def run_record(run_id: str) -> str:
280
+ return json.dumps(tools.run_record(ledger_root, run_id), indent=2, default=str)
281
+
221
282
  return server
222
283
 
223
284
 
285
+ def _installed_mcp_version() -> str | None:
286
+ try:
287
+ from importlib.metadata import PackageNotFoundError, version
288
+ return version("mcp")
289
+ except PackageNotFoundError:
290
+ return None
291
+
292
+
224
293
  def serve() -> int:
225
294
  """Run the stdio MCP server. Returns non-zero with a hint if the SDK is absent."""
226
295
  try:
227
296
  server = build_server()
228
297
  except ImportError:
229
- print(
230
- 'The MCP server needs the optional "mcp" extra:\n'
231
- ' pip install "rapier-runtime[mcp]"',
232
- file=sys.stderr,
233
- )
298
+ installed = _installed_mcp_version()
299
+ major = installed.split(".")[0] if installed else ""
300
+ if major.isdigit() and int(major) != 2:
301
+ print(
302
+ f"The MCP server needs the mcp SDK 2.x, but {installed} is installed\n"
303
+ "(its server API differs). Install a 2.x SDK:\n"
304
+ ' pip install "mcp>=2.3,<3"',
305
+ file=sys.stderr,
306
+ )
307
+ else:
308
+ print(
309
+ 'The MCP server needs the optional "mcp" extra:\n'
310
+ ' pip install "rapier-runtime[mcp]"',
311
+ file=sys.stderr,
312
+ )
234
313
  return 1
235
314
  server.run() # stdio transport by default
236
315
  return 0
@@ -162,6 +162,57 @@ def list_runs(ledger_root: str | None) -> dict[str, Any]:
162
162
  return {"ok": True, "runs": runs, "ledger_root": ledger_root}
163
163
 
164
164
 
165
+ def _load_envelope(ledger_root: str | None, run_id: str) -> dict[str, Any]:
166
+ """A run's persisted envelope (already redacted on write); ``LookupError`` if absent."""
167
+ if not ledger_root:
168
+ raise LookupError("run persistence is disabled (RAPIER_NO_PERSIST is set)")
169
+ if not _safe_run_id(run_id):
170
+ raise LookupError("invalid run id")
171
+ path = os.path.join(ledger_root, run_id, "envelope.json")
172
+ if not os.path.isfile(path):
173
+ raise LookupError(f"run '{run_id}' not found")
174
+ with open(path, encoding="utf-8") as fh:
175
+ return json.load(fh)
176
+
177
+
178
+ def run_index(ledger_root: str | None) -> dict[str, Any]:
179
+ """Every persisted run with enough to choose one: kind, topic, verdict (newest first).
180
+
181
+ Backs the ``rapier://runs`` resource. A run id is ``<YYYYmmddHHMMSS>-<kind>``.
182
+ """
183
+ listed = list_runs(ledger_root)
184
+ if not listed.get("ok"):
185
+ return listed
186
+ runs = []
187
+ for run_id in reversed(listed["runs"]):
188
+ try:
189
+ env = _load_envelope(ledger_root, run_id)
190
+ except (LookupError, ValueError, OSError):
191
+ continue # an in-progress or foreign dir has no envelope; it isn't a run yet
192
+ request = (env.get("request") or "").strip()
193
+ runs.append({
194
+ "run_id": run_id,
195
+ "kind": run_id.split("-", 1)[1] if "-" in run_id else "",
196
+ "topic": request.splitlines()[0][:120] if request else "",
197
+ "verdict": env.get("verdict"),
198
+ "report_uri": f"rapier://runs/{run_id}",
199
+ "record_uri": f"rapier://runs/{run_id}/record",
200
+ })
201
+ return {"ok": True, "ledger_root": ledger_root, "runs": runs}
202
+
203
+
204
+ def run_report(ledger_root: str | None, run_id: str) -> str:
205
+ """A run's plain-text report, as the CLI printed it. ``LookupError`` if absent."""
206
+ env = _load_envelope(ledger_root, run_id)
207
+ meta = env.get("meta") or {}
208
+ return meta.get("report_md") or meta.get("report") or env.get("recommendation") or ""
209
+
210
+
211
+ def run_record(ledger_root: str | None, run_id: str) -> dict[str, Any]:
212
+ """A run's full persisted record (the redacted envelope). ``LookupError`` if absent."""
213
+ return _load_envelope(ledger_root, run_id)
214
+
215
+
165
216
  def get_run(ledger_root: str | None, run_id: str) -> dict[str, Any]:
166
217
  """Return a persisted run's report + verdict by id (from its envelope.json)."""
167
218
  if not ledger_root:
@@ -1,6 +1,6 @@
1
1
  """MCP-0 — tool logic (no SDK needed) + graceful behaviour when the extra is absent.
2
2
 
3
- The FastMCP wiring itself is exercised only when the optional ``mcp`` extra is
3
+ The MCPServer wiring itself is exercised only when the optional ``mcp`` extra is
4
4
  installed; these tests lock in the parts that must hold regardless.
5
5
  """
6
6
  from __future__ import annotations
@@ -66,8 +66,8 @@ def test_run_with_progress_streams_stage_progress():
66
66
  async def info(self, m):
67
67
  self.infos.append(m)
68
68
 
69
- async def report_progress(self, done, total=None):
70
- self.progress.append((done, total))
69
+ async def report_progress(self, done, total=None, message=None):
70
+ self.progress.append((done, total, message))
71
71
 
72
72
  def _fn(log=None, **kw):
73
73
  log("stage: author (transform)")
@@ -78,7 +78,9 @@ def test_run_with_progress_streams_stage_progress():
78
78
  ctx = _Ctx()
79
79
  out = anyio.run(server._run_with_progress, _fn, ctx, 2)
80
80
  assert out == {"ok": True, "report_md": "R"}
81
- assert ctx.progress == [(1, 2), (2, 2)] # one tick per 'stage:' line
81
+ # one tick per 'stage:' line, carrying that line as the progress message
82
+ assert ctx.progress == [(1, 2, "stage: author (transform)"),
83
+ (2, 2, "stage: compose (transform)")]
82
84
  assert any("substitution" in m for m in ctx.infos) # every line streamed as info
83
85
 
84
86
 
@@ -111,9 +113,19 @@ def test_build_server_advertises_rapier_version_not_sdk():
111
113
  pytest.importorskip("mcp")
112
114
  from rapier import __version__
113
115
 
116
+ anyio = pytest.importorskip("anyio")
117
+ from mcp import Client
118
+
114
119
  srv = server.build_server()
115
- # The initialize handshake must report rapier's version, not the mcp SDK's.
116
- assert srv._mcp_server.version == __version__
120
+
121
+ async def go():
122
+ async with Client(srv) as client:
123
+ return client.server_info
124
+
125
+ # The initialize handshake must report rapier's name and version (2.x
126
+ # reports "" when a server passes none).
127
+ info = anyio.run(go)
128
+ assert (info.name, info.version) == ("rapier", __version__)
117
129
 
118
130
 
119
131
  # --- MCP-2: timeout + ledger-run access ---
@@ -169,3 +181,24 @@ def test_runs_disabled_without_ledger():
169
181
 
170
182
  assert tools.list_runs(None)["ok"] is False
171
183
  assert tools.get_run(None, "x")["ok"] is False
184
+
185
+
186
+ def test_serve_names_an_old_mcp_sdk_when_that_is_why_import_failed(monkeypatch, capsys):
187
+ def _raise():
188
+ raise ImportError("No module named 'mcp.server.mcpserver'")
189
+
190
+ monkeypatch.setattr(server, "build_server", _raise)
191
+ monkeypatch.setattr(server, "_installed_mcp_version", lambda: "1.30.0")
192
+ assert server.serve() == 1
193
+ err = capsys.readouterr().err
194
+ assert "1.30.0 is installed" in err and 'pip install "mcp>=2.3,<3"' in err
195
+
196
+
197
+ def test_serve_without_sdk_points_at_the_extra(monkeypatch, capsys):
198
+ def _raise():
199
+ raise ImportError("No module named 'mcp'")
200
+
201
+ monkeypatch.setattr(server, "build_server", _raise)
202
+ monkeypatch.setattr(server, "_installed_mcp_version", lambda: None)
203
+ assert server.serve() == 1
204
+ assert 'pip install "rapier-runtime[mcp]"' in capsys.readouterr().err
@@ -0,0 +1,98 @@
1
+ """Saved runs as MCP resources: rapier://runs, rapier://runs/{id}, rapier://runs/{id}/record.
2
+
3
+ The index/report/record logic is plain Python and always runs. The protocol test
4
+ needs the optional ``mcp`` extra and is skipped without it.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import os
10
+
11
+ import pytest
12
+
13
+ from rapier.mcp import tools
14
+
15
+
16
+ def _save_run(root, run_id, request="Should we ship?", verdict="PASS", report="REPORT TEXT"):
17
+ d = os.path.join(root, run_id)
18
+ os.makedirs(d)
19
+ with open(os.path.join(d, "envelope.json"), "w", encoding="utf-8") as fh:
20
+ json.dump({"request": request, "verdict": verdict,
21
+ "recommendation": "do it", "meta": {"report_md": report}}, fh)
22
+
23
+
24
+ @pytest.fixture
25
+ def ledger(tmp_path):
26
+ root = str(tmp_path / "runs")
27
+ os.makedirs(root)
28
+ _save_run(root, "20261016095019-frame", request="Is this a question?\nsecond line",
29
+ verdict=None, report="FRAME REPORT")
30
+ _save_run(root, "20261016095044-spar", report="SPAR REPORT")
31
+ os.makedirs(os.path.join(root, "20261016100000-spar")) # in progress: no envelope yet
32
+ return root
33
+
34
+
35
+ def test_index_lists_runs_newest_first_and_skips_unfinished(ledger):
36
+ out = tools.run_index(ledger)
37
+ assert out["ok"] is True
38
+ assert [r["run_id"] for r in out["runs"]] == ["20261016095044-spar", "20261016095019-frame"]
39
+ spar, frame = out["runs"]
40
+ assert spar["kind"] == "spar" and spar["verdict"] == "PASS"
41
+ assert frame["topic"] == "Is this a question?"
42
+ assert spar["report_uri"] == "rapier://runs/20261016095044-spar"
43
+ assert spar["record_uri"] == "rapier://runs/20261016095044-spar/record"
44
+
45
+
46
+ def test_index_when_persistence_is_off_or_nothing_saved(tmp_path):
47
+ assert tools.run_index(None)["ok"] is False
48
+ assert tools.run_index(str(tmp_path / "absent"))["runs"] == []
49
+
50
+
51
+ def test_report_and_record_read_the_saved_run(ledger):
52
+ assert tools.run_report(ledger, "20261016095044-spar") == "SPAR REPORT"
53
+ assert tools.run_record(ledger, "20261016095044-spar")["verdict"] == "PASS"
54
+
55
+
56
+ @pytest.mark.parametrize("bad", ["../etc", "a/b", "", "nope-spar"])
57
+ def test_report_rejects_traversal_and_unknown_ids(ledger, bad):
58
+ with pytest.raises(LookupError):
59
+ tools.run_report(ledger, bad)
60
+
61
+
62
+ def test_resources_over_the_protocol(ledger, monkeypatch):
63
+ pytest.importorskip("mcp.server.mcpserver")
64
+ anyio = pytest.importorskip("anyio")
65
+ from mcp import Client
66
+
67
+ from rapier.mcp.server import build_server
68
+
69
+ monkeypatch.delenv("RAPIER_NO_PERSIST", raising=False)
70
+ monkeypatch.setenv("RAPIER_MCP_LEDGER", ledger)
71
+ server = build_server()
72
+
73
+ async def go():
74
+ async with Client(server) as client:
75
+ listed = {str(r.uri): r for r in (await client.list_resources()).resources}
76
+ templates = {t.uri_template for t in
77
+ (await client.list_resource_templates()).resource_templates}
78
+
79
+ # A run saved after the server started still shows up in the next listing.
80
+ _save_run(ledger, "20261016110000-sparring", request="Later run")
81
+ relisted = {str(r.uri) for r in (await client.list_resources()).resources}
82
+
83
+ index = json.loads((await client.read_resource("rapier://runs")).contents[0].text)
84
+ report = (await client.read_resource("rapier://runs/20261016095044-spar")).contents[0]
85
+ record = (await client.read_resource(
86
+ "rapier://runs/20261016095044-spar/record")).contents[0]
87
+ return listed, templates, relisted, index, report, record
88
+
89
+ listed, templates, relisted, index, report, record = anyio.run(go)
90
+
91
+ assert "rapier://runs" in listed
92
+ assert listed["rapier://runs/20261016095044-spar"].mime_type == "text/plain"
93
+ assert "rapier://runs/20261016100000-spar" not in listed # unfinished run not listed
94
+ assert {"rapier://runs/{run_id}", "rapier://runs/{run_id}/record"} <= templates
95
+ assert "rapier://runs/20261016110000-sparring" in relisted
96
+ assert [r["run_id"] for r in index["runs"]][-1] == "20261016095019-frame"
97
+ assert report.text == "SPAR REPORT"
98
+ assert json.loads(record.text)["verdict"] == "PASS"
File without changes
File without changes