crucible-mcp 0.1.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.
- crucible/__init__.py +3 -0
- crucible/cli.py +523 -0
- crucible/domain/__init__.py +5 -0
- crucible/domain/detection.py +67 -0
- crucible/errors.py +50 -0
- crucible/knowledge/__init__.py +1 -0
- crucible/knowledge/loader.py +141 -0
- crucible/models.py +61 -0
- crucible/server.py +376 -0
- crucible/synthesis/__init__.py +1 -0
- crucible/tools/__init__.py +21 -0
- crucible/tools/delegation.py +326 -0
- crucible_mcp-0.1.0.dist-info/METADATA +158 -0
- crucible_mcp-0.1.0.dist-info/RECORD +17 -0
- crucible_mcp-0.1.0.dist-info/WHEEL +5 -0
- crucible_mcp-0.1.0.dist-info/entry_points.txt +3 -0
- crucible_mcp-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""Tool delegation - shell out to static analysis tools."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from crucible.errors import Result, err, ok
|
|
10
|
+
from crucible.models import Domain, Severity, ToolFinding
|
|
11
|
+
|
|
12
|
+
# Semgrep configs by domain
|
|
13
|
+
SEMGREP_CONFIGS: dict[Domain, list[str]] = {
|
|
14
|
+
Domain.SMART_CONTRACT: ["p/smart-contracts", "p/solidity"],
|
|
15
|
+
Domain.FRONTEND: ["p/javascript", "p/typescript", "p/react"],
|
|
16
|
+
Domain.BACKEND: ["p/python", "p/golang", "p/rust"],
|
|
17
|
+
Domain.INFRASTRUCTURE: ["p/terraform", "p/dockerfile", "p/kubernetes"],
|
|
18
|
+
Domain.UNKNOWN: ["auto"],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class ToolStatus:
|
|
24
|
+
"""Status of an external tool."""
|
|
25
|
+
|
|
26
|
+
name: str
|
|
27
|
+
installed: bool
|
|
28
|
+
path: str | None
|
|
29
|
+
version: str | None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def check_tool(name: str) -> ToolStatus:
|
|
33
|
+
"""Check if a tool is installed and get its version."""
|
|
34
|
+
path = shutil.which(name)
|
|
35
|
+
if not path:
|
|
36
|
+
return ToolStatus(name=name, installed=False, path=None, version=None)
|
|
37
|
+
|
|
38
|
+
# Try to get version
|
|
39
|
+
version = None
|
|
40
|
+
try:
|
|
41
|
+
if name == "semgrep":
|
|
42
|
+
result = subprocess.run([name, "--version"], capture_output=True, text=True, timeout=5)
|
|
43
|
+
version = result.stdout.strip().split("\n")[0] if result.returncode == 0 else None
|
|
44
|
+
elif name == "ruff" or name == "slither":
|
|
45
|
+
result = subprocess.run([name, "--version"], capture_output=True, text=True, timeout=5)
|
|
46
|
+
version = result.stdout.strip() if result.returncode == 0 else None
|
|
47
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
return ToolStatus(name=name, installed=True, path=path, version=version)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check_all_tools() -> dict[str, ToolStatus]:
|
|
54
|
+
"""Check status of all supported tools."""
|
|
55
|
+
tools = ["semgrep", "ruff", "slither", "bandit"]
|
|
56
|
+
return {name: check_tool(name) for name in tools}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_semgrep_config(domain: Domain) -> str:
|
|
60
|
+
"""Get the appropriate semgrep config for a domain."""
|
|
61
|
+
configs = SEMGREP_CONFIGS.get(domain, SEMGREP_CONFIGS[Domain.UNKNOWN])
|
|
62
|
+
# Join multiple configs with --config flags handled by semgrep
|
|
63
|
+
return configs[0] if configs else "auto"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _severity_from_semgrep(level: str) -> Severity:
|
|
67
|
+
"""Map semgrep severity to our Severity enum."""
|
|
68
|
+
mapping = {
|
|
69
|
+
"ERROR": Severity.HIGH,
|
|
70
|
+
"WARNING": Severity.MEDIUM,
|
|
71
|
+
"INFO": Severity.INFO,
|
|
72
|
+
}
|
|
73
|
+
return mapping.get(level.upper(), Severity.INFO)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def delegate_semgrep(
|
|
77
|
+
path: str,
|
|
78
|
+
config: str = "auto",
|
|
79
|
+
timeout: int = 120,
|
|
80
|
+
) -> Result[list[ToolFinding], str]:
|
|
81
|
+
"""
|
|
82
|
+
Run semgrep on a file or directory.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
path: File or directory to scan
|
|
86
|
+
config: Semgrep config (auto, p/python, p/javascript, etc.)
|
|
87
|
+
timeout: Timeout in seconds
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Result containing list of findings or error message
|
|
91
|
+
"""
|
|
92
|
+
if not Path(path).exists():
|
|
93
|
+
return err(f"Path does not exist: {path}")
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
result = subprocess.run(
|
|
97
|
+
["semgrep", "--config", config, "--json", "--quiet", path],
|
|
98
|
+
capture_output=True,
|
|
99
|
+
text=True,
|
|
100
|
+
timeout=timeout,
|
|
101
|
+
)
|
|
102
|
+
except FileNotFoundError:
|
|
103
|
+
return err("semgrep not found. Install with: pip install semgrep")
|
|
104
|
+
except subprocess.TimeoutExpired:
|
|
105
|
+
return err(f"semgrep timed out after {timeout}s")
|
|
106
|
+
|
|
107
|
+
if result.returncode not in (0, 1): # 1 means findings found
|
|
108
|
+
return err(f"semgrep failed: {result.stderr}")
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
output = json.loads(result.stdout) if result.stdout else {"results": []}
|
|
112
|
+
except json.JSONDecodeError as e:
|
|
113
|
+
return err(f"Failed to parse semgrep output: {e}")
|
|
114
|
+
|
|
115
|
+
findings: list[ToolFinding] = []
|
|
116
|
+
for r in output.get("results", []):
|
|
117
|
+
finding = ToolFinding(
|
|
118
|
+
tool="semgrep",
|
|
119
|
+
rule=r.get("check_id", "unknown"),
|
|
120
|
+
severity=_severity_from_semgrep(r.get("extra", {}).get("severity", "INFO")),
|
|
121
|
+
message=r.get("extra", {}).get("message", r.get("check_id", "")),
|
|
122
|
+
location=f"{r.get('path', '?')}:{r.get('start', {}).get('line', '?')}",
|
|
123
|
+
suggestion=r.get("extra", {}).get("fix", None),
|
|
124
|
+
)
|
|
125
|
+
findings.append(finding)
|
|
126
|
+
|
|
127
|
+
return ok(findings)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def delegate_ruff(
|
|
131
|
+
path: str,
|
|
132
|
+
timeout: int = 60,
|
|
133
|
+
) -> Result[list[ToolFinding], str]:
|
|
134
|
+
"""
|
|
135
|
+
Run ruff on a Python file or directory.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
path: File or directory to scan
|
|
139
|
+
timeout: Timeout in seconds
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Result containing list of findings or error message
|
|
143
|
+
"""
|
|
144
|
+
if not Path(path).exists():
|
|
145
|
+
return err(f"Path does not exist: {path}")
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
result = subprocess.run(
|
|
149
|
+
["ruff", "check", "--output-format=json", path],
|
|
150
|
+
capture_output=True,
|
|
151
|
+
text=True,
|
|
152
|
+
timeout=timeout,
|
|
153
|
+
)
|
|
154
|
+
except FileNotFoundError:
|
|
155
|
+
return err("ruff not found. Install with: pip install ruff")
|
|
156
|
+
except subprocess.TimeoutExpired:
|
|
157
|
+
return err(f"ruff timed out after {timeout}s")
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
output = json.loads(result.stdout) if result.stdout else []
|
|
161
|
+
except json.JSONDecodeError as e:
|
|
162
|
+
return err(f"Failed to parse ruff output: {e}")
|
|
163
|
+
|
|
164
|
+
findings: list[ToolFinding] = []
|
|
165
|
+
for r in output:
|
|
166
|
+
finding = ToolFinding(
|
|
167
|
+
tool="ruff",
|
|
168
|
+
rule=r.get("code", "unknown"),
|
|
169
|
+
severity=_severity_from_ruff(r.get("code", "")),
|
|
170
|
+
message=r.get("message", ""),
|
|
171
|
+
location=f"{r.get('filename', '?')}:{r.get('location', {}).get('row', '?')}",
|
|
172
|
+
suggestion=r.get("fix", {}).get("message") if r.get("fix") else None,
|
|
173
|
+
)
|
|
174
|
+
findings.append(finding)
|
|
175
|
+
|
|
176
|
+
return ok(findings)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _severity_from_ruff(code: str) -> Severity:
|
|
180
|
+
"""Map ruff rule codes to severity based on category."""
|
|
181
|
+
if not code:
|
|
182
|
+
return Severity.LOW
|
|
183
|
+
|
|
184
|
+
# Security-related rules are higher severity
|
|
185
|
+
# S = bandit (security), T = flake8-print (debug code)
|
|
186
|
+
if code.startswith("S"):
|
|
187
|
+
# S1xx = security issues
|
|
188
|
+
if code.startswith("S1"):
|
|
189
|
+
return Severity.HIGH
|
|
190
|
+
return Severity.MEDIUM
|
|
191
|
+
|
|
192
|
+
# Error-prone patterns
|
|
193
|
+
# B = bugbear, E9xx = syntax errors, F = pyflakes
|
|
194
|
+
if code.startswith("B") or code.startswith("E9") or code.startswith("F"):
|
|
195
|
+
return Severity.MEDIUM
|
|
196
|
+
|
|
197
|
+
# Everything else is low (style, formatting)
|
|
198
|
+
return Severity.LOW
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def delegate_bandit(
|
|
202
|
+
path: str,
|
|
203
|
+
timeout: int = 60,
|
|
204
|
+
) -> Result[list[ToolFinding], str]:
|
|
205
|
+
"""
|
|
206
|
+
Run bandit on a Python file or directory.
|
|
207
|
+
|
|
208
|
+
Bandit catches hardcoded secrets (B105, B106, B107) that semgrep's
|
|
209
|
+
p/bandit config doesn't include.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
path: File or directory to scan
|
|
213
|
+
timeout: Timeout in seconds
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Result containing list of findings or error message
|
|
217
|
+
"""
|
|
218
|
+
if not Path(path).exists():
|
|
219
|
+
return err(f"Path does not exist: {path}")
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
result = subprocess.run(
|
|
223
|
+
["bandit", "-f", "json", "-r", path],
|
|
224
|
+
capture_output=True,
|
|
225
|
+
text=True,
|
|
226
|
+
timeout=timeout,
|
|
227
|
+
)
|
|
228
|
+
except FileNotFoundError:
|
|
229
|
+
return err("bandit not found. Install with: pip install bandit")
|
|
230
|
+
except subprocess.TimeoutExpired:
|
|
231
|
+
return err(f"bandit timed out after {timeout}s")
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
output = json.loads(result.stdout) if result.stdout else {"results": []}
|
|
235
|
+
except json.JSONDecodeError as e:
|
|
236
|
+
return err(f"Failed to parse bandit output: {e}")
|
|
237
|
+
|
|
238
|
+
# Map bandit severity to our Severity
|
|
239
|
+
severity_map = {
|
|
240
|
+
"HIGH": Severity.HIGH,
|
|
241
|
+
"MEDIUM": Severity.MEDIUM,
|
|
242
|
+
"LOW": Severity.LOW,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
findings: list[ToolFinding] = []
|
|
246
|
+
for r in output.get("results", []):
|
|
247
|
+
finding = ToolFinding(
|
|
248
|
+
tool="bandit",
|
|
249
|
+
rule=r.get("test_id", "unknown"),
|
|
250
|
+
severity=severity_map.get(r.get("issue_severity", ""), Severity.INFO),
|
|
251
|
+
message=r.get("issue_text", ""),
|
|
252
|
+
location=f"{r.get('filename', '?')}:{r.get('line_number', '?')}",
|
|
253
|
+
suggestion=None,
|
|
254
|
+
)
|
|
255
|
+
findings.append(finding)
|
|
256
|
+
|
|
257
|
+
return ok(findings)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def delegate_slither(
|
|
261
|
+
path: str,
|
|
262
|
+
detectors: list[str] | None = None,
|
|
263
|
+
timeout: int = 300,
|
|
264
|
+
) -> Result[list[ToolFinding], str]:
|
|
265
|
+
"""
|
|
266
|
+
Run slither on a Solidity file or project.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
path: File or directory to scan
|
|
270
|
+
detectors: Specific detectors to run (None = all)
|
|
271
|
+
timeout: Timeout in seconds
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Result containing list of findings or error message
|
|
275
|
+
"""
|
|
276
|
+
if not Path(path).exists():
|
|
277
|
+
return err(f"Path does not exist: {path}")
|
|
278
|
+
|
|
279
|
+
cmd = ["slither", path, "--json", "-"]
|
|
280
|
+
if detectors:
|
|
281
|
+
cmd.extend(["--detect", ",".join(detectors)])
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
result = subprocess.run(
|
|
285
|
+
cmd,
|
|
286
|
+
capture_output=True,
|
|
287
|
+
text=True,
|
|
288
|
+
timeout=timeout,
|
|
289
|
+
)
|
|
290
|
+
except FileNotFoundError:
|
|
291
|
+
return err("slither not found. Install with: pip install slither-analyzer")
|
|
292
|
+
except subprocess.TimeoutExpired:
|
|
293
|
+
return err(f"slither timed out after {timeout}s")
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
output = json.loads(result.stdout) if result.stdout else {"results": {"detectors": []}}
|
|
297
|
+
except json.JSONDecodeError as e:
|
|
298
|
+
return err(f"Failed to parse slither output: {e}")
|
|
299
|
+
|
|
300
|
+
# Map slither impact to severity
|
|
301
|
+
impact_map = {
|
|
302
|
+
"High": Severity.HIGH,
|
|
303
|
+
"Medium": Severity.MEDIUM,
|
|
304
|
+
"Low": Severity.LOW,
|
|
305
|
+
"Informational": Severity.INFO,
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
findings: list[ToolFinding] = []
|
|
309
|
+
for d in output.get("results", {}).get("detectors", []):
|
|
310
|
+
elements = d.get("elements", [])
|
|
311
|
+
location = "unknown"
|
|
312
|
+
if elements:
|
|
313
|
+
first = elements[0]
|
|
314
|
+
location = f"{first.get('source_mapping', {}).get('filename_relative', '?')}"
|
|
315
|
+
|
|
316
|
+
finding = ToolFinding(
|
|
317
|
+
tool="slither",
|
|
318
|
+
rule=d.get("check", "unknown"),
|
|
319
|
+
severity=impact_map.get(d.get("impact", ""), Severity.INFO),
|
|
320
|
+
message=d.get("description", ""),
|
|
321
|
+
location=location,
|
|
322
|
+
suggestion=None,
|
|
323
|
+
)
|
|
324
|
+
findings.append(finding)
|
|
325
|
+
|
|
326
|
+
return ok(findings)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crucible-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Code review MCP server for Claude. Not affiliated with Atlassian.
|
|
5
|
+
Author: be.nvy
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: mcp,code-review,static-analysis,claude
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: mcp>=1.0.0
|
|
11
|
+
Requires-Dist: pyyaml>=6.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
15
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
16
|
+
Requires-Dist: ruff>=0.3; extra == "dev"
|
|
17
|
+
|
|
18
|
+
# Crucible
|
|
19
|
+
|
|
20
|
+
Code review MCP server for Claude. Runs static analysis and loads review skills based on what kind of code you're looking at.
|
|
21
|
+
|
|
22
|
+
> **Note:** This project is not affiliated with Atlassian or their Crucible code review tool. Just an unfortunate naming collision.
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
26
|
+
│ Your Code ──→ Crucible ──→ Claude │
|
|
27
|
+
│ (analysis) (synthesis) │
|
|
28
|
+
│ │
|
|
29
|
+
│ .sol file ──→ slither, semgrep ──→ web3-engineer skill loaded │
|
|
30
|
+
│ .py file ──→ ruff, bandit ──→ backend-engineer skill loaded │
|
|
31
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**MCP provides data. Skills provide perspective. Claude orchestrates.**
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Install from PyPI
|
|
40
|
+
pip install crucible-mcp
|
|
41
|
+
|
|
42
|
+
# Or install from source
|
|
43
|
+
pip install -e ".[dev]"
|
|
44
|
+
|
|
45
|
+
# Install skills to ~/.claude/crucible/skills/
|
|
46
|
+
crucible skills install
|
|
47
|
+
|
|
48
|
+
# Install analysis tools for your stack
|
|
49
|
+
pip install semgrep ruff # Python
|
|
50
|
+
pip install slither-analyzer # Solidity
|
|
51
|
+
pip install bandit # Python security
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> **Tools are separate by design.** Different workflows need different analyzers. Install what you need, skip what you don't. Crucible gracefully handles missing tools.
|
|
55
|
+
|
|
56
|
+
## MCP Setup
|
|
57
|
+
|
|
58
|
+
Works with any MCP client (Claude Code, Cursor, etc.). Add to your `.mcp.json`:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"mcpServers": {
|
|
63
|
+
"crucible": {
|
|
64
|
+
"command": "crucible-mcp"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Then in Claude:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
Review src/Vault.sol
|
|
74
|
+
|
|
75
|
+
→ Crucible: domains_detected: [solidity, smart_contract, web3]
|
|
76
|
+
→ Crucible: severity_summary: {critical: 1, high: 3}
|
|
77
|
+
→ Claude loads: web3-engineer, security-engineer skills
|
|
78
|
+
→ Claude synthesizes multi-perspective review
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## MCP Tools
|
|
82
|
+
|
|
83
|
+
| Tool | Purpose |
|
|
84
|
+
|------|---------|
|
|
85
|
+
| `quick_review(path)` | Run analysis, return findings + domains |
|
|
86
|
+
| `get_principles(topic)` | Load engineering knowledge |
|
|
87
|
+
| `delegate_*` | Direct tool access (semgrep, ruff, slither, bandit) |
|
|
88
|
+
| `check_tools()` | Show installed analysis tools |
|
|
89
|
+
|
|
90
|
+
## CLI
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
crucible skills list # List all skills
|
|
94
|
+
crucible skills show <skill> # Show which version is active
|
|
95
|
+
crucible skills init <skill> # Copy to .crucible/ for customization
|
|
96
|
+
|
|
97
|
+
crucible knowledge list # List all knowledge files
|
|
98
|
+
crucible knowledge init <file> # Copy for customization
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## How It Works
|
|
102
|
+
|
|
103
|
+
Crucible detects what kind of code you're reviewing, runs the right analysis tools, and returns findings with domain metadata. Claude uses this to load appropriate review skills.
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
.sol file → slither + semgrep → web3-engineer, gas-optimizer skills
|
|
107
|
+
.py file → ruff + bandit → backend-engineer, security-engineer skills
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
See [ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full flow.
|
|
111
|
+
|
|
112
|
+
## Customization
|
|
113
|
+
|
|
114
|
+
Override skills and knowledge for your project or personal preferences:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Customize a skill for your project
|
|
118
|
+
crucible skills init security-engineer
|
|
119
|
+
# Creates .crucible/skills/security-engineer/SKILL.md
|
|
120
|
+
|
|
121
|
+
# Add project-specific concerns, team conventions, etc.
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Resolution order (first found wins):
|
|
125
|
+
1. `.crucible/` — Project overrides
|
|
126
|
+
2. `~/.claude/crucible/` — User preferences
|
|
127
|
+
3. Bundled — Package defaults
|
|
128
|
+
|
|
129
|
+
See [CUSTOMIZATION.md](docs/CUSTOMIZATION.md) for the full guide.
|
|
130
|
+
|
|
131
|
+
## What's Included
|
|
132
|
+
|
|
133
|
+
**18 Review Skills** — Different review perspectives (security, performance, accessibility, web3, etc.)
|
|
134
|
+
|
|
135
|
+
See [SKILLS.md](docs/SKILLS.md) for the full list with triggers and focus areas.
|
|
136
|
+
|
|
137
|
+
**12 Knowledge Files** — Engineering principles for security, testing, APIs, databases, smart contracts, etc.
|
|
138
|
+
|
|
139
|
+
See [KNOWLEDGE.md](docs/KNOWLEDGE.md) for topics covered and skill linkages.
|
|
140
|
+
|
|
141
|
+
## Documentation
|
|
142
|
+
|
|
143
|
+
| Doc | What's In It |
|
|
144
|
+
|-----|--------------|
|
|
145
|
+
| [FEATURES.md](docs/FEATURES.md) | Complete feature reference |
|
|
146
|
+
| [ARCHITECTURE.md](docs/ARCHITECTURE.md) | How MCP, tools, skills, and knowledge fit together |
|
|
147
|
+
| [CUSTOMIZATION.md](docs/CUSTOMIZATION.md) | Override skills and knowledge for your project |
|
|
148
|
+
| [SKILLS.md](docs/SKILLS.md) | All 18 review personas with triggers and key questions |
|
|
149
|
+
| [KNOWLEDGE.md](docs/KNOWLEDGE.md) | All 12 knowledge files with topics covered |
|
|
150
|
+
| [CONTRIBUTING.md](docs/CONTRIBUTING.md) | Adding tools, skills, and knowledge |
|
|
151
|
+
|
|
152
|
+
## Development
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pip install -e ".[dev]"
|
|
156
|
+
pytest # Run tests (263 tests)
|
|
157
|
+
ruff check src/ --fix # Lint
|
|
158
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
crucible/__init__.py,sha256=ZLZQWKmjTHaeeDijcOl3xmaEgoI2W3a8FCFwcieZGv0,77
|
|
2
|
+
crucible/cli.py,sha256=2-aHRpNoSyLSsiDPBYGyg-16dsylHL-opTVUpshKgAE,17149
|
|
3
|
+
crucible/errors.py,sha256=HrX_yvJEhXJoKodXGo_iY9wqx2J3ONYy0a_LbrVC5As,819
|
|
4
|
+
crucible/models.py,sha256=9bEsNUvKcmq0r_-pCiZ8lv7RaelDQcPgiDia5eghxl4,1624
|
|
5
|
+
crucible/server.py,sha256=9_rxelihpZEpYc4axoRNP5rh_PsuzJZpj6qwCdBtz4M,12857
|
|
6
|
+
crucible/domain/__init__.py,sha256=2fsoB5wH2Pl3vtGRt4voYOSZ04-zLoW8pNq6nvzVMgU,118
|
|
7
|
+
crucible/domain/detection.py,sha256=TNeLB_VQgS1AsT5BKDf_tIpGa47THrFoRXwU4u54VB0,1797
|
|
8
|
+
crucible/knowledge/__init__.py,sha256=unb7kyO1MtB3Zt-TGx_O8LE79KyrGrNHoFFHgUWUvGU,40
|
|
9
|
+
crucible/knowledge/loader.py,sha256=VuMSZCEkKTP0vTGkju9tHIzokXfq5RRWHYjSo54WDPs,4525
|
|
10
|
+
crucible/synthesis/__init__.py,sha256=CYrkZG4bdAjp8XdOh1smfKscd3YU5lZlaDLGwLE9c-0,46
|
|
11
|
+
crucible/tools/__init__.py,sha256=gFRThTk1E-fHzpe8bB5rtBG6Z6G-ysPzjVEHfKGbEYU,400
|
|
12
|
+
crucible/tools/delegation.py,sha256=A5NM7snJOptwhuaqxfMfPmVoAvWjzpyx3vZoyvRIh_A,10183
|
|
13
|
+
crucible_mcp-0.1.0.dist-info/METADATA,sha256=-wOJb1OYlrxQ3Z872Nc3EwIG0KwHkeImrsezKV1cZNM,5529
|
|
14
|
+
crucible_mcp-0.1.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
15
|
+
crucible_mcp-0.1.0.dist-info/entry_points.txt,sha256=18BZaH1OlFSFYtKuHq0Z8yYX8Wmx7Ikfqay-P00ZX3Q,83
|
|
16
|
+
crucible_mcp-0.1.0.dist-info/top_level.txt,sha256=4hzuFgqbFPOO-WiU_DYxTm8VYIxTXh7Wlp0gRcWR0Cs,9
|
|
17
|
+
crucible_mcp-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
crucible
|