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.
- rocq_solid_mcp/__init__.py +6 -0
- rocq_solid_mcp/cli.py +63 -0
- rocq_solid_mcp/config.py +146 -0
- rocq_solid_mcp/manager.py +1082 -0
- rocq_solid_mcp/models.py +767 -0
- rocq_solid_mcp/notifications.py +225 -0
- rocq_solid_mcp/pipeline.py +91 -0
- rocq_solid_mcp/pretty.py +167 -0
- rocq_solid_mcp/rocq_client.py +980 -0
- rocq_solid_mcp/server.py +156 -0
- rocq_solid_mcp/session.py +84 -0
- rocq_solid_mcp-1.0.0.dist-info/METADATA +184 -0
- rocq_solid_mcp-1.0.0.dist-info/RECORD +17 -0
- rocq_solid_mcp-1.0.0.dist-info/WHEEL +5 -0
- rocq_solid_mcp-1.0.0.dist-info/entry_points.txt +3 -0
- rocq_solid_mcp-1.0.0.dist-info/licenses/LICENSE +21 -0
- rocq_solid_mcp-1.0.0.dist-info/top_level.txt +1 -0
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())
|
rocq_solid_mcp/config.py
ADDED
|
@@ -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
|
+
}
|