rocq-solid-mcp 1.0.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.
- rocq_solid_mcp-1.0.0/LICENSE +21 -0
- rocq_solid_mcp-1.0.0/PKG-INFO +184 -0
- rocq_solid_mcp-1.0.0/README.md +149 -0
- rocq_solid_mcp-1.0.0/pyproject.toml +59 -0
- rocq_solid_mcp-1.0.0/setup.cfg +4 -0
- rocq_solid_mcp-1.0.0/src/rocq_solid_mcp/__init__.py +6 -0
- rocq_solid_mcp-1.0.0/src/rocq_solid_mcp/cli.py +63 -0
- rocq_solid_mcp-1.0.0/src/rocq_solid_mcp/config.py +146 -0
- rocq_solid_mcp-1.0.0/src/rocq_solid_mcp/manager.py +1082 -0
- rocq_solid_mcp-1.0.0/src/rocq_solid_mcp/models.py +767 -0
- rocq_solid_mcp-1.0.0/src/rocq_solid_mcp/notifications.py +225 -0
- rocq_solid_mcp-1.0.0/src/rocq_solid_mcp/pipeline.py +91 -0
- rocq_solid_mcp-1.0.0/src/rocq_solid_mcp/pretty.py +167 -0
- rocq_solid_mcp-1.0.0/src/rocq_solid_mcp/rocq_client.py +980 -0
- rocq_solid_mcp-1.0.0/src/rocq_solid_mcp/server.py +156 -0
- rocq_solid_mcp-1.0.0/src/rocq_solid_mcp/session.py +84 -0
- rocq_solid_mcp-1.0.0/src/rocq_solid_mcp.egg-info/PKG-INFO +184 -0
- rocq_solid_mcp-1.0.0/src/rocq_solid_mcp.egg-info/SOURCES.txt +20 -0
- rocq_solid_mcp-1.0.0/src/rocq_solid_mcp.egg-info/dependency_links.txt +1 -0
- rocq_solid_mcp-1.0.0/src/rocq_solid_mcp.egg-info/entry_points.txt +3 -0
- rocq_solid_mcp-1.0.0/src/rocq_solid_mcp.egg-info/requires.txt +7 -0
- rocq_solid_mcp-1.0.0/src/rocq_solid_mcp.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Benjamin Peters and contributors
|
|
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,184 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rocq-solid-mcp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: RocqSolid: an MCP server for Rocq proof development, backed by rocq-lsp
|
|
5
|
+
Author: Benjamin Peters
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/bn-peters/rocq-solid-mcp
|
|
8
|
+
Project-URL: Repository, https://github.com/bn-peters/rocq-solid-mcp
|
|
9
|
+
Project-URL: Issues, https://github.com/bn-peters/rocq-solid-mcp/issues
|
|
10
|
+
Keywords: rocq,coq,mcp,proof assistant,lsp,agent
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: fastmcp<5,>=4.1
|
|
29
|
+
Requires-Dist: pydantic<3,>=2.13
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.25; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-xdist>=3.6; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# RocqSolid
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/rocq-solid-mcp/)
|
|
39
|
+
[](https://github.com/bn-peters/rocq-solid-mcp/actions/workflows/ci.yml)
|
|
40
|
+
|
|
41
|
+
An MCP server for Rocq proof development.
|
|
42
|
+
|
|
43
|
+
The MCP is deliberately small:
|
|
44
|
+
1. `check` generates diagnostics for a given file. It tries to return errors
|
|
45
|
+
from proofs throughout the file. Proof states are cached for efficiency.
|
|
46
|
+
2. `run_at` runs a command at a given position in a file. This is useful for
|
|
47
|
+
queries like `Search` or `Check`, and for one-off debugging commands.
|
|
48
|
+
|
|
49
|
+
## Requirements
|
|
50
|
+
|
|
51
|
+
*RocqSolid appears to be more stable on OCaml 5 than on OCaml 4. Consider
|
|
52
|
+
upgrading if you're experiencing crashes.*
|
|
53
|
+
|
|
54
|
+
RocqSolid depends on a [patched version](https://github.com/bn-peters/coq-lsp)
|
|
55
|
+
of [`coq-lsp`](https://github.com/rocq-community/coq-lsp). It improves the
|
|
56
|
+
stability and error reporting of `coq-lsp`, and RocqSolid does not work
|
|
57
|
+
without. It is compatible with Rocq 9.0, 9.1, and 9.2. It needs to be installed
|
|
58
|
+
in all opam switches RocqSolid should work with:
|
|
59
|
+
```bash
|
|
60
|
+
opam repo add rocq-solid https://github.com/bn-peters/rocq-solid-mcp-opam.git
|
|
61
|
+
opam install rocq-solid-mcp-lsp
|
|
62
|
+
```
|
|
63
|
+
You might be asked to uninstall `coq-lsp`. Don't worry, `rocq-solid-mcp-lsp` is compatible with standard `coq-lsp`.
|
|
64
|
+
|
|
65
|
+
## Agent Setup
|
|
66
|
+
|
|
67
|
+
We recommend using [`uv`](https://docs.astral.sh/uv/), a Python packaging tool.
|
|
68
|
+
To teach your favorite agent about RocqSolid:
|
|
69
|
+
```bash
|
|
70
|
+
claude mcp add --scope user rocq-solid -- uvx rocq-solid-mcp
|
|
71
|
+
codex mcp add rocq-solid -- uvx rocq-solid-mcp
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Project Setup
|
|
75
|
+
|
|
76
|
+
The MCP looks for `_CoqProject` and `_RocqProject` files to set up file
|
|
77
|
+
dependencies correctly. To generate them in a `dune`-based project, add
|
|
78
|
+
`(generate_project_file)` to your `rocq.theory` stanzas:
|
|
79
|
+
```
|
|
80
|
+
; in theories/dune
|
|
81
|
+
(rocq.theory
|
|
82
|
+
(name my-project)
|
|
83
|
+
(generate_project_file)
|
|
84
|
+
(theories Stdlib))
|
|
85
|
+
```
|
|
86
|
+
You need to link the right opam switch to your development so that RocqSolid
|
|
87
|
+
knows which one to use:
|
|
88
|
+
```
|
|
89
|
+
opam switch link [switch] .
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Timeouts
|
|
93
|
+
|
|
94
|
+
If you have files that compile very slowly, RocqSolid may time out. You can
|
|
95
|
+
raise this limit with `ROCQ_LSP_TIMEOUT`.
|
|
96
|
+
|
|
97
|
+
For Claude, re-register RocqSolid with a higher limit:
|
|
98
|
+
```bash
|
|
99
|
+
claude mcp remove --scope user rocq-solid
|
|
100
|
+
claude mcp add --scope user rocq-solid -e ROCQ_LSP_TIMEOUT=290 -- uvx rocq-solid-mcp
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
For Codex, add the last two options to the existing entry in
|
|
104
|
+
`~/.codex/config.toml`:
|
|
105
|
+
```toml
|
|
106
|
+
[mcp_servers.rocq-solid]
|
|
107
|
+
command = "uvx"
|
|
108
|
+
args = ["rocq-solid-mcp"]
|
|
109
|
+
# add these two options:
|
|
110
|
+
tool_timeout_sec = 300
|
|
111
|
+
env = { ROCQ_LSP_TIMEOUT = "290" } # slightly lower than tool_timeout_sec
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Upgrading
|
|
115
|
+
|
|
116
|
+
To upgrade RocqSolid:
|
|
117
|
+
```
|
|
118
|
+
uvx rocq-solid-mcp@latest --help
|
|
119
|
+
```
|
|
120
|
+
To upgrade `coq-lsp`, run in your switch:
|
|
121
|
+
```
|
|
122
|
+
opam update && opam upgrade rocq-solid-mcp-lsp
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Troubleshooting
|
|
126
|
+
|
|
127
|
+
RocqSolid needs Python 3.11 or newer. If your default `python3` is older, `uvx`
|
|
128
|
+
reports that no solution was found. Use `uvx --python 3.12 rocq-solid-mcp`
|
|
129
|
+
instead.
|
|
130
|
+
|
|
131
|
+
You can check a single file from the command line, which prints the diagnostics
|
|
132
|
+
as JSON:
|
|
133
|
+
```bash
|
|
134
|
+
uvx rocq-solid-mcp check path/to/file.v
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Environment Variables
|
|
138
|
+
|
|
139
|
+
All optional; defaults in parentheses.
|
|
140
|
+
|
|
141
|
+
- `ROCQ_LSP_BINARY` (`coq-lsp`): the `coq-lsp` executable
|
|
142
|
+
- `ROCQ_LSP_USE_OPAM` (`1`): run via `opam exec --`; `0` runs the binary
|
|
143
|
+
directly
|
|
144
|
+
- `ROCQ_LSP_WORKSPACE` (unset): override the inferred workspace root
|
|
145
|
+
- `ROCQ_LSP_TIMEOUT` (`55`, max `3600`): deadline per call; set slightly below
|
|
146
|
+
the MCP tool timeout
|
|
147
|
+
- `ROCQ_LSP_HARD_TIMEOUT_GRACE` (`5`): extra wait after cancellation before the
|
|
148
|
+
session is killed
|
|
149
|
+
- `ROCQ_LSP_COMMAND_TIMEOUT` (`5`): default per-sentence timeout; `0` disables
|
|
150
|
+
- `ROCQ_LSP_TIMEOUT_AFTER_ERROR` (`10`): overall deadline for the rest of a
|
|
151
|
+
check after its first error; `0` disables
|
|
152
|
+
- `ROCQ_LSP_MESSAGE_LIMIT` (`10`, max `50`): max errors returned per check
|
|
153
|
+
- `ROCQ_LSP_MAX_ERRORS` (`1000000`): max errors `coq-lsp` collects
|
|
154
|
+
- `ROCQ_LSP_IDLE_TTL_SECONDS` (`1800`): idle time before cached sessions are
|
|
155
|
+
stopped
|
|
156
|
+
- `ROCQ_LSP_DUNE_BUILD` (`1`): build dune dependencies before checks; `0`
|
|
157
|
+
disables
|
|
158
|
+
- `ROCQ_LSP_DUNE_TIMEOUT` (`55`, max `3600`): limit per dune detection or build
|
|
159
|
+
command
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
## Support
|
|
163
|
+
|
|
164
|
+
Join us on the Iris Mattermost!
|
|
165
|
+
1. Go to the [GitLab log-in page](https://gitlab.mpi-sws.org/users/sign_in) and select the "GitHub" button, then log in with your GitHub account.
|
|
166
|
+
2. Log in to Mattermost; this should use your GitLab login.
|
|
167
|
+
3. Join the [RocqSolid](https://mattermost.mpi-sws.org/iris/channels/bens-rocq-mcp) channel.
|
|
168
|
+
|
|
169
|
+
If you notice that your agent misbehaves with this MCP, please let us know! It
|
|
170
|
+
would be particularly helpful if you could share your transcripts.
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
In a clone of this repository:
|
|
175
|
+
```bash
|
|
176
|
+
uv venv .venv && uv pip install -e ".[dev]"
|
|
177
|
+
make test
|
|
178
|
+
```
|
|
179
|
+
`uv tool install -e .` installs a development version of `rocq-solid-mcp` to
|
|
180
|
+
`~/.local/bin/`.
|
|
181
|
+
|
|
182
|
+
To release a new version, bump `version` in `pyproject.toml` and push a tag
|
|
183
|
+
`vX.Y.Z`. The `release` GitHub workflow builds the package and publishes it to
|
|
184
|
+
PyPI.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# RocqSolid
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/rocq-solid-mcp/)
|
|
4
|
+
[](https://github.com/bn-peters/rocq-solid-mcp/actions/workflows/ci.yml)
|
|
5
|
+
|
|
6
|
+
An MCP server for Rocq proof development.
|
|
7
|
+
|
|
8
|
+
The MCP is deliberately small:
|
|
9
|
+
1. `check` generates diagnostics for a given file. It tries to return errors
|
|
10
|
+
from proofs throughout the file. Proof states are cached for efficiency.
|
|
11
|
+
2. `run_at` runs a command at a given position in a file. This is useful for
|
|
12
|
+
queries like `Search` or `Check`, and for one-off debugging commands.
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
*RocqSolid appears to be more stable on OCaml 5 than on OCaml 4. Consider
|
|
17
|
+
upgrading if you're experiencing crashes.*
|
|
18
|
+
|
|
19
|
+
RocqSolid depends on a [patched version](https://github.com/bn-peters/coq-lsp)
|
|
20
|
+
of [`coq-lsp`](https://github.com/rocq-community/coq-lsp). It improves the
|
|
21
|
+
stability and error reporting of `coq-lsp`, and RocqSolid does not work
|
|
22
|
+
without. It is compatible with Rocq 9.0, 9.1, and 9.2. It needs to be installed
|
|
23
|
+
in all opam switches RocqSolid should work with:
|
|
24
|
+
```bash
|
|
25
|
+
opam repo add rocq-solid https://github.com/bn-peters/rocq-solid-mcp-opam.git
|
|
26
|
+
opam install rocq-solid-mcp-lsp
|
|
27
|
+
```
|
|
28
|
+
You might be asked to uninstall `coq-lsp`. Don't worry, `rocq-solid-mcp-lsp` is compatible with standard `coq-lsp`.
|
|
29
|
+
|
|
30
|
+
## Agent Setup
|
|
31
|
+
|
|
32
|
+
We recommend using [`uv`](https://docs.astral.sh/uv/), a Python packaging tool.
|
|
33
|
+
To teach your favorite agent about RocqSolid:
|
|
34
|
+
```bash
|
|
35
|
+
claude mcp add --scope user rocq-solid -- uvx rocq-solid-mcp
|
|
36
|
+
codex mcp add rocq-solid -- uvx rocq-solid-mcp
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Project Setup
|
|
40
|
+
|
|
41
|
+
The MCP looks for `_CoqProject` and `_RocqProject` files to set up file
|
|
42
|
+
dependencies correctly. To generate them in a `dune`-based project, add
|
|
43
|
+
`(generate_project_file)` to your `rocq.theory` stanzas:
|
|
44
|
+
```
|
|
45
|
+
; in theories/dune
|
|
46
|
+
(rocq.theory
|
|
47
|
+
(name my-project)
|
|
48
|
+
(generate_project_file)
|
|
49
|
+
(theories Stdlib))
|
|
50
|
+
```
|
|
51
|
+
You need to link the right opam switch to your development so that RocqSolid
|
|
52
|
+
knows which one to use:
|
|
53
|
+
```
|
|
54
|
+
opam switch link [switch] .
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Timeouts
|
|
58
|
+
|
|
59
|
+
If you have files that compile very slowly, RocqSolid may time out. You can
|
|
60
|
+
raise this limit with `ROCQ_LSP_TIMEOUT`.
|
|
61
|
+
|
|
62
|
+
For Claude, re-register RocqSolid with a higher limit:
|
|
63
|
+
```bash
|
|
64
|
+
claude mcp remove --scope user rocq-solid
|
|
65
|
+
claude mcp add --scope user rocq-solid -e ROCQ_LSP_TIMEOUT=290 -- uvx rocq-solid-mcp
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
For Codex, add the last two options to the existing entry in
|
|
69
|
+
`~/.codex/config.toml`:
|
|
70
|
+
```toml
|
|
71
|
+
[mcp_servers.rocq-solid]
|
|
72
|
+
command = "uvx"
|
|
73
|
+
args = ["rocq-solid-mcp"]
|
|
74
|
+
# add these two options:
|
|
75
|
+
tool_timeout_sec = 300
|
|
76
|
+
env = { ROCQ_LSP_TIMEOUT = "290" } # slightly lower than tool_timeout_sec
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Upgrading
|
|
80
|
+
|
|
81
|
+
To upgrade RocqSolid:
|
|
82
|
+
```
|
|
83
|
+
uvx rocq-solid-mcp@latest --help
|
|
84
|
+
```
|
|
85
|
+
To upgrade `coq-lsp`, run in your switch:
|
|
86
|
+
```
|
|
87
|
+
opam update && opam upgrade rocq-solid-mcp-lsp
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Troubleshooting
|
|
91
|
+
|
|
92
|
+
RocqSolid needs Python 3.11 or newer. If your default `python3` is older, `uvx`
|
|
93
|
+
reports that no solution was found. Use `uvx --python 3.12 rocq-solid-mcp`
|
|
94
|
+
instead.
|
|
95
|
+
|
|
96
|
+
You can check a single file from the command line, which prints the diagnostics
|
|
97
|
+
as JSON:
|
|
98
|
+
```bash
|
|
99
|
+
uvx rocq-solid-mcp check path/to/file.v
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Environment Variables
|
|
103
|
+
|
|
104
|
+
All optional; defaults in parentheses.
|
|
105
|
+
|
|
106
|
+
- `ROCQ_LSP_BINARY` (`coq-lsp`): the `coq-lsp` executable
|
|
107
|
+
- `ROCQ_LSP_USE_OPAM` (`1`): run via `opam exec --`; `0` runs the binary
|
|
108
|
+
directly
|
|
109
|
+
- `ROCQ_LSP_WORKSPACE` (unset): override the inferred workspace root
|
|
110
|
+
- `ROCQ_LSP_TIMEOUT` (`55`, max `3600`): deadline per call; set slightly below
|
|
111
|
+
the MCP tool timeout
|
|
112
|
+
- `ROCQ_LSP_HARD_TIMEOUT_GRACE` (`5`): extra wait after cancellation before the
|
|
113
|
+
session is killed
|
|
114
|
+
- `ROCQ_LSP_COMMAND_TIMEOUT` (`5`): default per-sentence timeout; `0` disables
|
|
115
|
+
- `ROCQ_LSP_TIMEOUT_AFTER_ERROR` (`10`): overall deadline for the rest of a
|
|
116
|
+
check after its first error; `0` disables
|
|
117
|
+
- `ROCQ_LSP_MESSAGE_LIMIT` (`10`, max `50`): max errors returned per check
|
|
118
|
+
- `ROCQ_LSP_MAX_ERRORS` (`1000000`): max errors `coq-lsp` collects
|
|
119
|
+
- `ROCQ_LSP_IDLE_TTL_SECONDS` (`1800`): idle time before cached sessions are
|
|
120
|
+
stopped
|
|
121
|
+
- `ROCQ_LSP_DUNE_BUILD` (`1`): build dune dependencies before checks; `0`
|
|
122
|
+
disables
|
|
123
|
+
- `ROCQ_LSP_DUNE_TIMEOUT` (`55`, max `3600`): limit per dune detection or build
|
|
124
|
+
command
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
## Support
|
|
128
|
+
|
|
129
|
+
Join us on the Iris Mattermost!
|
|
130
|
+
1. Go to the [GitLab log-in page](https://gitlab.mpi-sws.org/users/sign_in) and select the "GitHub" button, then log in with your GitHub account.
|
|
131
|
+
2. Log in to Mattermost; this should use your GitLab login.
|
|
132
|
+
3. Join the [RocqSolid](https://mattermost.mpi-sws.org/iris/channels/bens-rocq-mcp) channel.
|
|
133
|
+
|
|
134
|
+
If you notice that your agent misbehaves with this MCP, please let us know! It
|
|
135
|
+
would be particularly helpful if you could share your transcripts.
|
|
136
|
+
|
|
137
|
+
## Development
|
|
138
|
+
|
|
139
|
+
In a clone of this repository:
|
|
140
|
+
```bash
|
|
141
|
+
uv venv .venv && uv pip install -e ".[dev]"
|
|
142
|
+
make test
|
|
143
|
+
```
|
|
144
|
+
`uv tool install -e .` installs a development version of `rocq-solid-mcp` to
|
|
145
|
+
`~/.local/bin/`.
|
|
146
|
+
|
|
147
|
+
To release a new version, bump `version` in `pyproject.toml` and push a tag
|
|
148
|
+
`vX.Y.Z`. The `release` GitHub workflow builds the package and publishes it to
|
|
149
|
+
PyPI.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "rocq-solid-mcp"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "RocqSolid: an MCP server for Rocq proof development, backed by rocq-lsp"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "Benjamin Peters" }]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
keywords = ["rocq", "coq", "mcp", "proof assistant", "lsp", "agent"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 5 - Production/Stable",
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Intended Audience :: Science/Research",
|
|
16
|
+
"Operating System :: MacOS",
|
|
17
|
+
"Operating System :: POSIX :: Linux",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"fastmcp>=4.1,<5",
|
|
29
|
+
"pydantic>=2.13,<3",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.0",
|
|
35
|
+
"pytest-asyncio>=0.25",
|
|
36
|
+
"pytest-xdist>=3.6",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/bn-peters/rocq-solid-mcp"
|
|
41
|
+
Repository = "https://github.com/bn-peters/rocq-solid-mcp"
|
|
42
|
+
Issues = "https://github.com/bn-peters/rocq-solid-mcp/issues"
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
rocq-solid-mcp = "rocq_solid_mcp.cli:main"
|
|
46
|
+
# Legacy name from before the rename; kept so existing configurations work.
|
|
47
|
+
bens-rocq-mcp = "rocq_solid_mcp.cli:main"
|
|
48
|
+
|
|
49
|
+
[build-system]
|
|
50
|
+
requires = ["setuptools>=77.0"]
|
|
51
|
+
build-backend = "setuptools.build_meta"
|
|
52
|
+
|
|
53
|
+
[tool.setuptools.packages.find]
|
|
54
|
+
where = ["src"]
|
|
55
|
+
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
asyncio_mode = "auto"
|
|
58
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
59
|
+
testpaths = ["tests/unit", "tests/integration", "tests/e2e"]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from typing import TextIO
|
|
9
|
+
|
|
10
|
+
from .manager import RocqLspManager
|
|
11
|
+
from .models import RenderOptions
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main(argv: Sequence[str] | None = None) -> int | None:
|
|
15
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
16
|
+
if not args:
|
|
17
|
+
run_mcp_server()
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
parsed = _parser().parse_args(args)
|
|
21
|
+
if parsed.command == "check":
|
|
22
|
+
return asyncio.run(_check(parsed.file, sys.stdout))
|
|
23
|
+
return 2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def run_mcp_server() -> None:
|
|
27
|
+
from .server import main as server_main
|
|
28
|
+
|
|
29
|
+
server_main()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def _check(file: str, output: TextIO) -> int:
|
|
33
|
+
manager = RocqLspManager()
|
|
34
|
+
try:
|
|
35
|
+
result = await manager.check_file(
|
|
36
|
+
file,
|
|
37
|
+
command_timeout_seconds=0,
|
|
38
|
+
timeout_after_error_seconds=0,
|
|
39
|
+
overall_timeout_seconds=0,
|
|
40
|
+
long_running_seconds=0,
|
|
41
|
+
)
|
|
42
|
+
rendered = result.render(RenderOptions(message_limit=None))
|
|
43
|
+
print(json.dumps(rendered, indent=2), file=output)
|
|
44
|
+
return 0
|
|
45
|
+
finally:
|
|
46
|
+
await manager.close()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _parser() -> argparse.ArgumentParser:
|
|
50
|
+
parser = argparse.ArgumentParser(prog="rocq-solid-mcp")
|
|
51
|
+
subcommands = parser.add_subparsers(dest="command", required=True)
|
|
52
|
+
|
|
53
|
+
check = subcommands.add_parser(
|
|
54
|
+
"check",
|
|
55
|
+
help="check a Rocq file and print formatted JSON",
|
|
56
|
+
)
|
|
57
|
+
check.add_argument("file", help="path to a .v file")
|
|
58
|
+
|
|
59
|
+
return parser
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import os
|
|
5
|
+
import warnings
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
DEFAULT_COMMAND_TIMEOUT_SECONDS = 5.0
|
|
10
|
+
DEFAULT_TIMEOUT_AFTER_ERROR_SECONDS = 10.0
|
|
11
|
+
DEFAULT_MESSAGE_LIMIT = 10
|
|
12
|
+
MAX_MESSAGE_LIMIT = 50
|
|
13
|
+
DEFAULT_OVERALL_TIMEOUT_SECONDS = 55
|
|
14
|
+
MAX_OVERALL_TIMEOUT_SECONDS = 3600.0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class TimeoutPolicy:
|
|
19
|
+
request_seconds: float = DEFAULT_OVERALL_TIMEOUT_SECONDS
|
|
20
|
+
cancel_grace_seconds: float = 5.0
|
|
21
|
+
command_seconds: float | None = DEFAULT_COMMAND_TIMEOUT_SECONDS
|
|
22
|
+
# Overall deadline for the rest of a check once the first error
|
|
23
|
+
# diagnostic has been observed: the whole call is cancelled at most this
|
|
24
|
+
# many seconds after the first error. None or a non-positive value
|
|
25
|
+
# disables it.
|
|
26
|
+
request_seconds_after_error: float | None = DEFAULT_TIMEOUT_AFTER_ERROR_SECONDS
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class RocqLspConfig:
|
|
31
|
+
binary: str
|
|
32
|
+
timeout_policy: TimeoutPolicy
|
|
33
|
+
max_errors: int
|
|
34
|
+
workspace: Path | None
|
|
35
|
+
message_limit: int = DEFAULT_MESSAGE_LIMIT
|
|
36
|
+
use_opam: bool = True
|
|
37
|
+
idle_ttl_seconds: float = 1800.0
|
|
38
|
+
dune_build: bool = True
|
|
39
|
+
dune_timeout_seconds: float = DEFAULT_OVERALL_TIMEOUT_SECONDS
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _float_env(name: str, default: str) -> float:
|
|
43
|
+
value = float(os.environ.get(name, default))
|
|
44
|
+
if not math.isfinite(value):
|
|
45
|
+
# nan/inf would poison timeout arithmetic and asyncio timers.
|
|
46
|
+
return float(default)
|
|
47
|
+
return value
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _optional_positive_float_env(name: str, default: str = "0") -> float | None:
|
|
51
|
+
"""Read an optional float where non-positive values disable the setting.
|
|
52
|
+
|
|
53
|
+
A set-but-empty variable falls back to the default, matching the unset
|
|
54
|
+
case, instead of failing to parse. Non-finite values (nan/inf) disable
|
|
55
|
+
the setting: they would poison timeout arithmetic and asyncio timers,
|
|
56
|
+
and "no limit" is exactly what disabling means.
|
|
57
|
+
"""
|
|
58
|
+
raw = os.environ.get(name, default)
|
|
59
|
+
if not raw.strip():
|
|
60
|
+
raw = default
|
|
61
|
+
value = float(raw)
|
|
62
|
+
if not math.isfinite(value) or value <= 0:
|
|
63
|
+
return None
|
|
64
|
+
return value
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def bounded_overall_timeout_seconds(value: float | None) -> float:
|
|
68
|
+
if value is None:
|
|
69
|
+
return DEFAULT_OVERALL_TIMEOUT_SECONDS
|
|
70
|
+
return min(float(value), MAX_OVERALL_TIMEOUT_SECONDS)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def bounded_message_limit(limit: int | None) -> int:
|
|
74
|
+
if limit is None:
|
|
75
|
+
return DEFAULT_MESSAGE_LIMIT
|
|
76
|
+
return min(max(int(limit), 0), MAX_MESSAGE_LIMIT)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _int_env(name: str, default: str) -> int:
|
|
80
|
+
return int(os.environ.get(name, default))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _bool_env(name: str, default: str) -> bool:
|
|
84
|
+
value = os.environ.get(name, default).strip().lower()
|
|
85
|
+
if value in {"1", "true", "yes", "on"}:
|
|
86
|
+
return True
|
|
87
|
+
if value in {"0", "false", "no", "off"}:
|
|
88
|
+
return False
|
|
89
|
+
raise ValueError(f"{name} must be a boolean value")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def load_config() -> RocqLspConfig:
|
|
93
|
+
if "ROCQ_LSP_COMMAND_TIMEOUT_AFTER_ERROR" in os.environ:
|
|
94
|
+
warnings.warn(
|
|
95
|
+
"ROCQ_LSP_COMMAND_TIMEOUT_AFTER_ERROR is no longer read; use "
|
|
96
|
+
"ROCQ_LSP_TIMEOUT_AFTER_ERROR, which bounds the rest of a check "
|
|
97
|
+
"after its first error (non-positive values disable it).",
|
|
98
|
+
stacklevel=2,
|
|
99
|
+
)
|
|
100
|
+
workspace = os.environ.get("ROCQ_LSP_WORKSPACE")
|
|
101
|
+
return RocqLspConfig(
|
|
102
|
+
binary=os.environ.get("ROCQ_LSP_BINARY", "coq-lsp"),
|
|
103
|
+
timeout_policy=TimeoutPolicy(
|
|
104
|
+
request_seconds=bounded_overall_timeout_seconds(
|
|
105
|
+
_float_env("ROCQ_LSP_TIMEOUT", str(DEFAULT_OVERALL_TIMEOUT_SECONDS))
|
|
106
|
+
),
|
|
107
|
+
cancel_grace_seconds=_float_env("ROCQ_LSP_HARD_TIMEOUT_GRACE", "5"),
|
|
108
|
+
command_seconds=_optional_positive_float_env(
|
|
109
|
+
"ROCQ_LSP_COMMAND_TIMEOUT", str(DEFAULT_COMMAND_TIMEOUT_SECONDS)
|
|
110
|
+
),
|
|
111
|
+
request_seconds_after_error=_optional_positive_float_env(
|
|
112
|
+
"ROCQ_LSP_TIMEOUT_AFTER_ERROR",
|
|
113
|
+
str(DEFAULT_TIMEOUT_AFTER_ERROR_SECONDS),
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
max_errors=_int_env("ROCQ_LSP_MAX_ERRORS", "1000000"),
|
|
117
|
+
workspace=Path(workspace).expanduser().resolve() if workspace else None,
|
|
118
|
+
message_limit=bounded_message_limit(
|
|
119
|
+
_int_env("ROCQ_LSP_MESSAGE_LIMIT", str(DEFAULT_MESSAGE_LIMIT))
|
|
120
|
+
),
|
|
121
|
+
use_opam=_bool_env("ROCQ_LSP_USE_OPAM", "1"),
|
|
122
|
+
idle_ttl_seconds=_float_env("ROCQ_LSP_IDLE_TTL_SECONDS", "1800"),
|
|
123
|
+
dune_build=_bool_env("ROCQ_LSP_DUNE_BUILD", "1"),
|
|
124
|
+
dune_timeout_seconds=bounded_overall_timeout_seconds(
|
|
125
|
+
_float_env("ROCQ_LSP_DUNE_TIMEOUT", str(DEFAULT_OVERALL_TIMEOUT_SECONDS))
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def rocq_lsp_command(binary: str, *, use_opam: bool) -> tuple[str, ...]:
|
|
131
|
+
if use_opam:
|
|
132
|
+
return ("opam", "exec", "--", binary)
|
|
133
|
+
return (binary,)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def rocq_lsp_settings(max_errors: int) -> dict[str, object]:
|
|
137
|
+
return {
|
|
138
|
+
"client_version": "any",
|
|
139
|
+
"eager_diagnostics": True,
|
|
140
|
+
"max_errors": max_errors,
|
|
141
|
+
"check_only_on_request": False,
|
|
142
|
+
"send_perf_data": True,
|
|
143
|
+
"send_execinfo": True,
|
|
144
|
+
"send_diags": True,
|
|
145
|
+
"send_diags_extra_data": True,
|
|
146
|
+
}
|