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.
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
+ # Use `uv run --project . <command>` to run this module directly for testing.
4
+
3
5
  import asyncio
4
6
 
5
7
  from .mcp_core import LOGGER, main
@@ -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 = SCRIPT_DIR / "crowdsec-mcp.log"
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.1.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
- ### Setup
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
- uv sync
62
+ uvx --from crowdsec-local-mcp init
62
63
  ```
63
64
 
64
- ## Configuration for Claude Desktop
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
- **Important**: Replace `/path/to/crowdsec-mcp-rule-helper` with the actual absolute path to your cloned repository.
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=rMW9ANz3H1C8gpDK8HZ56-nJb7YmdO_9AyW-P7uChu0,454
3
- crowdsec_local_mcp/mcp_core.py,sha256=kHoLu2pBXc40hSJzIHTOvErZw8wl1tTp33Xcma_6mVI,4826
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.1.0.dist-info/licenses/LICENSE,sha256=3UN9Hca_TnpUOAecGoPKh1fjI5Ol-rSoP8epbBuERE4,1065
26
- crowdsec_local_mcp-0.1.0.dist-info/METADATA,sha256=KbQnfzLN_FQ84dfRg1n-GtpTud6Q6ShrZlwkboFi0zc,2772
27
- crowdsec_local_mcp-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
- crowdsec_local_mcp-0.1.0.dist-info/entry_points.txt,sha256=CQZ_MiGOe7dJBNDo8tNBiYg6B1eRtC_h1qXtBietr5c,65
29
- crowdsec_local_mcp-0.1.0.dist-info/top_level.txt,sha256=MC0OAZ7qK5gG78swUkedPT3pfekze1NL5cO90s90CYM,19
30
- crowdsec_local_mcp-0.1.0.dist-info/RECORD,,
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,,
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  crowdsec-mcp = crowdsec_local_mcp.__main__:run
3
+ init = crowdsec_local_mcp.setup_cli:main