crowdsec-local-mcp 0.2.0__py3-none-any.whl → 0.8.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 (27) hide show
  1. crowdsec_local_mcp/__init__.py +6 -1
  2. crowdsec_local_mcp/__main__.py +1 -3
  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 +8 -2
  9. crowdsec_local_mcp/mcp_core.py +112 -18
  10. crowdsec_local_mcp/mcp_scenarios.py +579 -23
  11. crowdsec_local_mcp/mcp_waf.py +774 -402
  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-pr.txt +10 -0
  16. crowdsec_local_mcp/prompts/prompt-waf-tests.txt +113 -0
  17. crowdsec_local_mcp/prompts/prompt-waf-top-level.txt +33 -0
  18. crowdsec_local_mcp/prompts/prompt-waf.txt +0 -26
  19. crowdsec_local_mcp/setup_cli.py +98 -29
  20. crowdsec_local_mcp-0.8.0.post1.dev0.dist-info/METADATA +114 -0
  21. crowdsec_local_mcp-0.8.0.post1.dev0.dist-info/RECORD +39 -0
  22. {crowdsec_local_mcp-0.2.0.dist-info → crowdsec_local_mcp-0.8.0.post1.dev0.dist-info}/WHEEL +1 -1
  23. crowdsec_local_mcp-0.2.0.dist-info/METADATA +0 -74
  24. crowdsec_local_mcp-0.2.0.dist-info/RECORD +0 -31
  25. {crowdsec_local_mcp-0.2.0.dist-info → crowdsec_local_mcp-0.8.0.post1.dev0.dist-info}/entry_points.txt +0 -0
  26. {crowdsec_local_mcp-0.2.0.dist-info → crowdsec_local_mcp-0.8.0.post1.dev0.dist-info}/licenses/LICENSE +0 -0
  27. {crowdsec_local_mcp-0.2.0.dist-info → crowdsec_local_mcp-0.8.0.post1.dev0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,113 @@
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
+ - The path in `appsec-rules` is relative to the hub root, because the rule is not on the hub yet.
12
+ - Use this template and replace the placeholders only:
13
+ ```
14
+ appsec-rules:
15
+ - ./appsec-rules/crowdsecurity/base-config.yaml
16
+ - <PUT_THE_NEW_RULE_PATH_HERE>
17
+ nuclei_template: <PUT_THE_TEST_TEMPLATE_FILENAME_HERE>
18
+ ```
19
+
20
+ 2. **Adapted Nuclei Template**
21
+ - Preserve the original request structure that exercises the vulnerability.
22
+ - Normalise metadata so the template identifies itself as a test:
23
+ - `info.name` matches the CVE identifier.
24
+ - `info.author` is `crowdsec`.
25
+ - `info.severity` is `info`.
26
+ - `info.description` briefly states it is for testing (e.g. `CVE-2020-17496 testing`).
27
+ - `info.tags` includes `appsec-testing`.
28
+ - Keep payloads, raw requests, and helper fields that are required for the exploit to function.
29
+ - 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.
30
+ - When the original template lacks an explicit matcher, add a `matchers` block with a single status matcher for `403`.
31
+ - If the template requires `cookie-reuse` or other execution flags, keep them intact.
32
+
33
+ 3. **General Expectations**
34
+ - Do not attempt to regenerate the detection rule—only produce the two testing artifacts.
35
+ - Respect YAML indentation (two spaces) and quote strings that contain special characters.
36
+ - Mirror the case and structure of URIs, headers, and payloads from the input template.
37
+ - Provide helpful inline comments only when strictly necessary to explain intentional failures.
38
+ - Place test assets in the hub under `.appsec-tests/<test_name>/`:
39
+ - `config.yaml`
40
+ - `<test_name>.yaml` (the adapted nuclei template)
41
+ - If the rule is for virtual patching, use `CVE-YYYY-XYZ` as the test name and `vpatch-CVE-YYYY-XYZ` as the rule name.
42
+
43
+ ## Output Format
44
+
45
+ Always return exactly two sections separated by delimiters:
46
+
47
+ ```
48
+ ===TEST_CONFIG===
49
+ <config.yaml content>
50
+
51
+ ===TEST_NUCLEI===
52
+ <adapted nuclei template content>
53
+ ```
54
+
55
+ No additional sections or commentary should be emitted.
56
+
57
+ ## Example Input
58
+
59
+ ```yaml
60
+ id: CVE-2020-17496
61
+
62
+ info:
63
+ name: vBulletin 5.5.4 - 5.6.2- Remote Command Execution
64
+ author: pussycat0x
65
+ severity: critical
66
+ 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.'
67
+
68
+ http:
69
+ - raw:
70
+ - |
71
+ POST /ajax/render/widget_tabbedcontainer_tab_panel HTTP/1.1
72
+ Host: {{Hostname}}
73
+ Content-Type: application/x-www-form-urlencoded
74
+
75
+ subWidgets[0][template]=widget_php&subWidgets[0][config][code]=echo shell_exec('cat ../../../../../../../../../../../../etc/passwd'); exit;
76
+
77
+ matchers-condition: and
78
+ matchers:
79
+ - type: status
80
+ status:
81
+ - 200
82
+ ```
83
+
84
+ ## Example Output
85
+
86
+ ===TEST_CONFIG===
87
+ appsec-rules:
88
+ - ./appsec-rules/crowdsecurity/base-config.yaml
89
+ - ./appsec-rules/crowdsecurity/vpatch-CVE-2020-17496.yaml
90
+ nuclei_template: CVE-2020-17496.yaml
91
+
92
+ ===TEST_NUCLEI===
93
+ id: CVE-2020-17496
94
+ info:
95
+ name: CVE-2020-17496
96
+ author: crowdsec
97
+ severity: info
98
+ description: CVE-2020-17496 testing
99
+ tags: appsec-testing
100
+ http:
101
+ - raw:
102
+ - |
103
+ POST /ajax/render/widget_tabbedcontainer_tab_panel HTTP/1.1
104
+ Host: {{Hostname}}
105
+ Content-Type: application/x-www-form-urlencoded
106
+
107
+ subWidgets[0][template]=widget_php&subWidgets[0][config][code]=echo shell_exec('cat ../../../../../../../../../../../../etc/passwd'); exit;
108
+
109
+ cookie-reuse: true
110
+ matchers:
111
+ - type: status
112
+ status:
113
+ - 403
@@ -0,0 +1,33 @@
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, suggest help preparing a PR
30
+ - Use tool `get_waf_pr_prompt` to guide PR preparation and draft a PR comment
31
+ - Use tool `prepare_waf_pr` to write the rule and test assets into a local hub clone
32
+ - Help the user to deploy the rule on his setup:
33
+ - 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
@@ -2,27 +2,28 @@ import argparse
2
2
  import json
3
3
  import os
4
4
  import platform
5
+ import shlex
5
6
  import shutil
7
+ import subprocess
6
8
  import sys
7
9
  from dataclasses import dataclass
8
10
  from pathlib import Path
9
- from typing import Dict, Iterable, List, Optional, Tuple
11
+ from collections.abc import Iterable
10
12
 
11
13
  SERVER_KEY = "crowdsec-local-mcp"
12
- SERVER_LABEL = "CrowdSec MCP"
13
-
14
+ SERVER_LABEL = "CrowdSecMCP"
14
15
 
15
16
  @dataclass
16
17
  class CLIArgs:
17
18
  target: str
18
- config_path: Optional[Path]
19
+ config_path: Path | None
19
20
  dry_run: bool
20
21
  force: bool
21
- command_override: Optional[str]
22
- cwd_override: Optional[Path]
22
+ command_override: str | None
23
+ cwd_override: Path | None
23
24
 
24
25
 
25
- def main(argv: Optional[Iterable[str]] = None) -> None:
26
+ def main(argv: Iterable[str] | None = None) -> None:
26
27
  args = _parse_args(argv)
27
28
  command, cmd_args = _resolve_runner(args.command_override)
28
29
  server_payload = {
@@ -42,6 +43,8 @@ def main(argv: Optional[Iterable[str]] = None) -> None:
42
43
 
43
44
  if args.target == "claude-desktop":
44
45
  _configure_claude(args, server_payload)
46
+ elif args.target == "claude-code":
47
+ _configure_claude_code(args, server_payload)
45
48
  elif args.target == "chatgpt":
46
49
  _configure_chatgpt(args, server_payload)
47
50
  elif args.target == "vscode":
@@ -50,17 +53,17 @@ def main(argv: Optional[Iterable[str]] = None) -> None:
50
53
  raise ValueError(f"Unsupported target '{args.target}'")
51
54
 
52
55
 
53
- def _parse_args(argv: Optional[Iterable[str]]) -> CLIArgs:
56
+ def _parse_args(argv: Iterable[str] | None) -> CLIArgs:
54
57
  parser = argparse.ArgumentParser(
55
58
  prog="init",
56
59
  description=(
57
60
  "Initialize CrowdSec MCP integration for supported clients "
58
- "(Claude Desktop, ChatGPT Desktop, Visual Studio Code, or stdio)."
61
+ "(Claude Desktop, Claude Code, ChatGPT Desktop, Visual Studio Code, or stdio)."
59
62
  ),
60
63
  )
61
64
  parser.add_argument(
62
65
  "target",
63
- choices=("claude-desktop", "chatgpt", "vscode", "stdio"),
66
+ choices=("claude-desktop", "claude-code", "chatgpt", "vscode", "stdio"),
64
67
  help="Client to configure.",
65
68
  )
66
69
  parser.add_argument(
@@ -105,7 +108,7 @@ def _parse_args(argv: Optional[Iterable[str]]) -> CLIArgs:
105
108
  )
106
109
 
107
110
 
108
- def _resolve_runner(command_override: Optional[str]) -> Tuple[str, List[str]]:
111
+ def _resolve_runner(command_override: str | None) -> tuple[str, list[str]]:
109
112
  if command_override:
110
113
  command_parts = command_override.strip().split()
111
114
  if not command_parts:
@@ -129,7 +132,7 @@ def _resolve_runner(command_override: Optional[str]) -> Tuple[str, List[str]]:
129
132
  return python_executable, ["-m", "crowdsec_local_mcp"]
130
133
 
131
134
 
132
- def _configure_claude(args: CLIArgs, server_payload: Dict[str, object]) -> None:
135
+ def _configure_claude(args: CLIArgs, server_payload: dict[str, object]) -> None:
133
136
  config_path = _resolve_path(args.config_path, _claude_candidates())
134
137
  _write_mcp_config(
135
138
  config_path,
@@ -139,7 +142,52 @@ def _configure_claude(args: CLIArgs, server_payload: Dict[str, object]) -> None:
139
142
  )
140
143
 
141
144
 
142
- def _configure_chatgpt(args: CLIArgs, server_payload: Dict[str, object]) -> None:
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:
143
191
  config_path = _resolve_path(args.config_path, _chatgpt_candidates())
144
192
  _write_mcp_config(
145
193
  config_path,
@@ -149,9 +197,9 @@ def _configure_chatgpt(args: CLIArgs, server_payload: Dict[str, object]) -> None
149
197
  )
150
198
 
151
199
 
152
- def _configure_vscode(args: CLIArgs, server_payload: Dict[str, object]) -> None:
200
+ def _configure_vscode(args: CLIArgs, server_payload: dict[str, object]) -> None:
153
201
  config_path = _resolve_path(args.config_path, _vscode_candidates())
154
- vscode_payload: Dict[str, object] = {
202
+ vscode_payload: dict[str, object] = {
155
203
  "command": server_payload["command"],
156
204
  "args": server_payload["args"],
157
205
  }
@@ -171,13 +219,22 @@ def _configure_vscode(args: CLIArgs, server_payload: Dict[str, object]) -> None:
171
219
 
172
220
  def _write_mcp_config(
173
221
  config_path: Path,
174
- server_payload: Dict[str, object],
222
+ server_payload: dict[str, object],
175
223
  args: CLIArgs,
176
224
  *,
177
225
  client_name: str,
178
226
  servers_key: str = "mcpServers",
179
227
  ) -> None:
180
- config, existed = _load_json(config_path, allow_missing=True)
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
181
238
  if not existed and not (args.force or args.dry_run):
182
239
  raise FileNotFoundError(
183
240
  f"{config_path} does not exist. Re-run with --force to create it "
@@ -199,7 +256,7 @@ def _write_mcp_config(
199
256
  print(f"Configured {client_name} at {config_path}")
200
257
 
201
258
 
202
- def _print_stdio(server_payload: Dict[str, object]) -> None:
259
+ def _print_stdio(server_payload: dict[str, object]) -> None:
203
260
  snippet = {
204
261
  "server": SERVER_KEY,
205
262
  "command": server_payload["command"],
@@ -209,13 +266,19 @@ def _print_stdio(server_payload: Dict[str, object]) -> None:
209
266
  if cwd is not None:
210
267
  snippet["cwd"] = cwd
211
268
 
269
+ snippet_json = json.dumps(snippet, indent=2)
212
270
  print(
213
271
  "Use the following configuration with stdio-compatible MCP clients:\n"
214
- f"{json.dumps(snippet, indent=2)}"
272
+ f"{snippet_json}"
215
273
  )
216
274
 
217
275
 
218
- def _load_json(path: Path, *, allow_missing: bool) -> Tuple[Dict[str, object], bool]:
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]:
219
282
  if not path.exists():
220
283
  if allow_missing:
221
284
  return {}, False
@@ -228,10 +291,16 @@ def _load_json(path: Path, *, allow_missing: bool) -> Tuple[Dict[str, object], b
228
291
  try:
229
292
  return json.loads(content), True
230
293
  except json.JSONDecodeError as exc:
231
- raise ValueError(f"Failed to parse JSON from {path}: {exc}") from 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
232
301
 
233
302
 
234
- def _resolve_path(explicit: Optional[Path], candidates: List[Path]) -> Path:
303
+ def _resolve_path(explicit: Path | None, candidates: list[Path]) -> Path:
235
304
  if explicit:
236
305
  return explicit.expanduser()
237
306
 
@@ -258,7 +327,7 @@ def _backup_file_if_exists(path: Path) -> None:
258
327
  print(f"Existing configuration backed up to {backup_path}")
259
328
 
260
329
 
261
- def _claude_candidates() -> List[Path]:
330
+ def _claude_candidates() -> list[Path]:
262
331
  system = platform.system()
263
332
  if system == "Darwin":
264
333
  return [
@@ -274,7 +343,7 @@ def _claude_candidates() -> List[Path]:
274
343
  return [Path.home() / ".config" / "Claude" / "claude_desktop_config.json"]
275
344
 
276
345
 
277
- def _chatgpt_candidates() -> List[Path]:
346
+ def _chatgpt_candidates() -> list[Path]:
278
347
  system = platform.system()
279
348
  if system == "Darwin":
280
349
  return [
@@ -290,17 +359,17 @@ def _chatgpt_candidates() -> List[Path]:
290
359
  return [Path.home() / ".config" / "ChatGPT" / "config.json"]
291
360
 
292
361
 
293
- def _vscode_candidates() -> List[Path]:
362
+ def _vscode_candidates() -> list[Path]:
294
363
  system = platform.system()
295
364
  if system == "Windows":
296
365
  base = Path(os.environ.get("APPDATA", Path.home()))
297
366
  return [base / "Code" / "User" / "mcp.json", base / "Code - Insiders" / "User" / "mcp.json"]
298
- elif system == "Darwin":
367
+ if system == "Darwin":
299
368
  base = Path.home() / "Library" / "Application Support"
300
369
  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"]
370
+ # Linux and others
371
+ base = Path.home() / ".config"
372
+ return [base / "Code" / "User" / "mcp.json", base / "Code - Insiders" / "User" / "mcp.json"]
304
373
 
305
374
  if __name__ == "__main__":
306
375
  main()
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: crowdsec-local-mcp
3
+ Version: 0.8.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`.
@@ -0,0 +1,39 @@
1
+ crowdsec_local_mcp/__init__.py,sha256=OrcWsdYsNOPPV4W05EsheZKz0kHcYMujo7pfRyHJ_9Q,236
2
+ crowdsec_local_mcp/__main__.py,sha256=Fb43e7FGfENpdWH-4Ojpz52uuT5_GCC8sInr82JOLz4,512
3
+ crowdsec_local_mcp/_version.py,sha256=MW1G6gkS04nDt7uvLYiQhV-JMbiJNiVvFxmhHQQjW4I,33
4
+ crowdsec_local_mcp/mcp_core.py,sha256=FAy5SCzuUPkXdDQhkU72f4XsQZDohIPqZ4f0bIFAriM,8278
5
+ crowdsec_local_mcp/mcp_scenarios.py,sha256=tWoo5N92hEhaOBmbfxccunoxcq1XmfS2ntYbfnlj6eY,37207
6
+ crowdsec_local_mcp/mcp_waf.py,sha256=uhWhaK-og66fXqDoQ9I2ZGAxz5L3ZhDsN6foAFIGBkU,61247
7
+ crowdsec_local_mcp/setup_cli.py,sha256=1RXhVtG3LgqknBkebzz1z6x_VAW51oDmYXTmQEqv6UY,11354
8
+ crowdsec_local_mcp/compose/scenario-test/.gitignore,sha256=hcFPKNf-CWOt-TmuTPZpafoUwaWxYNbJEEiOl9411fs,16
9
+ crowdsec_local_mcp/compose/scenario-test/docker-compose.yml,sha256=JEkCu9P-9Um3_IBw6xeZ8Tsj4jsdNK-Di-U9ktia1xw,580
10
+ crowdsec_local_mcp/compose/scenario-test/scenarios/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ crowdsec_local_mcp/compose/waf-test/.gitignore,sha256=BLMbJuVqzOfzHCa3Ru2nmNXaZdbj5P_hliIeIGgptAk,111
12
+ crowdsec_local_mcp/compose/waf-test/docker-compose.yml,sha256=MFMd-6F6e8c2Y54Y73aL46V6R90-gJIEiSjx_joCoCw,2124
13
+ crowdsec_local_mcp/compose/waf-test/crowdsec/init-bouncer.sh,sha256=vI8onvy5V2ENrjwKxTvptBNkTlVhR7S2bK33lekIwWM,578
14
+ crowdsec_local_mcp/compose/waf-test/crowdsec/acquis.d/appsec.yaml,sha256=qg1xZmcDUSaUfX4SCaT7CcCilMWpz91fyvVGl1LUTTA,189
15
+ crowdsec_local_mcp/compose/waf-test/crowdsec/appsec-configs/mcp-appsec.yaml.template,sha256=9PoFbUJ6IJep7vVZ6UPs4-MDOSL320U0x4a5mB2tvp0,330
16
+ crowdsec_local_mcp/compose/waf-test/nginx/Dockerfile,sha256=-6QWfRkv1AhdViYVA6rbmUpd_DeISgLmcQ3cboDpsgw,2050
17
+ crowdsec_local_mcp/compose/waf-test/nginx/nginx.conf,sha256=mZpWFNQK9haOj8Fd-ab4GpC4Li3m0qUJLIpg5StL3pU,472
18
+ crowdsec_local_mcp/compose/waf-test/nginx/crowdsec/crowdsec-openresty-bouncer.conf,sha256=s_53rJk5qcSUG1XuHAh4XRUU84xw_tEcJWOYON6JsdU,630
19
+ crowdsec_local_mcp/compose/waf-test/nginx/site-enabled/default-site.conf,sha256=Rq4_jPkTkEL50YF4pNq2jwTjJmokgniQzKy7Y3ca964,357
20
+ crowdsec_local_mcp/compose/waf-test/rules/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ crowdsec_local_mcp/compose/waf-test/rules/base-config.yaml,sha256=Xvk_SxmgKPJBWvr1cN9NIKX2A4-pTMmQo4dmIwPC7yA,1066
22
+ crowdsec_local_mcp/prompts/prompt-expr-helpers.txt,sha256=o6g1-fth5XP0KCDtKc85c6Cb4m2bbDvO-oNqCt5_SlA,16613
23
+ crowdsec_local_mcp/prompts/prompt-scenario-deploy.txt,sha256=re2lJiLzvkdlFJBmV57Lhm2zrqFeGnC6oMUtNS0Rq50,2378
24
+ crowdsec_local_mcp/prompts/prompt-scenario-examples.txt,sha256=H-LdAyhhjWBysquIaL90oKGNdZ20L_PqhqJVuCZL6vw,7484
25
+ crowdsec_local_mcp/prompts/prompt-scenario.txt,sha256=XIrgc0Hw0UAwEIuSRkWzZ9BI3qUjZsaZ-zXs_cBWwpM,7059
26
+ crowdsec_local_mcp/prompts/prompt-waf-deploy.txt,sha256=xFotKHMZiSVYZpjC-PItf1Ee0l3PVpof7917bybZtQg,3247
27
+ crowdsec_local_mcp/prompts/prompt-waf-examples.txt,sha256=e76mjm-wQa_clk61_7E6AsRgdt55m3MycY0lBkfL2Mc,11095
28
+ crowdsec_local_mcp/prompts/prompt-waf-pr.txt,sha256=omA1FE68YRUS3egGn6GRLzU81WPb_Iccl3rDrGz7AyM,617
29
+ crowdsec_local_mcp/prompts/prompt-waf-tests.txt,sha256=ZvSKsQ-B44c6BoORXwDiW33297ORuyoSeq-7EW8MaAM,4654
30
+ crowdsec_local_mcp/prompts/prompt-waf-top-level.txt,sha256=U9rUHw8uJDweG0g8NUYGxmAmM1OFH_pBAr9cAk8wW3c,2628
31
+ crowdsec_local_mcp/prompts/prompt-waf.txt,sha256=VxTc7QM3yYtZoj5nYUMAIQJHyeniPstU-cPrHdyq7bs,10920
32
+ crowdsec_local_mcp/yaml-schemas/appsec_rules_schema.yaml,sha256=zXu-ikNT-bZNGWdOi5hOqZjpjM_dOnKIdMTtwng8lOM,10639
33
+ crowdsec_local_mcp/yaml-schemas/scenario_schema.yaml,sha256=k0NYxyOUicmMip3Req3zE2CM7tyy8ARcxHlYkSSXbSI,24078
34
+ crowdsec_local_mcp-0.8.0.post1.dev0.dist-info/licenses/LICENSE,sha256=3UN9Hca_TnpUOAecGoPKh1fjI5Ol-rSoP8epbBuERE4,1065
35
+ crowdsec_local_mcp-0.8.0.post1.dev0.dist-info/METADATA,sha256=N5tWmFfuFBbnq0D97GlAU4ipFnFquY5RBtz5vDoTJAE,5234
36
+ crowdsec_local_mcp-0.8.0.post1.dev0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
37
+ crowdsec_local_mcp-0.8.0.post1.dev0.dist-info/entry_points.txt,sha256=EFTrsplHoT4x-GRrip0jxSQmH7NKBb5w5nX0PphGxTY,106
38
+ crowdsec_local_mcp-0.8.0.post1.dev0.dist-info/top_level.txt,sha256=MC0OAZ7qK5gG78swUkedPT3pfekze1NL5cO90s90CYM,19
39
+ crowdsec_local_mcp-0.8.0.post1.dev0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5