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.
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/rocq-solid-mcp)](https://pypi.org/project/rocq-solid-mcp/)
39
+ [![CI](https://github.com/bn-peters/rocq-solid-mcp/actions/workflows/ci.yml/badge.svg?branch=master)](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
+ [![PyPI](https://img.shields.io/pypi/v/rocq-solid-mcp)](https://pypi.org/project/rocq-solid-mcp/)
4
+ [![CI](https://github.com/bn-peters/rocq-solid-mcp/actions/workflows/ci.yml/badge.svg?branch=master)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """RocqSolid: an MCP server for Rocq proof development via rocq-lsp."""
2
+
3
+ from .manager import RocqLspManager
4
+ from .models import CheckResult
5
+
6
+ __all__ = ["CheckResult", "RocqLspManager"]
@@ -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
+ }