dcert 3.0.35__py3-none-win_amd64.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.
dcert/__init__.py ADDED
@@ -0,0 +1,91 @@
1
+ """dcert: A Python MCP wrapper for the dcert TLS certificate MCP server.
2
+
3
+ This package provides a FastMCP proxy that wraps the dcert-mcp Rust binary,
4
+ automatically exposing all TLS certificate tools via the Model Context Protocol.
5
+
6
+ The proxy pattern means this package requires zero code changes when new
7
+ tools are added to the Rust binary — they are discovered and forwarded
8
+ automatically at runtime via the MCP protocol.
9
+
10
+ Usage as a server:
11
+ from dcert import create_server
12
+ server = create_server()
13
+ server.run()
14
+
15
+ Usage as a client:
16
+ from dcert import create_client
17
+ async with create_client() as client:
18
+ tools = await client.list_tools()
19
+ result = await client.call_tool("analyze_certificate", {"target": "example.com"})
20
+
21
+ Usage with typed async wrappers:
22
+ from dcert.tools import DcertClient
23
+ async with DcertClient() as dcert:
24
+ result = await dcert.analyze_certificate(target="example.com")
25
+ """
26
+
27
+ __version__ = "3.0.35"
28
+
29
+ from dcert.client import create_client
30
+ from dcert.resilience import (
31
+ CircuitBreaker,
32
+ CircuitBreakerOpen,
33
+ OTelConfig,
34
+ RateLimiter,
35
+ ResilienceConfig,
36
+ setup_otel,
37
+ truncate_response,
38
+ )
39
+ from dcert.server import create_server
40
+ from dcert.tools import (
41
+ DcertClient,
42
+ DcertConnectionError,
43
+ DcertError,
44
+ DcertTimeoutError,
45
+ DcertToolError,
46
+ analyze_certificate,
47
+ check_expiry,
48
+ check_revocation,
49
+ compare_certificates,
50
+ convert_pem_to_pfx,
51
+ convert_pfx_to_pem,
52
+ create_keystore,
53
+ create_truststore,
54
+ export_pem,
55
+ tls_connection_info,
56
+ verify_key_match,
57
+ )
58
+
59
+ __all__ = [
60
+ # Core API
61
+ "create_server",
62
+ "create_client",
63
+ "__version__",
64
+ # Client
65
+ "DcertClient",
66
+ # Exceptions
67
+ "DcertError",
68
+ "DcertTimeoutError",
69
+ "DcertConnectionError",
70
+ "DcertToolError",
71
+ # Resilience
72
+ "ResilienceConfig",
73
+ "OTelConfig",
74
+ "CircuitBreaker",
75
+ "CircuitBreakerOpen",
76
+ "RateLimiter",
77
+ "setup_otel",
78
+ "truncate_response",
79
+ # Tool wrappers
80
+ "analyze_certificate",
81
+ "check_expiry",
82
+ "check_revocation",
83
+ "compare_certificates",
84
+ "tls_connection_info",
85
+ "export_pem",
86
+ "verify_key_match",
87
+ "convert_pfx_to_pem",
88
+ "convert_pem_to_pfx",
89
+ "create_keystore",
90
+ "create_truststore",
91
+ ]
Binary file
dcert/bin/dcert.exe ADDED
Binary file
dcert/checksums.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "version": "0.0.0",
3
+ "archives": {}
4
+ }
dcert/cli.py ADDED
@@ -0,0 +1,263 @@
1
+ """CLI entry points for dcert.
2
+
3
+ Provides three commands:
4
+ - ``dcert``: Thin wrapper that execs the bundled Rust ``dcert`` binary.
5
+ - ``dcert-mcp``: Thin wrapper that execs the bundled Rust ``dcert-mcp`` binary.
6
+ - ``dcert-python``: Python MCP proxy server wrapping the Rust binary via FastMCP.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import os
13
+ import shutil
14
+ import stat
15
+ import sys
16
+ from pathlib import Path
17
+
18
+
19
+ def _is_python_script(path: str) -> bool:
20
+ """Check if *path* is a Python console-script wrapper (not a compiled binary).
21
+
22
+ Reads the first 128 bytes; if the file starts with ``#!`` and the first
23
+ line contains ``python``, it is a pip-generated console_script wrapper
24
+ and must be skipped to avoid an infinite exec loop (see helm-mcp PR #33).
25
+ """
26
+ try:
27
+ with open(path, "rb") as fh:
28
+ head = fh.read(128)
29
+ first_line = head.split(b"\n", 1)[0].lower()
30
+ return head[:2] == b"#!" and b"python" in first_line
31
+ except OSError:
32
+ return False
33
+
34
+
35
+ def _find_bundled_binary(name: str) -> str | None:
36
+ """Locate a binary bundled inside the package ``bin/`` directory.
37
+
38
+ If the binary exists but is not executable, it is chmod'd on first use.
39
+
40
+ Returns:
41
+ Absolute path to the binary, or ``None`` if not found.
42
+ """
43
+ pkg_dir = Path(__file__).parent
44
+ bundled = pkg_dir / "bin" / name
45
+ # On Windows the bundled binary carries a .exe suffix (dcert.exe).
46
+ if not bundled.is_file() and sys.platform == "win32" and not name.endswith(".exe"):
47
+ win_bundled = pkg_dir / "bin" / f"{name}.exe"
48
+ if win_bundled.is_file():
49
+ bundled = win_bundled
50
+ if not bundled.is_file():
51
+ return None
52
+ # Ensure the binary is executable (pip may not preserve permissions
53
+ # for package-data files extracted from wheels).
54
+ if not os.access(str(bundled), os.X_OK):
55
+ try:
56
+ bundled.chmod(bundled.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
57
+ except OSError:
58
+ return None
59
+ return str(bundled)
60
+
61
+
62
+ def _find_binary(name: str) -> str:
63
+ """Find a binary by name: bundled in package, then PATH, then auto-download.
64
+
65
+ Skips Python console-script wrappers on PATH to avoid infinite exec
66
+ loops when pip installs the universal wheel.
67
+
68
+ Raises:
69
+ FileNotFoundError: If the binary cannot be located.
70
+ """
71
+ # 1. Bundled binary inside the Python package
72
+ bundled = _find_bundled_binary(name)
73
+ if bundled:
74
+ return bundled
75
+
76
+ # 2. Binary on PATH — skip Python console-script wrappers
77
+ found = shutil.which(name)
78
+ if found and not _is_python_script(found):
79
+ return found
80
+
81
+ # 3. Auto-download from GitHub Releases (fallback for universal wheel)
82
+ from dcert import __version__
83
+ from dcert.download import ensure_binary
84
+
85
+ try:
86
+ downloaded = ensure_binary(__version__)
87
+ if downloaded:
88
+ return downloaded
89
+ except Exception:
90
+ pass
91
+
92
+ raise FileNotFoundError(
93
+ f"{name} binary not found. Install dcert via:\n"
94
+ " brew tap SCGIS-Wales/tap && brew install dcert\n"
95
+ " or: pip install dcert (platform wheel bundles the binary)"
96
+ )
97
+
98
+
99
+ def dcert_main() -> None:
100
+ """Entry point for the ``dcert`` command.
101
+
102
+ Locates the bundled Rust ``dcert`` binary and replaces the current
103
+ process with it, forwarding all command-line arguments.
104
+ """
105
+ try:
106
+ binary = _find_binary("dcert")
107
+ except FileNotFoundError as e:
108
+ print(f"Error: {e}", file=sys.stderr)
109
+ sys.exit(1)
110
+ os.execvp(binary, [binary] + sys.argv[1:])
111
+
112
+
113
+ def dcert_mcp_main() -> None:
114
+ """Entry point for the ``dcert-mcp`` command.
115
+
116
+ Locates the bundled Rust ``dcert-mcp`` binary and replaces the current
117
+ process with it, forwarding all command-line arguments.
118
+ """
119
+ try:
120
+ binary = _find_binary("dcert-mcp")
121
+ except FileNotFoundError as e:
122
+ print(f"Error: {e}", file=sys.stderr)
123
+ sys.exit(1)
124
+ os.execvp(binary, [binary] + sys.argv[1:])
125
+
126
+
127
+ def main() -> None:
128
+ """Run the dcert MCP proxy server (``dcert-python`` command)."""
129
+ parser = argparse.ArgumentParser(
130
+ description="dcert: MCP server for TLS certificate analysis",
131
+ )
132
+ parser.add_argument(
133
+ "--transport",
134
+ choices=["stdio", "http", "sse"],
135
+ default="stdio",
136
+ help="Transport mode (default: stdio)",
137
+ )
138
+ parser.add_argument(
139
+ "--host",
140
+ default="0.0.0.0",
141
+ help="Host for HTTP/SSE mode (default: 0.0.0.0)",
142
+ )
143
+ parser.add_argument(
144
+ "--port",
145
+ type=int,
146
+ default=8080,
147
+ help="Port for HTTP/SSE mode (default: 8080)",
148
+ )
149
+ parser.add_argument(
150
+ "--binary",
151
+ default=None,
152
+ help="Path to dcert-mcp binary (auto-detected if not set)",
153
+ )
154
+ parser.add_argument(
155
+ "--setup",
156
+ action="store_true",
157
+ help="Download the dcert-mcp binary and exit",
158
+ )
159
+
160
+ # -- Resiliency flags --
161
+ parser.add_argument(
162
+ "--no-retry",
163
+ action="store_true",
164
+ help="Disable automatic retry on connection errors",
165
+ )
166
+ parser.add_argument(
167
+ "--no-circuit-breaker",
168
+ action="store_true",
169
+ help="Disable circuit breaker",
170
+ )
171
+ parser.add_argument(
172
+ "--rate-limit",
173
+ type=float,
174
+ default=None,
175
+ metavar="RPS",
176
+ help="Enable rate limiting at RPS requests per second",
177
+ )
178
+ parser.add_argument(
179
+ "--cache",
180
+ action="store_true",
181
+ help="Enable response caching",
182
+ )
183
+ parser.add_argument(
184
+ "--bulkhead-max",
185
+ type=int,
186
+ default=None,
187
+ metavar="N",
188
+ help="Maximum concurrent tool calls (default: 10)",
189
+ )
190
+
191
+ # -- OpenTelemetry --
192
+ parser.add_argument(
193
+ "--otel",
194
+ action="store_true",
195
+ help="Enable OpenTelemetry tracing",
196
+ )
197
+ parser.add_argument(
198
+ "--otel-exporter",
199
+ choices=["console", "otlp"],
200
+ default=None,
201
+ help="OpenTelemetry exporter (default: console)",
202
+ )
203
+
204
+ args = parser.parse_args()
205
+
206
+ if args.setup:
207
+ from dcert import __version__
208
+ from dcert.download import ensure_binary
209
+
210
+ try:
211
+ path = ensure_binary(__version__)
212
+ if path:
213
+ print(f"dcert-mcp binary ready at: {path}")
214
+ else:
215
+ print(
216
+ "No checksums available for this platform. Install the binary manually.",
217
+ file=sys.stderr,
218
+ )
219
+ sys.exit(1)
220
+ except Exception as e:
221
+ print(f"Error downloading binary: {e}", file=sys.stderr)
222
+ sys.exit(1)
223
+ return
224
+
225
+ # Apply CLI overrides to environment so ResilienceConfig picks them up
226
+ if args.no_retry:
227
+ os.environ["DCERT_MCP_NO_RETRY"] = "1"
228
+ if args.no_circuit_breaker:
229
+ os.environ["DCERT_MCP_NO_CIRCUIT_BREAKER"] = "1"
230
+ if args.rate_limit is not None:
231
+ os.environ["DCERT_MCP_RATE_LIMIT_ENABLED"] = "1"
232
+ os.environ["DCERT_MCP_RATE_LIMIT_RPS"] = str(args.rate_limit)
233
+ if args.cache:
234
+ os.environ["DCERT_MCP_CACHE_ENABLED"] = "1"
235
+ if args.bulkhead_max is not None:
236
+ os.environ["DCERT_MCP_BULKHEAD_MAX"] = str(args.bulkhead_max)
237
+
238
+ # OpenTelemetry
239
+ if args.otel:
240
+ os.environ["DCERT_MCP_OTEL_ENABLED"] = "1"
241
+ if args.otel_exporter is not None:
242
+ os.environ["DCERT_MCP_OTEL_EXPORTER"] = args.otel_exporter
243
+
244
+ from dcert.resilience import OTelConfig, setup_otel
245
+
246
+ setup_otel(OTelConfig())
247
+
248
+ from dcert.server import create_server
249
+
250
+ try:
251
+ server = create_server(binary_path=args.binary)
252
+ except FileNotFoundError as e:
253
+ print(f"Error: {e}", file=sys.stderr)
254
+ sys.exit(1)
255
+
256
+ if args.transport == "stdio":
257
+ server.run()
258
+ else:
259
+ server.run(transport=args.transport, host=args.host, port=args.port)
260
+
261
+
262
+ if __name__ == "__main__":
263
+ main()
dcert/client.py ADDED
@@ -0,0 +1,44 @@
1
+ """FastMCP client for connecting to the dcert-mcp Rust binary.
2
+
3
+ Provides a thin client wrapper that connects to the Rust binary via stdio
4
+ transport. All tool discovery is handled by the MCP protocol at runtime,
5
+ so new tools added to the binary are automatically available.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from fastmcp import Client
11
+ from fastmcp.client.transports import StdioTransport
12
+
13
+ from dcert.server import _build_subprocess_env, _find_binary
14
+
15
+
16
+ def create_client(
17
+ binary_path: str | None = None,
18
+ env: dict[str, str] | None = None,
19
+ ) -> Client:
20
+ """Create a FastMCP client connected to the dcert-mcp Rust binary via stdio.
21
+
22
+ Args:
23
+ binary_path: Explicit path to the dcert-mcp binary. Auto-detected if ``None``.
24
+ env: Additional environment variables to pass to the subprocess.
25
+
26
+ Returns:
27
+ A FastMCP ``Client`` instance. Use as an async context manager.
28
+
29
+ Example::
30
+
31
+ async with create_client() as client:
32
+ tools = await client.list_tools()
33
+ result = await client.call_tool(
34
+ "analyze_certificate", {"target": "example.com"}
35
+ )
36
+ """
37
+ binary = binary_path or _find_binary()
38
+ subprocess_env = _build_subprocess_env(extra_env=env)
39
+ transport = StdioTransport(
40
+ command=binary,
41
+ args=[],
42
+ env=subprocess_env or None,
43
+ )
44
+ return Client(transport)
dcert/download.py ADDED
@@ -0,0 +1,268 @@
1
+ """Auto-download dcert binaries from GitHub Releases.
2
+
3
+ Downloads the platform-appropriate tar.gz archive, verifies its SHA256
4
+ checksum against values embedded in the package (checksums.json), extracts
5
+ the ``dcert`` and ``dcert-mcp`` binaries, and installs them to a
6
+ PATH-accessible directory.
7
+
8
+ Supply chain security:
9
+ - Checksums are baked into the wheel at build time, not fetched at runtime.
10
+ - Downloads use HTTPS with certificate verification.
11
+ - Archive is written to a temp file and only extracted after checksum passes.
12
+ - No shell commands or install-time hooks are used.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ import json
19
+ import logging
20
+ import os
21
+ import platform
22
+ import shutil
23
+ import stat
24
+ import sys
25
+ import sysconfig
26
+ import tarfile
27
+ import tempfile
28
+ import urllib.request
29
+ import zipfile
30
+ from pathlib import Path
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ GITHUB_RELEASE_URL = (
35
+ "https://github.com/SCGIS-Wales/dcert/releases/download/v{version}/{archive_name}"
36
+ )
37
+
38
+ # Timeout for the HTTP connection and read (seconds).
39
+ DOWNLOAD_TIMEOUT = 60
40
+
41
+ # Maps (system, machine) to Rust target triple
42
+ PLATFORM_MAP: dict[tuple[str, str], str] = {
43
+ ("darwin", "arm64"): "aarch64-apple-darwin",
44
+ ("darwin", "x86_64"): "x86_64-apple-darwin",
45
+ ("linux", "x86_64"): "x86_64-unknown-linux-gnu",
46
+ ("windows", "amd64"): "x86_64-pc-windows-msvc",
47
+ }
48
+
49
+
50
+ def _is_windows() -> bool:
51
+ return platform.system().lower() == "windows"
52
+
53
+
54
+ def _bin_filename(stem: str) -> str:
55
+ """Return the on-disk binary filename for *stem* (adds .exe on Windows)."""
56
+ return f"{stem}.exe" if _is_windows() else stem
57
+
58
+
59
+ def _load_checksums() -> dict:
60
+ """Load embedded checksums from package data.
61
+
62
+ Returns:
63
+ Parsed checksums dict, or empty dict if file is missing.
64
+ """
65
+ checksums_path = Path(__file__).parent / "checksums.json"
66
+ if not checksums_path.exists():
67
+ return {}
68
+ with open(checksums_path) as f:
69
+ return json.load(f)
70
+
71
+
72
+ def _get_target_triple() -> str | None:
73
+ """Get the Rust target triple for the current platform.
74
+
75
+ Returns:
76
+ Target triple like ``aarch64-apple-darwin``, or ``None`` if
77
+ the platform is not supported.
78
+ """
79
+ system = platform.system().lower()
80
+ machine = platform.machine().lower()
81
+ return PLATFORM_MAP.get((system, machine))
82
+
83
+
84
+ def _get_archive_name() -> str | None:
85
+ """Get the platform-specific archive filename.
86
+
87
+ Returns:
88
+ Archive name like ``dcert-aarch64-apple-darwin.tar.gz``,
89
+ or ``None`` if the platform is not supported.
90
+ """
91
+ triple = _get_target_triple()
92
+ if triple is None:
93
+ return None
94
+ # Windows binaries are shipped as .zip (PowerShell-friendly); other
95
+ # platforms use .tar.gz.
96
+ ext = "zip" if _is_windows() else "tar.gz"
97
+ return f"dcert-{triple}.{ext}"
98
+
99
+
100
+ def _get_install_dir() -> Path:
101
+ """Get the best directory for installing the binaries.
102
+
103
+ Prefers the Python scripts directory (where pip puts console_scripts,
104
+ which is on PATH). Falls back to ``~/.local/bin``.
105
+
106
+ Returns:
107
+ Writable directory path.
108
+ """
109
+ scripts_dir = Path(sysconfig.get_path("scripts"))
110
+ if os.access(str(scripts_dir), os.W_OK):
111
+ return scripts_dir
112
+ # Fallback: user-local bin
113
+ local_bin = Path.home() / ".local" / "bin"
114
+ local_bin.mkdir(parents=True, exist_ok=True)
115
+ return local_bin
116
+
117
+
118
+ def _verify_checksum(file_path: Path, expected_sha256: str) -> bool:
119
+ """Verify SHA256 checksum of a file.
120
+
121
+ Args:
122
+ file_path: Path to the file to verify.
123
+ expected_sha256: Expected hex-encoded SHA256 digest.
124
+
125
+ Returns:
126
+ True if checksum matches, False otherwise.
127
+ """
128
+ sha256 = hashlib.sha256()
129
+ with open(file_path, "rb") as f:
130
+ for chunk in iter(lambda: f.read(8192), b""):
131
+ sha256.update(chunk)
132
+ return sha256.hexdigest() == expected_sha256
133
+
134
+
135
+ def _extract_binaries(archive_path: Path, install_dir: Path) -> Path:
136
+ """Extract dcert and dcert-mcp binaries from a downloaded archive.
137
+
138
+ Handles both .tar.gz (Linux/macOS) and .zip (Windows) archives. On
139
+ Windows the binaries are named ``dcert.exe`` / ``dcert-mcp.exe``.
140
+
141
+ Args:
142
+ archive_path: Path to the downloaded .tar.gz or .zip file.
143
+ install_dir: Directory to install binaries into.
144
+
145
+ Returns:
146
+ Path to the installed dcert-mcp binary.
147
+
148
+ Raises:
149
+ RuntimeError: If the dcert-mcp binary is not found in the archive.
150
+ """
151
+ is_win = _is_windows()
152
+ mcp_name = _bin_filename("dcert-mcp")
153
+ wanted = {_bin_filename("dcert"), mcp_name}
154
+ found_mcp = False
155
+
156
+ def _emit(name: str, file_obj) -> None:
157
+ nonlocal found_mcp
158
+ # Write contents manually rather than using extract() to avoid path
159
+ # traversal — member names could contain "../" even after Path.name.
160
+ target = install_dir / name
161
+ with open(target, "wb") as out:
162
+ shutil.copyfileobj(file_obj, out)
163
+ if not is_win:
164
+ target.chmod(target.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
165
+ if name == mcp_name:
166
+ found_mcp = True
167
+
168
+ if archive_path.suffix == ".zip" or is_win:
169
+ with zipfile.ZipFile(archive_path) as zf:
170
+ for member in zf.infolist():
171
+ name = Path(member.filename).name
172
+ if name in wanted:
173
+ with zf.open(member) as file_obj:
174
+ _emit(name, file_obj)
175
+ else:
176
+ with tarfile.open(archive_path, "r:gz") as tar:
177
+ for member in tar.getmembers():
178
+ name = Path(member.name).name
179
+ if name in wanted:
180
+ file_obj = tar.extractfile(member)
181
+ if file_obj is None:
182
+ continue
183
+ _emit(name, file_obj)
184
+
185
+ if not found_mcp:
186
+ raise RuntimeError("dcert-mcp binary not found in archive")
187
+
188
+ return install_dir / mcp_name
189
+
190
+
191
+ def ensure_binary(version: str) -> str | None:
192
+ """Ensure the dcert-mcp binary is available, downloading if needed.
193
+
194
+ If the binary already exists in the install directory and is executable,
195
+ returns its path immediately. Otherwise downloads the platform archive
196
+ from GitHub Releases, verifies the SHA256 checksum, extracts the
197
+ binaries, and installs them.
198
+
199
+ Args:
200
+ version: Package version (e.g. ``"3.0.12"``). Used to construct
201
+ the download URL and match against checksums.
202
+
203
+ Returns:
204
+ Absolute path to the dcert-mcp binary, or ``None`` if download
205
+ is not possible (e.g. no checksums available for this platform).
206
+
207
+ Raises:
208
+ RuntimeError: If the downloaded archive fails checksum verification.
209
+ urllib.error.URLError: If the download fails.
210
+ """
211
+ install_dir = _get_install_dir()
212
+ target = install_dir / _bin_filename("dcert-mcp")
213
+
214
+ # Already installed?
215
+ if target.exists() and os.access(str(target), os.X_OK):
216
+ return str(target)
217
+
218
+ # Load embedded checksums
219
+ archive_name = _get_archive_name()
220
+ if archive_name is None:
221
+ logger.debug(
222
+ "Unsupported platform %s/%s — skipping auto-download",
223
+ platform.system(),
224
+ platform.machine(),
225
+ )
226
+ return None
227
+
228
+ checksums = _load_checksums()
229
+ expected = checksums.get("archives", {}).get(archive_name)
230
+ if not expected:
231
+ logger.debug("No checksum for %s — skipping auto-download", archive_name)
232
+ return None
233
+
234
+ url = GITHUB_RELEASE_URL.format(version=version, archive_name=archive_name)
235
+ logger.info("Downloading dcert binaries from %s", url)
236
+ print(
237
+ f"Downloading dcert binaries for {platform.system()}/{platform.machine()}...",
238
+ file=sys.stderr,
239
+ )
240
+
241
+ # Download to temp file, verify checksum, then extract
242
+ fd, tmp_path = tempfile.mkstemp(dir=str(install_dir), prefix=".dcert-")
243
+ try:
244
+ os.close(fd)
245
+ with (
246
+ urllib.request.urlopen(url, timeout=DOWNLOAD_TIMEOUT) as resp,
247
+ open(tmp_path, "wb") as out,
248
+ ):
249
+ shutil.copyfileobj(resp, out)
250
+
251
+ if not _verify_checksum(Path(tmp_path), expected):
252
+ raise RuntimeError(
253
+ f"Checksum mismatch for {archive_name}. "
254
+ "The downloaded archive does not match the expected hash. "
255
+ "This could indicate a tampered download."
256
+ )
257
+
258
+ # Extract binaries from the verified archive
259
+ mcp_path = _extract_binaries(Path(tmp_path), install_dir)
260
+
261
+ print(f"Installed dcert binaries to {install_dir}", file=sys.stderr)
262
+ logger.info("Installed dcert binaries to %s", install_dir)
263
+ return str(mcp_path)
264
+ except Exception:
265
+ # Clean up temp file on any failure
266
+ if os.path.exists(tmp_path):
267
+ os.unlink(tmp_path)
268
+ raise
dcert/py.typed ADDED
File without changes