rocq-solid-mcp 1.0.0__py3-none-any.whl

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,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"]
rocq_solid_mcp/cli.py ADDED
@@ -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
+ }