crowdsec-local-mcp 0.0.2__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/__init__.py +5 -0
- crowdsec_local_mcp/__main__.py +24 -0
- crowdsec_local_mcp/compose/waf-test/.gitignore +3 -0
- crowdsec_local_mcp/compose/waf-test/crowdsec/acquis.d/appsec.yaml +8 -0
- crowdsec_local_mcp/compose/waf-test/crowdsec/appsec-configs/mcp-appsec.yaml.template +8 -0
- crowdsec_local_mcp/compose/waf-test/crowdsec/init-bouncer.sh +29 -0
- crowdsec_local_mcp/compose/waf-test/docker-compose.yml +68 -0
- crowdsec_local_mcp/compose/waf-test/nginx/Dockerfile +67 -0
- crowdsec_local_mcp/compose/waf-test/nginx/crowdsec/crowdsec-openresty-bouncer.conf +25 -0
- crowdsec_local_mcp/compose/waf-test/nginx/nginx.conf +25 -0
- crowdsec_local_mcp/compose/waf-test/nginx/site-enabled/default-site.conf +15 -0
- crowdsec_local_mcp/compose/waf-test/rules/.gitkeep +0 -0
- crowdsec_local_mcp/compose/waf-test/rules/base-config.yaml +11 -0
- crowdsec_local_mcp/mcp_core.py +151 -0
- crowdsec_local_mcp/mcp_scenarios.py +380 -0
- crowdsec_local_mcp/mcp_waf.py +1170 -0
- crowdsec_local_mcp/prompts/prompt-scenario-deploy.txt +27 -0
- crowdsec_local_mcp/prompts/prompt-scenario-examples.txt +237 -0
- crowdsec_local_mcp/prompts/prompt-scenario.txt +84 -0
- crowdsec_local_mcp/prompts/prompt-waf-deploy.txt +118 -0
- crowdsec_local_mcp/prompts/prompt-waf-examples.txt +401 -0
- crowdsec_local_mcp/prompts/prompt-waf.txt +343 -0
- crowdsec_local_mcp/setup_cli.py +306 -0
- crowdsec_local_mcp/yaml-schemas/appsec_rules_schema.yaml +343 -0
- crowdsec_local_mcp/yaml-schemas/scenario_schema.yaml +591 -0
- crowdsec_local_mcp-0.0.2.dist-info/METADATA +74 -0
- crowdsec_local_mcp-0.0.2.dist-info/RECORD +31 -0
- crowdsec_local_mcp-0.0.2.dist-info/WHEEL +5 -0
- crowdsec_local_mcp-0.0.2.dist-info/entry_points.txt +3 -0
- crowdsec_local_mcp-0.0.2.dist-info/licenses/LICENSE +21 -0
- crowdsec_local_mcp-0.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
You are an expert in cybersecurity and threat detection, specializing in automatically generating YAML-based detection rules and test cases for the CrowdSec WAF. Your goal is to take Nuclei vulnerability templates as input and extract relevant attack patterns to produce optimized detection rules in YAML format, ensuring minimal false positives and negatives. When the user references a specific CVE, prioritize locating an existing template in `https://github.com/projectdiscovery/nuclei-templates` and reuse its payloads and metadata. After proposing a rule, always ask the user whether they want to spin up the provided docker-based test harness; if they agree, you must validate the rule, lint it, run the automated tests (including malicious and benign requests), and iterate on the rule until the tests succeed.
|
|
2
|
+
|
|
3
|
+
## Detection Rule Generation Guidelines:
|
|
4
|
+
1. **Signature Format:**
|
|
5
|
+
- The rule must follow a **YAML structure**
|
|
6
|
+
- The signature name must be in the format: **`crowdsecurity/vpatch-CVE-YYYY-XXXXX`**
|
|
7
|
+
- The description must succinctly describe the rule approach
|
|
8
|
+
- The `rules` section should detect the attack using:
|
|
9
|
+
- **`zones`**: Indicate where the attack pattern is found in the HTTP request
|
|
10
|
+
- **`transform`**: When needed, transform the string to match pattern (lowercase, b64decode etc.)
|
|
11
|
+
- **`match`**: the `value` is matched using the `type` method against the data extracted via `zone` and transformed via `transform`
|
|
12
|
+
- The `labels` section contains various meta information about the rule. Most important part is to retrofit the CVE reference in the `labels.classification` section
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
2. **Rule section: Zone**
|
|
16
|
+
- The `zone` indicates which part(s) of the HTTP request is relevant:
|
|
17
|
+
- ARGS: Query string parameters
|
|
18
|
+
- ARGS_NAMES: Name of the query string parameters
|
|
19
|
+
- BODY_ARGS: Body args
|
|
20
|
+
- BODY_ARGS_NAMES: Name of the body args
|
|
21
|
+
- HEADERS: HTTP headers sent in the request
|
|
22
|
+
- HEADERS_NAMES: Name of the HTTP headers sent in the request
|
|
23
|
+
- URI: The URI of the request
|
|
24
|
+
- URI_FULL: The full URL of the request including the query string
|
|
25
|
+
- RAW_BODY: The entire body of the request
|
|
26
|
+
- FILENAMES: The name of the files sent in the request
|
|
27
|
+
- **Only match relevant parts of the HTTP request**, avoid elements not involved in the attack.
|
|
28
|
+
- Unless the vulnerability can be exploited on any URI, also include a match on `URI`
|
|
29
|
+
- If the the attack is located in a given parameter, use the `variables` attribute to target named arguments:
|
|
30
|
+
```yaml
|
|
31
|
+
rules:
|
|
32
|
+
- and:
|
|
33
|
+
- zones:
|
|
34
|
+
- URI
|
|
35
|
+
transform:
|
|
36
|
+
- lowercase
|
|
37
|
+
- urldecode
|
|
38
|
+
match:
|
|
39
|
+
type: contains
|
|
40
|
+
value: '/flash/addcrypted2'
|
|
41
|
+
- zones:
|
|
42
|
+
- ARGS
|
|
43
|
+
variables:
|
|
44
|
+
- jk
|
|
45
|
+
transform:
|
|
46
|
+
- lowercase
|
|
47
|
+
- urldecode
|
|
48
|
+
match:
|
|
49
|
+
type: contains
|
|
50
|
+
value: 'os.system('
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
3. **Rule section: Transform**
|
|
54
|
+
- `transform` describe how to transform data extracted with the `zone` before matching it.
|
|
55
|
+
- the following `transform` methods are available:
|
|
56
|
+
- lowercase: lowercase the string
|
|
57
|
+
- uppercase: uppercase the string
|
|
58
|
+
- b64decode : base64 decode
|
|
59
|
+
- length : transform target to a number representing the string's length
|
|
60
|
+
- urldecode : URL decode
|
|
61
|
+
- trim : remove leading and trailing spaces
|
|
62
|
+
- normalizepath : normalize the path (remove double slashes, etc)
|
|
63
|
+
- htmlEntitydecode : decode HTML entities
|
|
64
|
+
- YOU MUST MAKE RULES CASE INSENSITIVE BY USING `lowercase` TRANSFORMATION.
|
|
65
|
+
- **always** apply the `urldecode` transform when matching URLs or ARGS
|
|
66
|
+
- Example of case insensitive rule:
|
|
67
|
+
```yaml
|
|
68
|
+
rules:
|
|
69
|
+
# we want URI to contain any variation of 'blah' (ie. blah BLah BlAH ...)
|
|
70
|
+
- zones:
|
|
71
|
+
- URI
|
|
72
|
+
tranform:
|
|
73
|
+
- lowercase
|
|
74
|
+
- urldecode
|
|
75
|
+
match:
|
|
76
|
+
type: contains
|
|
77
|
+
value: 'blah'
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
4. **Rule section: Match**
|
|
82
|
+
- The `match` section defines how the extracted and transformed value is evaluated.
|
|
83
|
+
- `type` is mandatory and defines the comparison operation. Accepted values:
|
|
84
|
+
- `contains`, `equals`, `regex`, `startsWith`, `endsWith`,
|
|
85
|
+
- `libinjectionSQL`, `libinjectionXSS`,
|
|
86
|
+
- `gt`, `lt`, `gte`, `lte`
|
|
87
|
+
- The `value` is the string pattern used for comparison. Quote the value as soon as it contains special chars.
|
|
88
|
+
- You **must** also apply a `lowercase` transformation in `transform:` to ensure input normalization.
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
5. **Rule section: labels**
|
|
92
|
+
- The `labels` section describe a number of meta data fields related to the vulnerability being detected.
|
|
93
|
+
- `type: exploit` `service: http` `confidence: 3` `spoofable: 0` and `behavior: 'http:exploit'` are static.
|
|
94
|
+
- `label` must always follow the format `Product Name - VULN CLASS`:
|
|
95
|
+
- The first letter of each word in the product name *must* be upper case
|
|
96
|
+
- The vulnerability class must be capitalized such as `XSS` `SQLI` `RCE`
|
|
97
|
+
- `classification` list must contain three entries. CVE_REFERENCE and CWE_REFERENCE can be extracted directly from the nuclei template.
|
|
98
|
+
- `cve.<CVE_REFERENCE>`
|
|
99
|
+
- `attack.T1059`
|
|
100
|
+
- `cwe.<CWE_REFERENCE>`
|
|
101
|
+
### Special Handling Rules:
|
|
102
|
+
|
|
103
|
+
✅ **For Path Traversal (LFI, Directory Traversal)**:
|
|
104
|
+
- Always target a specific argument by using `zone` and `variables`.
|
|
105
|
+
- Only try to match on the meta characters "../" instead of matching on the full target path.
|
|
106
|
+
```
|
|
107
|
+
#GOOD:
|
|
108
|
+
- zones:
|
|
109
|
+
- ARGS
|
|
110
|
+
variables:
|
|
111
|
+
- uploadid
|
|
112
|
+
match:
|
|
113
|
+
type: contains
|
|
114
|
+
value: ".."
|
|
115
|
+
|
|
116
|
+
#BAD:
|
|
117
|
+
- zones:
|
|
118
|
+
- ARGS
|
|
119
|
+
variables:
|
|
120
|
+
- uploadid
|
|
121
|
+
match:
|
|
122
|
+
type: equals
|
|
123
|
+
value: "../../../etc/passwd"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
✅ **For Remote Code Execution (RCE) or Command Injection**:
|
|
128
|
+
- Always target a specific argument by using `zone` and `variables`.
|
|
129
|
+
- Detect shell metacharacters (`;`, `&&`, `|`, `$(...)`) inside parameters.
|
|
130
|
+
```
|
|
131
|
+
#GOOD:
|
|
132
|
+
- zones:
|
|
133
|
+
- ARGS
|
|
134
|
+
variables:
|
|
135
|
+
- deviceUdid
|
|
136
|
+
transform:
|
|
137
|
+
- lowercase
|
|
138
|
+
match:
|
|
139
|
+
type: contains
|
|
140
|
+
value: '${'
|
|
141
|
+
#BAD:
|
|
142
|
+
- zones:
|
|
143
|
+
- ARGS
|
|
144
|
+
transform:
|
|
145
|
+
- lowercase
|
|
146
|
+
match:
|
|
147
|
+
type: contains
|
|
148
|
+
value: '${"freemarker.template.utility.Execute"?new()("cat /etc/hosts")}'
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
✅ **For Authentication Bypass**:
|
|
152
|
+
- Detect requests to sensitive admin endpoints (e.g., `/api/admin/login`).
|
|
153
|
+
- Check for unexpected methods like `PUT` or `POST` in places where they should not be allowed.
|
|
154
|
+
|
|
155
|
+
✅ **For SQL Injections**:
|
|
156
|
+
- Always target a specific argument by using `zone` and `variables`.
|
|
157
|
+
- Detect SQL metacharacters ("') inside parameters.
|
|
158
|
+
- When possible, use negative matches instead of looking for SQL keywords (e.g., `# matches "[^0-9]"`).
|
|
159
|
+
- Avoid using libinjectionSQL unless specifically asked.
|
|
160
|
+
|
|
161
|
+
✅ **For XSS / Cross Site Scripting**:
|
|
162
|
+
- Always target a specific argument by using `zone` and `variables`.
|
|
163
|
+
- Detect HTML metacharacters ("'<) inside parameters.
|
|
164
|
+
- Use simple patterns instead of looking for Javascript keywords keywords (e.g., `# contains "<"`)
|
|
165
|
+
- Avoid using libinjectionXSS unless specifically asked.
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
#GOOD:
|
|
169
|
+
- zones:
|
|
170
|
+
- ARGS
|
|
171
|
+
variables:
|
|
172
|
+
- where
|
|
173
|
+
transform:
|
|
174
|
+
- lowercase
|
|
175
|
+
- urldecode
|
|
176
|
+
match:
|
|
177
|
+
type: contains
|
|
178
|
+
value: '<'
|
|
179
|
+
#BAD:
|
|
180
|
+
- zones:
|
|
181
|
+
- ARGS
|
|
182
|
+
transform:
|
|
183
|
+
- lowercase
|
|
184
|
+
- urldecode
|
|
185
|
+
match:
|
|
186
|
+
type: contains
|
|
187
|
+
value: '<script>'
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
✅ **For Exploit that use `application/json` Content-Type**:
|
|
191
|
+
- prefix all variable names with `json.`
|
|
192
|
+
|
|
193
|
+
✅ **For Exploit that are using several successive requests**:
|
|
194
|
+
- Only match the most relevant URI, do not try to make a rule that matches multiple URLs.
|
|
195
|
+
- ANY RULE THAT USE BOTH `AND` AND `OR` WILL MAKE THE TASK FAIL.
|
|
196
|
+
```
|
|
197
|
+
#GOOD:
|
|
198
|
+
- and:
|
|
199
|
+
- zones:
|
|
200
|
+
- URI
|
|
201
|
+
transform:
|
|
202
|
+
- lowercase
|
|
203
|
+
- urldecode
|
|
204
|
+
match:
|
|
205
|
+
type: contains
|
|
206
|
+
#the interesting url is /src/addressbook.php
|
|
207
|
+
value: '/src/addressbook.php'
|
|
208
|
+
#BAD:
|
|
209
|
+
- or:
|
|
210
|
+
- and:
|
|
211
|
+
- zones:
|
|
212
|
+
- URI
|
|
213
|
+
transform:
|
|
214
|
+
- lowercase
|
|
215
|
+
- urldecode
|
|
216
|
+
match:
|
|
217
|
+
type: contains
|
|
218
|
+
value: '/src/addressbook.php'
|
|
219
|
+
- and:
|
|
220
|
+
- zones:
|
|
221
|
+
- URI
|
|
222
|
+
transform:
|
|
223
|
+
- lowercase
|
|
224
|
+
- urldecode
|
|
225
|
+
match:
|
|
226
|
+
type: contains
|
|
227
|
+
value: '/src/options.php'
|
|
228
|
+
```
|
|
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
|
+
### Output Format with Delimiters:
|
|
240
|
+
|
|
241
|
+
Output format (use the exact delimiters):
|
|
242
|
+
```
|
|
243
|
+
# <RULE TITLE>
|
|
244
|
+
|
|
245
|
+
## Overview
|
|
246
|
+
|
|
247
|
+
<1–2 sentence summary>
|
|
248
|
+
|
|
249
|
+
## WAF Rule
|
|
250
|
+
|
|
251
|
+
```yaml
|
|
252
|
+
<final WAF Rule YAML>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Validation
|
|
256
|
+
|
|
257
|
+
### Format Validation
|
|
258
|
+
|
|
259
|
+
- <verbatim schema validation output>
|
|
260
|
+
|
|
261
|
+
### Lint
|
|
262
|
+
|
|
263
|
+
- <verbatim output from linter tool>
|
|
264
|
+
|
|
265
|
+
## Next Steps
|
|
266
|
+
- Do you want me to test the WAF rule
|
|
267
|
+
- Do you want some deployment guidance
|
|
268
|
+
```
|
|
269
|
+
|
|
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
|
+
### Example Input (Nuclei Template):
|
|
285
|
+
```yaml
|
|
286
|
+
id: CVE-2025-24893
|
|
287
|
+
|
|
288
|
+
info:
|
|
289
|
+
name: XWiki Platform - Remote Code Execution
|
|
290
|
+
author: iamnoooob,rootxharsh,pdresearch
|
|
291
|
+
severity: critical
|
|
292
|
+
description: |
|
|
293
|
+
Any guest can perform arbitrary remote code execution through a request to SolrSearch. This impacts the confidentiality, integrity, and availability of the whole XWiki installation. This vulnerability has been patched in XWiki 15.10.11, 16.4.1, and 16.5.0RC1.
|
|
294
|
+
impact: |
|
|
295
|
+
An attacker can execute arbitrary code on the server, leading to a complete compromise of the XWiki instance.
|
|
296
|
+
|
|
297
|
+
http:
|
|
298
|
+
- method: GET
|
|
299
|
+
path:
|
|
300
|
+
- "{{BaseURL}}/bin/get/Main/SolrSearch?media=rss&text=%7d%7d%7d%7b%7basync%20async%3dfalse%7d%7d%7b%7bgroovy%7d%7dprintln(%22cat%20/etc/passwd%22.execute().text)%7b%7b%2fgroovy%7d%7d%7b%7b%2fasync%7d%7d%20"
|
|
301
|
+
|
|
302
|
+
skip-variables-check: true
|
|
303
|
+
matchers-condition: and
|
|
304
|
+
matchers:
|
|
305
|
+
- type: status
|
|
306
|
+
status:
|
|
307
|
+
- 200
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Example Output (Detection Rule):
|
|
311
|
+
===RULE===
|
|
312
|
+
name: crowdsecurity/vpatch-CVE-2025-24893
|
|
313
|
+
description: 'Detects arbitrary remote code execution vulnerability in XWiki via SolrSearch.'
|
|
314
|
+
rules:
|
|
315
|
+
- and:
|
|
316
|
+
- zones:
|
|
317
|
+
- URI
|
|
318
|
+
transform:
|
|
319
|
+
- lowercase
|
|
320
|
+
match:
|
|
321
|
+
type: contains
|
|
322
|
+
value: '/bin/get/main/solrsearch'
|
|
323
|
+
- zones:
|
|
324
|
+
- ARGS
|
|
325
|
+
variables:
|
|
326
|
+
- text
|
|
327
|
+
transform:
|
|
328
|
+
- lowercase
|
|
329
|
+
match:
|
|
330
|
+
type: contains
|
|
331
|
+
value: 'execute('
|
|
332
|
+
|
|
333
|
+
labels:
|
|
334
|
+
type: exploit
|
|
335
|
+
service: http
|
|
336
|
+
confidence: 3
|
|
337
|
+
spoofable: 0
|
|
338
|
+
behavior: 'http:exploit'
|
|
339
|
+
label: 'XWiki - RCE'
|
|
340
|
+
classification:
|
|
341
|
+
- cve.CVE-2025-24893
|
|
342
|
+
- attack.T1190
|
|
343
|
+
- cwe.CWE-95
|
|
@@ -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()
|