crowdsec-local-mcp 0.1.0__py3-none-any.whl → 0.7.0.post1.dev0__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.
Files changed (26) hide show
  1. crowdsec_local_mcp/__init__.py +6 -1
  2. crowdsec_local_mcp/__main__.py +1 -1
  3. crowdsec_local_mcp/_version.py +1 -0
  4. crowdsec_local_mcp/compose/scenario-test/.gitignore +1 -0
  5. crowdsec_local_mcp/compose/scenario-test/docker-compose.yml +19 -0
  6. crowdsec_local_mcp/compose/scenario-test/scenarios/.gitkeep +0 -0
  7. crowdsec_local_mcp/compose/waf-test/docker-compose.yml +5 -6
  8. crowdsec_local_mcp/compose/waf-test/nginx/Dockerfile +3 -2
  9. crowdsec_local_mcp/mcp_core.py +114 -19
  10. crowdsec_local_mcp/mcp_scenarios.py +579 -23
  11. crowdsec_local_mcp/mcp_waf.py +567 -337
  12. crowdsec_local_mcp/prompts/prompt-expr-helpers.txt +514 -0
  13. crowdsec_local_mcp/prompts/prompt-scenario-deploy.txt +70 -21
  14. crowdsec_local_mcp/prompts/prompt-scenario.txt +26 -2
  15. crowdsec_local_mcp/prompts/prompt-waf-tests.txt +101 -0
  16. crowdsec_local_mcp/prompts/prompt-waf-top-level.txt +31 -0
  17. crowdsec_local_mcp/prompts/prompt-waf.txt +0 -26
  18. crowdsec_local_mcp/setup_cli.py +375 -0
  19. crowdsec_local_mcp-0.7.0.post1.dev0.dist-info/METADATA +114 -0
  20. crowdsec_local_mcp-0.7.0.post1.dev0.dist-info/RECORD +38 -0
  21. {crowdsec_local_mcp-0.1.0.dist-info → crowdsec_local_mcp-0.7.0.post1.dev0.dist-info}/entry_points.txt +1 -0
  22. crowdsec_local_mcp-0.1.0.dist-info/METADATA +0 -93
  23. crowdsec_local_mcp-0.1.0.dist-info/RECORD +0 -30
  24. {crowdsec_local_mcp-0.1.0.dist-info → crowdsec_local_mcp-0.7.0.post1.dev0.dist-info}/WHEEL +0 -0
  25. {crowdsec_local_mcp-0.1.0.dist-info → crowdsec_local_mcp-0.7.0.post1.dev0.dist-info}/licenses/LICENSE +0 -0
  26. {crowdsec_local_mcp-0.1.0.dist-info → crowdsec_local_mcp-0.7.0.post1.dev0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,101 @@
1
+ You are an expert in generating automated test assets for the CrowdSec WAF harness. Given a Nuclei template and the name of the rule under test, produce the artifacts required by the docker-based test workflow. Focus exclusively on generating the test configuration (`config.yaml`) and the adapted Nuclei template that validates the rule blocks the attack with a `403` status code.
2
+
3
+ ## Test Artifact Guidelines
4
+
5
+ 1. **config.yaml**
6
+ - Must be valid YAML with the keys `appsec-rules:` and `nuclei_template`.
7
+ - `appsec-rules:` is a YAML list that always includes:
8
+ - `./appsec-rules/crowdsecurity/base-config.yaml`
9
+ - The path to the generated rule (e.g. `./appsec-rules/crowdsecurity/vpatch-CVE-2020-17496.yaml`).
10
+ - `nuclei_template` is the filename of the adapted Nuclei template (e.g. `CVE-2020-17496.yaml`).
11
+
12
+ 2. **Adapted Nuclei Template**
13
+ - Preserve the original request structure that exercises the vulnerability.
14
+ - Normalise metadata so the template identifies itself as a test:
15
+ - `info.name` matches the CVE identifier.
16
+ - `info.author` is `crowdsec`.
17
+ - `info.severity` is `info`.
18
+ - `info.description` briefly states it is for testing (e.g. `CVE-2020-17496 testing`).
19
+ - `info.tags` includes `appsec-testing`.
20
+ - Keep payloads, raw requests, and helper fields that are required for the exploit to function.
21
+ - Force the rule-under-test to respond with a block by ensuring the only matcher checks for HTTP status code `403`. Remove every other matcher from the original template.
22
+ - When the original template lacks an explicit matcher, add a `matchers` block with a single status matcher for `403`.
23
+ - If the template requires `cookie-reuse` or other execution flags, keep them intact.
24
+
25
+ 3. **General Expectations**
26
+ - Do not attempt to regenerate the detection rule—only produce the two testing artifacts.
27
+ - Respect YAML indentation (two spaces) and quote strings that contain special characters.
28
+ - Mirror the case and structure of URIs, headers, and payloads from the input template.
29
+ - Provide helpful inline comments only when strictly necessary to explain intentional failures.
30
+
31
+ ## Output Format
32
+
33
+ Always return exactly two sections separated by delimiters:
34
+
35
+ ```
36
+ ===TEST_CONFIG===
37
+ <config.yaml content>
38
+
39
+ ===TEST_NUCLEI===
40
+ <adapted nuclei template content>
41
+ ```
42
+
43
+ No additional sections or commentary should be emitted.
44
+
45
+ ## Example Input
46
+
47
+ ```yaml
48
+ id: CVE-2020-17496
49
+
50
+ info:
51
+ name: vBulletin 5.5.4 - 5.6.2- Remote Command Execution
52
+ author: pussycat0x
53
+ severity: critical
54
+ description: 'vBulletin versions 5.5.4 through 5.6.2 allow remote command execution via crafted subWidgets data in an ajax/render/widget_tabbedcontainer_tab_panel request. NOTE: this issue exists because of an incomplete fix for CVE-2019-16759.'
55
+
56
+ http:
57
+ - raw:
58
+ - |
59
+ POST /ajax/render/widget_tabbedcontainer_tab_panel HTTP/1.1
60
+ Host: {{Hostname}}
61
+ Content-Type: application/x-www-form-urlencoded
62
+
63
+ subWidgets[0][template]=widget_php&subWidgets[0][config][code]=echo shell_exec('cat ../../../../../../../../../../../../etc/passwd'); exit;
64
+
65
+ matchers-condition: and
66
+ matchers:
67
+ - type: status
68
+ status:
69
+ - 200
70
+ ```
71
+
72
+ ## Example Output
73
+
74
+ ===TEST_CONFIG===
75
+ appsec-rules:
76
+ - ./appsec-rules/crowdsecurity/base-config.yaml
77
+ - ./appsec-rules/crowdsecurity/vpatch-CVE-2020-17496.yaml
78
+ nuclei_template: CVE-2020-17496.yaml
79
+
80
+ ===TEST_NUCLEI===
81
+ id: CVE-2020-17496
82
+ info:
83
+ name: CVE-2020-17496
84
+ author: crowdsec
85
+ severity: info
86
+ description: CVE-2020-17496 testing
87
+ tags: appsec-testing
88
+ http:
89
+ - raw:
90
+ - |
91
+ POST /ajax/render/widget_tabbedcontainer_tab_panel HTTP/1.1
92
+ Host: {{Hostname}}
93
+ Content-Type: application/x-www-form-urlencoded
94
+
95
+ subWidgets[0][template]=widget_php&subWidgets[0][config][code]=echo shell_exec('cat ../../../../../../../../../../../../etc/passwd'); exit;
96
+
97
+ cookie-reuse: true
98
+ matchers:
99
+ - type: status
100
+ status:
101
+ - 403
@@ -0,0 +1,31 @@
1
+ You are the top-level cyber security assistant for orchestrating CrowdSec AppSec WAF tasks. Treat this prompt as the first step whenever the user asks about creating, reviewing, testing, or deploying WAF rules so you can decide which specialized tool or prompt to invoke next.
2
+
3
+ The process is the following, and it's CRITICAL to follow it unless asked otherwise by the user:
4
+
5
+ 1. **Validate User Intent**: Ensure that you understand user objective and that your rule will follow his directives.
6
+ - If the user provides a CVE, use `fetch_nuclei_exploit` to get a working nuclei template / exploit.
7
+ - If the user provides a natural language description, ask further question if necessary to be sure the rule will be specific enough.
8
+ 2. **Generate Initial Rule**: Refer to `get_waf_prompt` tool and prompt for syntax and guidelines. If you want everything bundled (main prompt + examples), call `generate_waf_rule` instead.
9
+ 3. **Validate Syntax**: Use the `validate_waf_rule` tool to check YAML syntax
10
+ 4. **Lint for Quality**: Use the `lint_waf_rule` tool to check for warnings and improvement hints
11
+ 5. **Iterate if Needed**: If validation fails or linter shows warnings, fix the issues and repeat steps 2-3
12
+ 6. **Final Output**: Only provide final output when both validation passes and linting shows no critical issues. Summarize results and link the user to next-step tooling options below.
13
+
14
+ ⚠️ Final Validation Checklist (REQUIRED before output ends):
15
+ - Confirm validation tool returns "✅ VALIDATION PASSED"
16
+ - Address all warnings from linting tool
17
+
18
+ **IMPORTANT**: If validation or linting fails, you MUST iterate and fix the issues before providing the final output. Do not output rules that fail validation or have critical linting warnings.
19
+
20
+ ### After presenting **Final Output** to the user:
21
+
22
+ Once you have successfully generated and validated a WAF rule, propose the user the next steps:
23
+ - Deploy the rule on a live instance:
24
+ - Use tool `manage_waf_stack`
25
+ - Allows you and the user to manually tests for false-positives and false-negatives
26
+ - Create and validate a test for the rule - allows contribution to the hub:
27
+ - Use tool `generate_waf_tests` to get instructions on test creation
28
+ - Use tool `run_waf_tests` to run and validate the tests
29
+ - Once done, you can suggest the user to open a PR to https://github.com/crowdsecurity/hub/
30
+ - Help the user to deploy the rule on his setup:
31
+ - Use tool `deploy_waf_rule` to get detailed instructions and guide the user.
@@ -227,15 +227,6 @@ You are an expert in cybersecurity and threat detection, specializing in automat
227
227
  value: '/src/options.php'
228
228
  ```
229
229
 
230
- ### Validation and Quality Assurance Process:
231
- **CRITICAL: You MUST follow this iterative validation process:**
232
-
233
- 1. **Generate Initial Rule**: Create the detection rule following all guidelines above
234
- 2. **Validate Syntax**: Use the `validate_waf_rule` tool to check YAML syntax
235
- 3. **Lint for Quality**: Use the `lint_waf_rule` tool to check for warnings and improvement hints
236
- 4. **Iterate if Needed**: If validation fails or linter shows warnings, fix the issues and repeat steps 2-3
237
- 5. **Final Output**: Only provide final output when both validation passes and linting shows no critical issues
238
-
239
230
  ### Output Format with Delimiters:
240
231
 
241
232
  Output format (use the exact delimiters):
@@ -262,25 +253,8 @@ Output format (use the exact delimiters):
262
253
 
263
254
  - <verbatim output from linter tool>
264
255
 
265
- ## Next Steps
266
- - Do you want me to test the WAF rule
267
- - Do you want some deployment guidance
268
256
  ```
269
257
 
270
- ⚠️ Final Validation Checklist (REQUIRED before output ends):
271
- - Confirm validation tool returns "✅ VALIDATION PASSED"
272
- - Address all warnings from linting tool
273
- - Confirm all value: fields are lowercase
274
- - Confirm transform includes lowercase wherever applicable
275
- - Confirm no match.value contains capital letters
276
- - Confirm rule uses contains instead of regex when applicable
277
-
278
- **IMPORTANT**: If validation or linting fails, you MUST iterate and fix the issues before providing the final output. Do not output rules that fail validation or have critical linting warnings.
279
-
280
- ### After Rule Generation and Validation:
281
- Once you have successfully generated and validated a WAF rule, remind the user that the next step is to deploy it. Use the `deploy_waf_rule` tool to provide comprehensive deployment instructions.
282
-
283
-
284
258
  ### Example Input (Nuclei Template):
285
259
  ```yaml
286
260
  id: CVE-2025-24893
@@ -0,0 +1,375 @@
1
+ import argparse
2
+ import json
3
+ import os
4
+ import platform
5
+ import shlex
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from collections.abc import Iterable
12
+
13
+ SERVER_KEY = "crowdsec-local-mcp"
14
+ SERVER_LABEL = "CrowdSecMCP"
15
+
16
+ @dataclass
17
+ class CLIArgs:
18
+ target: str
19
+ config_path: Path | None
20
+ dry_run: bool
21
+ force: bool
22
+ command_override: str | None
23
+ cwd_override: Path | None
24
+
25
+
26
+ def main(argv: Iterable[str] | None = None) -> None:
27
+ args = _parse_args(argv)
28
+ command, cmd_args = _resolve_runner(args.command_override)
29
+ server_payload = {
30
+ "command": command,
31
+ "args": cmd_args,
32
+ "metadata": {
33
+ "label": SERVER_LABEL,
34
+ "description": "CrowdSec local MCP server",
35
+ },
36
+ }
37
+ if args.cwd_override:
38
+ server_payload["cwd"] = str(args.cwd_override)
39
+
40
+ if args.target == "stdio":
41
+ _print_stdio(server_payload)
42
+ return
43
+
44
+ if args.target == "claude-desktop":
45
+ _configure_claude(args, server_payload)
46
+ elif args.target == "claude-code":
47
+ _configure_claude_code(args, server_payload)
48
+ elif args.target == "chatgpt":
49
+ _configure_chatgpt(args, server_payload)
50
+ elif args.target == "vscode":
51
+ _configure_vscode(args, server_payload)
52
+ else:
53
+ raise ValueError(f"Unsupported target '{args.target}'")
54
+
55
+
56
+ def _parse_args(argv: Iterable[str] | None) -> CLIArgs:
57
+ parser = argparse.ArgumentParser(
58
+ prog="init",
59
+ description=(
60
+ "Initialize CrowdSec MCP integration for supported clients "
61
+ "(Claude Desktop, Claude Code, ChatGPT Desktop, Visual Studio Code, or stdio)."
62
+ ),
63
+ )
64
+ parser.add_argument(
65
+ "target",
66
+ choices=("claude-desktop", "claude-code", "chatgpt", "vscode", "stdio"),
67
+ help="Client to configure.",
68
+ )
69
+ parser.add_argument(
70
+ "--config-path",
71
+ type=Path,
72
+ help="Override the configuration file path to update.",
73
+ )
74
+ parser.add_argument(
75
+ "--dry-run",
76
+ action="store_true",
77
+ help="Print the resulting configuration instead of writing it.",
78
+ )
79
+ parser.add_argument(
80
+ "--force",
81
+ action="store_true",
82
+ help="Create configuration even if the file is missing.",
83
+ )
84
+ parser.add_argument(
85
+ "--command",
86
+ dest="command_override",
87
+ help=(
88
+ "Override the command used to launch the MCP server. "
89
+ "Defaults to 'uvx run --from crowdsec-local-mcp crowdsec-mcp' or "
90
+ "falls back to the current Python interpreter."
91
+ ),
92
+ )
93
+ parser.add_argument(
94
+ "--cwd",
95
+ dest="cwd_override",
96
+ type=Path,
97
+ help="Set the working directory used when launching the server.",
98
+ )
99
+
100
+ parsed = parser.parse_args(argv)
101
+ return CLIArgs(
102
+ target=parsed.target,
103
+ config_path=parsed.config_path,
104
+ dry_run=parsed.dry_run,
105
+ force=parsed.force,
106
+ command_override=parsed.command_override,
107
+ cwd_override=parsed.cwd_override,
108
+ )
109
+
110
+
111
+ def _resolve_runner(command_override: str | None) -> tuple[str, list[str]]:
112
+ if command_override:
113
+ command_parts = command_override.strip().split()
114
+ if not command_parts:
115
+ raise ValueError("Command override cannot be empty.")
116
+ return command_parts[0], command_parts[1:]
117
+
118
+ for executable in ("uvx", "uv"):
119
+ resolved = shutil.which(executable)
120
+ if resolved:
121
+ return resolved, [
122
+ "--from",
123
+ "crowdsec-local-mcp",
124
+ "crowdsec-mcp",
125
+ ]
126
+
127
+ python_executable = sys.executable
128
+ if not python_executable:
129
+ raise RuntimeError(
130
+ "Unable to determine a Python interpreter to launch the MCP server."
131
+ )
132
+ return python_executable, ["-m", "crowdsec_local_mcp"]
133
+
134
+
135
+ def _configure_claude(args: CLIArgs, server_payload: dict[str, object]) -> None:
136
+ config_path = _resolve_path(args.config_path, _claude_candidates())
137
+ _write_mcp_config(
138
+ config_path,
139
+ server_payload,
140
+ args,
141
+ client_name="Claude Desktop",
142
+ )
143
+
144
+
145
+ def _configure_claude_code(args: CLIArgs, server_payload: dict[str, object]) -> None:
146
+ runner_command = server_payload["command"]
147
+ if not isinstance(runner_command, str):
148
+ raise TypeError("Server payload 'command' must be a string.")
149
+ runner_args = server_payload.get("args", [])
150
+ if not isinstance(runner_args, list):
151
+ raise TypeError("Server payload 'args' must be a list.")
152
+
153
+ claude_invocation = [
154
+ "claude",
155
+ "mcp",
156
+ "add",
157
+ "--transport",
158
+ "stdio",
159
+ "--scope",
160
+ "user",
161
+ SERVER_LABEL,
162
+ "--",
163
+ runner_command,
164
+ *runner_args,
165
+ ]
166
+ quoted_command = " ".join(shlex.quote(part) for part in claude_invocation)
167
+
168
+ if args.dry_run:
169
+ print(
170
+ "Run the following command to register CrowdSec MCP with Claude Code:\n"
171
+ f"{quoted_command}"
172
+ )
173
+ return
174
+
175
+ if shutil.which("claude") is None:
176
+ raise FileNotFoundError(
177
+ "The 'claude' CLI is not available on PATH. Install Claude Code CLI and "
178
+ f"run:\n{quoted_command}"
179
+ )
180
+
181
+ result = subprocess.run(claude_invocation, check=False)
182
+ if result.returncode != 0:
183
+ raise RuntimeError(
184
+ f"'claude mcp add' failed with exit code {result.returncode}. "
185
+ f"Run manually:\n{quoted_command}"
186
+ )
187
+ print("Registered CrowdSec MCP with Claude Code CLI.")
188
+
189
+
190
+ def _configure_chatgpt(args: CLIArgs, server_payload: dict[str, object]) -> None:
191
+ config_path = _resolve_path(args.config_path, _chatgpt_candidates())
192
+ _write_mcp_config(
193
+ config_path,
194
+ server_payload,
195
+ args,
196
+ client_name="ChatGPT Desktop",
197
+ )
198
+
199
+
200
+ def _configure_vscode(args: CLIArgs, server_payload: dict[str, object]) -> None:
201
+ config_path = _resolve_path(args.config_path, _vscode_candidates())
202
+ vscode_payload: dict[str, object] = {
203
+ "command": server_payload["command"],
204
+ "args": server_payload["args"],
205
+ }
206
+ if "cwd" in server_payload:
207
+ vscode_payload["cwd"] = server_payload["cwd"]
208
+ metadata = server_payload.get("metadata")
209
+ if isinstance(metadata, dict):
210
+ vscode_payload["metadata"] = metadata
211
+ _write_mcp_config(
212
+ config_path,
213
+ vscode_payload,
214
+ args,
215
+ client_name="Visual Studio Code",
216
+ servers_key="servers",
217
+ )
218
+
219
+
220
+ def _write_mcp_config(
221
+ config_path: Path,
222
+ server_payload: dict[str, object],
223
+ args: CLIArgs,
224
+ *,
225
+ client_name: str,
226
+ servers_key: str = "mcpServers",
227
+ ) -> None:
228
+ manual_snippet = json.dumps(
229
+ {servers_key: {SERVER_KEY: server_payload}}, indent=2
230
+ )
231
+ config, existed = _load_json(
232
+ config_path,
233
+ allow_missing=True,
234
+ fallback_snippet=manual_snippet,
235
+ )
236
+ if config is None:
237
+ return
238
+ if not existed and not (args.force or args.dry_run):
239
+ raise FileNotFoundError(
240
+ f"{config_path} does not exist. Re-run with --force to create it "
241
+ "or provide --config-path pointing to an existing configuration file."
242
+ )
243
+ server_collection = config.setdefault(servers_key, {})
244
+ if not isinstance(server_collection, dict):
245
+ raise ValueError(f"Expected '{servers_key}' to be an object in {config_path}")
246
+
247
+ server_collection[SERVER_KEY] = server_payload
248
+
249
+ if args.dry_run:
250
+ print(json.dumps(config, indent=2))
251
+ return
252
+
253
+ _ensure_directory(config_path)
254
+ _backup_file_if_exists(config_path)
255
+ config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
256
+ print(f"Configured {client_name} at {config_path}")
257
+
258
+
259
+ def _print_stdio(server_payload: dict[str, object]) -> None:
260
+ snippet = {
261
+ "server": SERVER_KEY,
262
+ "command": server_payload["command"],
263
+ "args": server_payload["args"],
264
+ }
265
+ cwd = server_payload.get("cwd")
266
+ if cwd is not None:
267
+ snippet["cwd"] = cwd
268
+
269
+ snippet_json = json.dumps(snippet, indent=2)
270
+ print(
271
+ "Use the following configuration with stdio-compatible MCP clients:\n"
272
+ f"{snippet_json}"
273
+ )
274
+
275
+
276
+ def _load_json(
277
+ path: Path,
278
+ *,
279
+ allow_missing: bool,
280
+ fallback_snippet: str | None = None,
281
+ ) -> tuple[dict[str, object] | None, bool]:
282
+ if not path.exists():
283
+ if allow_missing:
284
+ return {}, False
285
+ raise FileNotFoundError(f"Configuration file {path} does not exist.")
286
+
287
+ content = path.read_text(encoding="utf-8")
288
+ if not content.strip():
289
+ return {}, True
290
+
291
+ try:
292
+ return json.loads(content), True
293
+ except json.JSONDecodeError as exc:
294
+ manual_msg = (
295
+ f"Couldn't parse {path}: {exc}.\n"
296
+ "Edit it manually to add:\n\n"
297
+ f"{fallback_snippet or '<no snippet available>'}"
298
+ )
299
+ print(manual_msg)
300
+ return None, True
301
+
302
+
303
+ def _resolve_path(explicit: Path | None, candidates: list[Path]) -> Path:
304
+ if explicit:
305
+ return explicit.expanduser()
306
+
307
+ expanded = [candidate.expanduser() for candidate in candidates]
308
+ for candidate in expanded:
309
+ if candidate.exists():
310
+ return candidate
311
+
312
+ if expanded:
313
+ return expanded[0]
314
+
315
+ raise ValueError("No configuration path candidates were provided.")
316
+
317
+
318
+ def _ensure_directory(path: Path) -> None:
319
+ path.parent.mkdir(parents=True, exist_ok=True)
320
+
321
+
322
+ def _backup_file_if_exists(path: Path) -> None:
323
+ if not path.exists():
324
+ return
325
+ backup_path = path.with_suffix(path.suffix + ".bak")
326
+ shutil.copy2(path, backup_path)
327
+ print(f"Existing configuration backed up to {backup_path}")
328
+
329
+
330
+ def _claude_candidates() -> list[Path]:
331
+ system = platform.system()
332
+ if system == "Darwin":
333
+ return [
334
+ Path.home()
335
+ / "Library"
336
+ / "Application Support"
337
+ / "Claude"
338
+ / "claude_desktop_config.json"
339
+ ]
340
+ if system == "Windows":
341
+ base = Path(os.environ.get("APPDATA", Path.home()))
342
+ return [base / "Claude" / "claude_desktop_config.json"]
343
+ return [Path.home() / ".config" / "Claude" / "claude_desktop_config.json"]
344
+
345
+
346
+ def _chatgpt_candidates() -> list[Path]:
347
+ system = platform.system()
348
+ if system == "Darwin":
349
+ return [
350
+ Path.home()
351
+ / "Library"
352
+ / "Application Support"
353
+ / "ChatGPT"
354
+ / "config.json"
355
+ ]
356
+ if system == "Windows":
357
+ base = Path(os.environ.get("APPDATA", Path.home()))
358
+ return [base / "ChatGPT" / "config.json"]
359
+ return [Path.home() / ".config" / "ChatGPT" / "config.json"]
360
+
361
+
362
+ def _vscode_candidates() -> list[Path]:
363
+ system = platform.system()
364
+ if system == "Windows":
365
+ base = Path(os.environ.get("APPDATA", Path.home()))
366
+ return [base / "Code" / "User" / "mcp.json", base / "Code - Insiders" / "User" / "mcp.json"]
367
+ if system == "Darwin":
368
+ base = Path.home() / "Library" / "Application Support"
369
+ return [base / "Code" / "User" / "mcp.json", base / "Code - Insiders" / "User" / "mcp.json"]
370
+ # Linux and others
371
+ base = Path.home() / ".config"
372
+ return [base / "Code" / "User" / "mcp.json", base / "Code - Insiders" / "User" / "mcp.json"]
373
+
374
+ if __name__ == "__main__":
375
+ main()
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: crowdsec-local-mcp
3
+ Version: 0.7.0.post1.dev0
4
+ Summary: An MCP exposing prompts and tools to help users write WAF rules, scenarios etc.
5
+ License-Expression: MIT
6
+ Classifier: Development Status :: 4 - Beta
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Topic :: Security
13
+ Classifier: Topic :: System :: Systems Administration
14
+ Classifier: Topic :: Utilities
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: jsonschema>=4.25.1
19
+ Requires-Dist: mcp>=1.15.0
20
+ Requires-Dist: pyyaml>=6.0.3
21
+ Requires-Dist: requests>=2.32.5
22
+ Dynamic: license-file
23
+
24
+ <p align="center">
25
+ <img src="https://github.com/crowdsecurity/crowdsec-docs/blob/main/crowdsec-docs/static/img/crowdsec_logo.png" alt="CrowdSec" title="CrowdSec" width="400" height="260"/>
26
+ </p>
27
+
28
+
29
+ **Life is too short to write YAML, just ask nicely!**
30
+
31
+ > A Model Context Protocol (MCP) server to generate, validate, and deploy CrowdSec WAF rules & Scenarios.
32
+
33
+
34
+ ## Features
35
+
36
+ ### WAF Rules Features
37
+
38
+ - **WAF Rule Generation**: Generate CrowdSec WAF rules from user input or a CVE reference
39
+ - **Validation**: Validate syntaxical correctness of WAF rules
40
+ - **Linting**: Get warnings and hints to improve your WAF rules
41
+ - **Deployment Guide**: Step-by-step deployment instructions
42
+ - **Docker Test Harness**: Spin up CrowdSec + nginx + bouncer to exercise rules for false positives/negatives
43
+ - **Nuclei Lookup**: Quickly jump to existing templates in the official `projectdiscovery/nuclei-templates` repository for a given CVE
44
+
45
+ ### Scenarios Features
46
+
47
+ - **CrowdSec Scenarios Generation**: Generate CrowdSec scenarios
48
+ - **Validation**: Validate syntaxical correctness of scenarios
49
+ - **Linting**: Get warnings and hints to improve your scenarios
50
+ - **Deployment Guide**: Step-by-step deployment instructions
51
+ - **Docker Test Harness**: Spin up CrowdSec to test scenario behavior
52
+
53
+ ## Demo
54
+
55
+ ### WAF Rules Creation and testing
56
+
57
+ - [Rule creation from natural language](https://claude.ai/share/f0f246b2-6b20-4d70-a16c-c6b627ab2d80)
58
+ - [Rule creation from CVE reference](https://claude.ai/share/b6599407-82dd-443c-a12d-9a9825ed99df)
59
+
60
+ ### Scenario Creation and testing
61
+
62
+ - [Rule creation on HTTP events](https://claude.ai/share/3658165a-5636-4a7e-8043-01e7a7517200)
63
+ - [Rule creation based on GeoLocation factors](https://claude.ai/share/ff154e66-3c1a-44e6-a464-b694f65bd67e)
64
+
65
+ ## Prerequisites
66
+
67
+ - [uv](https://docs.astral.sh/uv/) 0.4 or newer, which provides the `uvx` runner used in the examples below.
68
+ - Docker with the Compose plugin (Compose v2).
69
+
70
+ ## Installation
71
+
72
+ You can install the MCP using `uvx` **or** use packaged `.mcpb` file for claude code.
73
+
74
+ ### Using `.mcpb` package
75
+
76
+ If you're using `claude desktop`, you can configure the MCP directly by double-clicking the `.mcpb` file that accompanies the release.
77
+
78
+ > [!IMPORTANT]
79
+ > On MacOS, configure `uv` path in the extension settings if `uv` isn't installed in the standard path.
80
+
81
+ ### Using `uvx`
82
+
83
+ - Configure supported clients automatically with `uvx --from crowdsec-local-mcp init <client>`, where `<client>` is one of `claude-desktop`, `claude-code`, `chatgpt`, `vscode`, or `stdio`:
84
+
85
+ ```bash
86
+ uvx --from crowdsec-local-mcp init --dry-run claude-code
87
+ ```
88
+
89
+ Run `uvx --from crowdsec-local-mcp init --help` to see all flags and supported targets.
90
+
91
+ #### What `init` configures
92
+
93
+ The `init` helper writes the CrowdSec MCP server definition into the client’s JSON configuration:
94
+
95
+ - `claude-desktop` → `claude_desktop_config.json` in the Claude Desktop settings directory
96
+ - `claude-code` → invoke `claude mcp` command with needed args
97
+ - `chatgpt` → `config.json` in the ChatGPT Desktop settings directory
98
+ - `vscode` → `mcp.json` for VS Code (stable and insiders are both detected)
99
+
100
+ If the client's configuration file already exists, a `.bak` backup is created before the MCP server block is updated. When the file is missing you can either pass `--force` to create it, or point `--config-path` to a custom location. Combine `--dry-run` with these options to preview the JSON without making any changes.
101
+
102
+ By default the CLI launches the server with `uvx --from crowdsec-local-mcp crowdsec-mcp`. If neither `uvx` nor `uv` is available, it falls back to your current Python interpreter; you can override the executable with `--command` and the working directory with `--cwd`.
103
+
104
+ #### Using the `stdio` target
105
+
106
+ `stdio` does not modify any files. Instead, `init stdio` prints a ready-to-paste JSON snippet that you can drop into any stdio-compatible MCP client configuration. This is useful when you want to manually wire the server into tools that do not have built-in automation support yet.
107
+
108
+ ## Troubleshooting
109
+
110
+ If you just installed the mcp extension via `.mcpb` and `uv` or `uvx` isn't in the standard path, check the extension settings to configure `uv` path.
111
+
112
+ ## Logging
113
+
114
+ - 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`.