crowdsec-local-mcp 0.1.0__py3-none-any.whl → 0.2.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.
- crowdsec_local_mcp/__main__.py +2 -0
- crowdsec_local_mcp/mcp_core.py +2 -1
- crowdsec_local_mcp/setup_cli.py +306 -0
- {crowdsec_local_mcp-0.1.0.dist-info → crowdsec_local_mcp-0.2.0.dist-info}/METADATA +9 -28
- {crowdsec_local_mcp-0.1.0.dist-info → crowdsec_local_mcp-0.2.0.dist-info}/RECORD +9 -8
- {crowdsec_local_mcp-0.1.0.dist-info → crowdsec_local_mcp-0.2.0.dist-info}/entry_points.txt +1 -0
- {crowdsec_local_mcp-0.1.0.dist-info → crowdsec_local_mcp-0.2.0.dist-info}/WHEEL +0 -0
- {crowdsec_local_mcp-0.1.0.dist-info → crowdsec_local_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {crowdsec_local_mcp-0.1.0.dist-info → crowdsec_local_mcp-0.2.0.dist-info}/top_level.txt +0 -0
crowdsec_local_mcp/__main__.py
CHANGED
crowdsec_local_mcp/mcp_core.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
+
import tempfile
|
|
3
4
|
from collections import OrderedDict
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Any, Callable, Dict, List, Optional
|
|
@@ -11,7 +12,7 @@ from mcp.server.models import InitializationOptions
|
|
|
11
12
|
|
|
12
13
|
SCRIPT_DIR = Path(__file__).parent
|
|
13
14
|
PROMPTS_DIR = SCRIPT_DIR / "prompts"
|
|
14
|
-
LOG_FILE_PATH =
|
|
15
|
+
LOG_FILE_PATH = Path(tempfile.gettempdir()) / "crowdsec-mcp.log"
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def _configure_logger() -> logging.Logger:
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Iterable, List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
SERVER_KEY = "crowdsec-local-mcp"
|
|
12
|
+
SERVER_LABEL = "CrowdSec MCP"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class CLIArgs:
|
|
17
|
+
target: str
|
|
18
|
+
config_path: Optional[Path]
|
|
19
|
+
dry_run: bool
|
|
20
|
+
force: bool
|
|
21
|
+
command_override: Optional[str]
|
|
22
|
+
cwd_override: Optional[Path]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main(argv: Optional[Iterable[str]] = None) -> None:
|
|
26
|
+
args = _parse_args(argv)
|
|
27
|
+
command, cmd_args = _resolve_runner(args.command_override)
|
|
28
|
+
server_payload = {
|
|
29
|
+
"command": command,
|
|
30
|
+
"args": cmd_args,
|
|
31
|
+
"metadata": {
|
|
32
|
+
"label": SERVER_LABEL,
|
|
33
|
+
"description": "CrowdSec local MCP server",
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
if args.cwd_override:
|
|
37
|
+
server_payload["cwd"] = str(args.cwd_override)
|
|
38
|
+
|
|
39
|
+
if args.target == "stdio":
|
|
40
|
+
_print_stdio(server_payload)
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
if args.target == "claude-desktop":
|
|
44
|
+
_configure_claude(args, server_payload)
|
|
45
|
+
elif args.target == "chatgpt":
|
|
46
|
+
_configure_chatgpt(args, server_payload)
|
|
47
|
+
elif args.target == "vscode":
|
|
48
|
+
_configure_vscode(args, server_payload)
|
|
49
|
+
else:
|
|
50
|
+
raise ValueError(f"Unsupported target '{args.target}'")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _parse_args(argv: Optional[Iterable[str]]) -> CLIArgs:
|
|
54
|
+
parser = argparse.ArgumentParser(
|
|
55
|
+
prog="init",
|
|
56
|
+
description=(
|
|
57
|
+
"Initialize CrowdSec MCP integration for supported clients "
|
|
58
|
+
"(Claude Desktop, ChatGPT Desktop, Visual Studio Code, or stdio)."
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"target",
|
|
63
|
+
choices=("claude-desktop", "chatgpt", "vscode", "stdio"),
|
|
64
|
+
help="Client to configure.",
|
|
65
|
+
)
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--config-path",
|
|
68
|
+
type=Path,
|
|
69
|
+
help="Override the configuration file path to update.",
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--dry-run",
|
|
73
|
+
action="store_true",
|
|
74
|
+
help="Print the resulting configuration instead of writing it.",
|
|
75
|
+
)
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"--force",
|
|
78
|
+
action="store_true",
|
|
79
|
+
help="Create configuration even if the file is missing.",
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--command",
|
|
83
|
+
dest="command_override",
|
|
84
|
+
help=(
|
|
85
|
+
"Override the command used to launch the MCP server. "
|
|
86
|
+
"Defaults to 'uvx run --from crowdsec-local-mcp crowdsec-mcp' or "
|
|
87
|
+
"falls back to the current Python interpreter."
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"--cwd",
|
|
92
|
+
dest="cwd_override",
|
|
93
|
+
type=Path,
|
|
94
|
+
help="Set the working directory used when launching the server.",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
parsed = parser.parse_args(argv)
|
|
98
|
+
return CLIArgs(
|
|
99
|
+
target=parsed.target,
|
|
100
|
+
config_path=parsed.config_path,
|
|
101
|
+
dry_run=parsed.dry_run,
|
|
102
|
+
force=parsed.force,
|
|
103
|
+
command_override=parsed.command_override,
|
|
104
|
+
cwd_override=parsed.cwd_override,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _resolve_runner(command_override: Optional[str]) -> Tuple[str, List[str]]:
|
|
109
|
+
if command_override:
|
|
110
|
+
command_parts = command_override.strip().split()
|
|
111
|
+
if not command_parts:
|
|
112
|
+
raise ValueError("Command override cannot be empty.")
|
|
113
|
+
return command_parts[0], command_parts[1:]
|
|
114
|
+
|
|
115
|
+
for executable in ("uvx", "uv"):
|
|
116
|
+
resolved = shutil.which(executable)
|
|
117
|
+
if resolved:
|
|
118
|
+
return resolved, [
|
|
119
|
+
"--from",
|
|
120
|
+
"crowdsec-local-mcp",
|
|
121
|
+
"crowdsec-mcp",
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
python_executable = sys.executable
|
|
125
|
+
if not python_executable:
|
|
126
|
+
raise RuntimeError(
|
|
127
|
+
"Unable to determine a Python interpreter to launch the MCP server."
|
|
128
|
+
)
|
|
129
|
+
return python_executable, ["-m", "crowdsec_local_mcp"]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _configure_claude(args: CLIArgs, server_payload: Dict[str, object]) -> None:
|
|
133
|
+
config_path = _resolve_path(args.config_path, _claude_candidates())
|
|
134
|
+
_write_mcp_config(
|
|
135
|
+
config_path,
|
|
136
|
+
server_payload,
|
|
137
|
+
args,
|
|
138
|
+
client_name="Claude Desktop",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _configure_chatgpt(args: CLIArgs, server_payload: Dict[str, object]) -> None:
|
|
143
|
+
config_path = _resolve_path(args.config_path, _chatgpt_candidates())
|
|
144
|
+
_write_mcp_config(
|
|
145
|
+
config_path,
|
|
146
|
+
server_payload,
|
|
147
|
+
args,
|
|
148
|
+
client_name="ChatGPT Desktop",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _configure_vscode(args: CLIArgs, server_payload: Dict[str, object]) -> None:
|
|
153
|
+
config_path = _resolve_path(args.config_path, _vscode_candidates())
|
|
154
|
+
vscode_payload: Dict[str, object] = {
|
|
155
|
+
"command": server_payload["command"],
|
|
156
|
+
"args": server_payload["args"],
|
|
157
|
+
}
|
|
158
|
+
if "cwd" in server_payload:
|
|
159
|
+
vscode_payload["cwd"] = server_payload["cwd"]
|
|
160
|
+
metadata = server_payload.get("metadata")
|
|
161
|
+
if isinstance(metadata, dict):
|
|
162
|
+
vscode_payload["metadata"] = metadata
|
|
163
|
+
_write_mcp_config(
|
|
164
|
+
config_path,
|
|
165
|
+
vscode_payload,
|
|
166
|
+
args,
|
|
167
|
+
client_name="Visual Studio Code",
|
|
168
|
+
servers_key="servers",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _write_mcp_config(
|
|
173
|
+
config_path: Path,
|
|
174
|
+
server_payload: Dict[str, object],
|
|
175
|
+
args: CLIArgs,
|
|
176
|
+
*,
|
|
177
|
+
client_name: str,
|
|
178
|
+
servers_key: str = "mcpServers",
|
|
179
|
+
) -> None:
|
|
180
|
+
config, existed = _load_json(config_path, allow_missing=True)
|
|
181
|
+
if not existed and not (args.force or args.dry_run):
|
|
182
|
+
raise FileNotFoundError(
|
|
183
|
+
f"{config_path} does not exist. Re-run with --force to create it "
|
|
184
|
+
"or provide --config-path pointing to an existing configuration file."
|
|
185
|
+
)
|
|
186
|
+
server_collection = config.setdefault(servers_key, {})
|
|
187
|
+
if not isinstance(server_collection, dict):
|
|
188
|
+
raise ValueError(f"Expected '{servers_key}' to be an object in {config_path}")
|
|
189
|
+
|
|
190
|
+
server_collection[SERVER_KEY] = server_payload
|
|
191
|
+
|
|
192
|
+
if args.dry_run:
|
|
193
|
+
print(json.dumps(config, indent=2))
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
_ensure_directory(config_path)
|
|
197
|
+
_backup_file_if_exists(config_path)
|
|
198
|
+
config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
|
|
199
|
+
print(f"Configured {client_name} at {config_path}")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _print_stdio(server_payload: Dict[str, object]) -> None:
|
|
203
|
+
snippet = {
|
|
204
|
+
"server": SERVER_KEY,
|
|
205
|
+
"command": server_payload["command"],
|
|
206
|
+
"args": server_payload["args"],
|
|
207
|
+
}
|
|
208
|
+
cwd = server_payload.get("cwd")
|
|
209
|
+
if cwd is not None:
|
|
210
|
+
snippet["cwd"] = cwd
|
|
211
|
+
|
|
212
|
+
print(
|
|
213
|
+
"Use the following configuration with stdio-compatible MCP clients:\n"
|
|
214
|
+
f"{json.dumps(snippet, indent=2)}"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _load_json(path: Path, *, allow_missing: bool) -> Tuple[Dict[str, object], bool]:
|
|
219
|
+
if not path.exists():
|
|
220
|
+
if allow_missing:
|
|
221
|
+
return {}, False
|
|
222
|
+
raise FileNotFoundError(f"Configuration file {path} does not exist.")
|
|
223
|
+
|
|
224
|
+
content = path.read_text(encoding="utf-8")
|
|
225
|
+
if not content.strip():
|
|
226
|
+
return {}, True
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
return json.loads(content), True
|
|
230
|
+
except json.JSONDecodeError as exc:
|
|
231
|
+
raise ValueError(f"Failed to parse JSON from {path}: {exc}") from exc
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _resolve_path(explicit: Optional[Path], candidates: List[Path]) -> Path:
|
|
235
|
+
if explicit:
|
|
236
|
+
return explicit.expanduser()
|
|
237
|
+
|
|
238
|
+
expanded = [candidate.expanduser() for candidate in candidates]
|
|
239
|
+
for candidate in expanded:
|
|
240
|
+
if candidate.exists():
|
|
241
|
+
return candidate
|
|
242
|
+
|
|
243
|
+
if expanded:
|
|
244
|
+
return expanded[0]
|
|
245
|
+
|
|
246
|
+
raise ValueError("No configuration path candidates were provided.")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _ensure_directory(path: Path) -> None:
|
|
250
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _backup_file_if_exists(path: Path) -> None:
|
|
254
|
+
if not path.exists():
|
|
255
|
+
return
|
|
256
|
+
backup_path = path.with_suffix(path.suffix + ".bak")
|
|
257
|
+
shutil.copy2(path, backup_path)
|
|
258
|
+
print(f"Existing configuration backed up to {backup_path}")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _claude_candidates() -> List[Path]:
|
|
262
|
+
system = platform.system()
|
|
263
|
+
if system == "Darwin":
|
|
264
|
+
return [
|
|
265
|
+
Path.home()
|
|
266
|
+
/ "Library"
|
|
267
|
+
/ "Application Support"
|
|
268
|
+
/ "Claude"
|
|
269
|
+
/ "claude_desktop_config.json"
|
|
270
|
+
]
|
|
271
|
+
if system == "Windows":
|
|
272
|
+
base = Path(os.environ.get("APPDATA", Path.home()))
|
|
273
|
+
return [base / "Claude" / "claude_desktop_config.json"]
|
|
274
|
+
return [Path.home() / ".config" / "Claude" / "claude_desktop_config.json"]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _chatgpt_candidates() -> List[Path]:
|
|
278
|
+
system = platform.system()
|
|
279
|
+
if system == "Darwin":
|
|
280
|
+
return [
|
|
281
|
+
Path.home()
|
|
282
|
+
/ "Library"
|
|
283
|
+
/ "Application Support"
|
|
284
|
+
/ "ChatGPT"
|
|
285
|
+
/ "config.json"
|
|
286
|
+
]
|
|
287
|
+
if system == "Windows":
|
|
288
|
+
base = Path(os.environ.get("APPDATA", Path.home()))
|
|
289
|
+
return [base / "ChatGPT" / "config.json"]
|
|
290
|
+
return [Path.home() / ".config" / "ChatGPT" / "config.json"]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _vscode_candidates() -> List[Path]:
|
|
294
|
+
system = platform.system()
|
|
295
|
+
if system == "Windows":
|
|
296
|
+
base = Path(os.environ.get("APPDATA", Path.home()))
|
|
297
|
+
return [base / "Code" / "User" / "mcp.json", base / "Code - Insiders" / "User" / "mcp.json"]
|
|
298
|
+
elif system == "Darwin":
|
|
299
|
+
base = Path.home() / "Library" / "Application Support"
|
|
300
|
+
return [base / "Code" / "User" / "mcp.json", base / "Code - Insiders" / "User" / "mcp.json"]
|
|
301
|
+
else: # Linux and others
|
|
302
|
+
base = Path.home() / ".config"
|
|
303
|
+
return [base / "Code" / "User" / "mcp.json", base / "Code - Insiders" / "User" / "mcp.json"]
|
|
304
|
+
|
|
305
|
+
if __name__ == "__main__":
|
|
306
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crowdsec-local-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: An MCP exposing prompts and tools to help users write WAF rules, scenarios etc.
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -54,40 +54,21 @@ Dynamic: license-file
|
|
|
54
54
|
|
|
55
55
|
## Installation
|
|
56
56
|
|
|
57
|
-
###
|
|
57
|
+
### Quick MCP client setup
|
|
58
|
+
|
|
59
|
+
- Configure supported clients automatically with `uvx run --from crowdsec-local-mcp init <client>`, where `<client>` is one of `claude-desktop`, `chatgpt`, `vscode`, or `stdio`:
|
|
58
60
|
|
|
59
|
-
Install dependencies using `uv`:
|
|
60
61
|
```bash
|
|
61
|
-
|
|
62
|
+
uvx --from crowdsec-local-mcp init
|
|
62
63
|
```
|
|
63
64
|
|
|
64
|
-
##
|
|
65
|
-
|
|
66
|
-
### macOS/Linux
|
|
67
|
-
|
|
68
|
-
1. Find your Claude Desktop config file:
|
|
69
|
-
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
70
|
-
- Linux: `~/.config/Claude/claude_desktop_config.json`
|
|
71
|
-
|
|
72
|
-
2. Add the MCP server configuration:
|
|
73
|
-
```json
|
|
74
|
-
{
|
|
75
|
-
"mcpServers": {
|
|
76
|
-
"crowdsec-prompt-server": {
|
|
77
|
-
"command": "/path/to/crowdsec-mcp-rule-helper/.venv/bin/python",
|
|
78
|
-
"args": [
|
|
79
|
-
"/path/to/crowdsec-mcp-rule-helper/mcp-prompt.py"
|
|
80
|
-
],
|
|
81
|
-
"cwd": "/path/to/crowdsec-mcp-rule-helper"
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
```
|
|
65
|
+
## Logging
|
|
86
66
|
|
|
87
|
-
|
|
67
|
+
- The MCP server writes its log file to your operating system's temporary directory. On Linux/macOS this is typically `/tmp/crowdsec-mcp.log`; on Windows it resolves via `%TEMP%\crowdsec-mcp.log`.
|
|
88
68
|
|
|
89
69
|
## Pre Requisites
|
|
90
70
|
|
|
91
71
|
- Docker + Docker Compose
|
|
92
72
|
|
|
93
|
-
- Python
|
|
73
|
+
- Python >= 3.12
|
|
74
|
+
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
crowdsec_local_mcp/__init__.py,sha256=Ux30Xj13RP4H1xuTKmrzg9xIVu9CEuF0f2Kh3QxNNyE,76
|
|
2
|
-
crowdsec_local_mcp/__main__.py,sha256=
|
|
3
|
-
crowdsec_local_mcp/mcp_core.py,sha256=
|
|
2
|
+
crowdsec_local_mcp/__main__.py,sha256=5aKZR3rKmRvUTF4hRKsxkmAXFMUVWOYWKmx3BDb_3C8,533
|
|
3
|
+
crowdsec_local_mcp/mcp_core.py,sha256=jWGUF1kNkcH4sLS2xr3M7tuOTPUwg4nCXfNw502xPJk,4859
|
|
4
4
|
crowdsec_local_mcp/mcp_scenarios.py,sha256=3c5075THWv8gwwVz1cFED7ShDvcULheaVnf5BzhcJKI,13386
|
|
5
5
|
crowdsec_local_mcp/mcp_waf.py,sha256=yJIvuh3RhmIiV08RcWSSCaEfUDtpuaZVtcsJGfDIP38,43127
|
|
6
|
+
crowdsec_local_mcp/setup_cli.py,sha256=ACIntCBwQSJdfYlqibg4Rsp62LxG8StEAg9C2fML5wg,9386
|
|
6
7
|
crowdsec_local_mcp/compose/waf-test/.gitignore,sha256=BLMbJuVqzOfzHCa3Ru2nmNXaZdbj5P_hliIeIGgptAk,111
|
|
7
8
|
crowdsec_local_mcp/compose/waf-test/docker-compose.yml,sha256=wrXI-G_Cjk7AtT8oQXCgPF20uIIRZgoI3S6Dpbe1fBo,2187
|
|
8
9
|
crowdsec_local_mcp/compose/waf-test/crowdsec/init-bouncer.sh,sha256=vI8onvy5V2ENrjwKxTvptBNkTlVhR7S2bK33lekIwWM,578
|
|
@@ -22,9 +23,9 @@ crowdsec_local_mcp/prompts/prompt-waf-examples.txt,sha256=e76mjm-wQa_clk61_7E6As
|
|
|
22
23
|
crowdsec_local_mcp/prompts/prompt-waf.txt,sha256=gZaYfzXWkS7NnAbR_xvWAFKHa2c_qpkFVAkKBE5-CSA,12426
|
|
23
24
|
crowdsec_local_mcp/yaml-schemas/appsec_rules_schema.yaml,sha256=zXu-ikNT-bZNGWdOi5hOqZjpjM_dOnKIdMTtwng8lOM,10639
|
|
24
25
|
crowdsec_local_mcp/yaml-schemas/scenario_schema.yaml,sha256=k0NYxyOUicmMip3Req3zE2CM7tyy8ARcxHlYkSSXbSI,24078
|
|
25
|
-
crowdsec_local_mcp-0.
|
|
26
|
-
crowdsec_local_mcp-0.
|
|
27
|
-
crowdsec_local_mcp-0.
|
|
28
|
-
crowdsec_local_mcp-0.
|
|
29
|
-
crowdsec_local_mcp-0.
|
|
30
|
-
crowdsec_local_mcp-0.
|
|
26
|
+
crowdsec_local_mcp-0.2.0.dist-info/licenses/LICENSE,sha256=3UN9Hca_TnpUOAecGoPKh1fjI5Ol-rSoP8epbBuERE4,1065
|
|
27
|
+
crowdsec_local_mcp-0.2.0.dist-info/METADATA,sha256=lET-8T1leZ6gLFAnHSuwNU8_wQhx0ck84IxRKesanVQ,2515
|
|
28
|
+
crowdsec_local_mcp-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
29
|
+
crowdsec_local_mcp-0.2.0.dist-info/entry_points.txt,sha256=EFTrsplHoT4x-GRrip0jxSQmH7NKBb5w5nX0PphGxTY,106
|
|
30
|
+
crowdsec_local_mcp-0.2.0.dist-info/top_level.txt,sha256=MC0OAZ7qK5gG78swUkedPT3pfekze1NL5cO90s90CYM,19
|
|
31
|
+
crowdsec_local_mcp-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|