crucible-mcp 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crucible/cli.py +1054 -7
- crucible/hooks/__init__.py +15 -0
- crucible/hooks/precommit.py +660 -0
- crucible/knowledge/loader.py +70 -1
- crucible/models.py +15 -0
- crucible/server.py +599 -4
- crucible/skills/__init__.py +23 -0
- crucible/skills/loader.py +281 -0
- crucible/tools/delegation.py +96 -10
- crucible/tools/git.py +317 -0
- crucible_mcp-0.2.0.dist-info/METADATA +140 -0
- crucible_mcp-0.2.0.dist-info/RECORD +22 -0
- {crucible_mcp-0.1.0.dist-info → crucible_mcp-0.2.0.dist-info}/WHEEL +1 -1
- crucible_mcp-0.1.0.dist-info/METADATA +0 -158
- crucible_mcp-0.1.0.dist-info/RECORD +0 -17
- {crucible_mcp-0.1.0.dist-info → crucible_mcp-0.2.0.dist-info}/entry_points.txt +0 -0
- {crucible_mcp-0.1.0.dist-info → crucible_mcp-0.2.0.dist-info}/top_level.txt +0 -0
crucible/tools/git.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Git operations for change-aware code review."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
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
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class LineRange:
|
|
14
|
+
"""A range of lines in a file."""
|
|
15
|
+
|
|
16
|
+
start: int
|
|
17
|
+
end: int
|
|
18
|
+
|
|
19
|
+
def contains(self, line: int) -> bool:
|
|
20
|
+
"""Check if a line number is within this range."""
|
|
21
|
+
return self.start <= line <= self.end
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class GitChange:
|
|
26
|
+
"""A changed file in git with its modified line ranges."""
|
|
27
|
+
|
|
28
|
+
path: str
|
|
29
|
+
status: str # A=added, M=modified, D=deleted, R=renamed
|
|
30
|
+
added_lines: tuple[LineRange, ...]
|
|
31
|
+
old_path: str | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class GitContext:
|
|
36
|
+
"""Context about git changes for a review."""
|
|
37
|
+
|
|
38
|
+
mode: str
|
|
39
|
+
base_ref: str | None
|
|
40
|
+
changes: tuple[GitChange, ...]
|
|
41
|
+
commit_messages: tuple[str, ...] | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_git_repo(path: str | Path) -> bool:
|
|
45
|
+
"""Check if the path is inside a git repository."""
|
|
46
|
+
try:
|
|
47
|
+
result = subprocess.run(
|
|
48
|
+
["git", "rev-parse", "--git-dir"],
|
|
49
|
+
cwd=str(path),
|
|
50
|
+
capture_output=True,
|
|
51
|
+
text=True,
|
|
52
|
+
timeout=5,
|
|
53
|
+
)
|
|
54
|
+
return result.returncode == 0
|
|
55
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_repo_root(path: str | Path) -> Result[str, str]:
|
|
60
|
+
"""Get the root directory of the git repository."""
|
|
61
|
+
if not shutil.which("git"):
|
|
62
|
+
return err("git not found")
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
result = subprocess.run(
|
|
66
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
67
|
+
cwd=str(path),
|
|
68
|
+
capture_output=True,
|
|
69
|
+
text=True,
|
|
70
|
+
timeout=5,
|
|
71
|
+
)
|
|
72
|
+
if result.returncode != 0:
|
|
73
|
+
return err("Not a git repository")
|
|
74
|
+
return ok(result.stdout.strip())
|
|
75
|
+
except subprocess.TimeoutExpired:
|
|
76
|
+
return err("git command timed out")
|
|
77
|
+
except OSError as e:
|
|
78
|
+
return err(f"Failed to run git: {e}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _run_git(args: list[str], cwd: str, timeout: int = 30) -> Result[str, str]:
|
|
82
|
+
"""Run a git command and return stdout or error."""
|
|
83
|
+
if not shutil.which("git"):
|
|
84
|
+
return err("git not found")
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
result = subprocess.run(
|
|
88
|
+
["git", *args],
|
|
89
|
+
cwd=cwd,
|
|
90
|
+
capture_output=True,
|
|
91
|
+
text=True,
|
|
92
|
+
timeout=timeout,
|
|
93
|
+
)
|
|
94
|
+
if result.returncode != 0:
|
|
95
|
+
stderr = result.stderr.strip()
|
|
96
|
+
if "unknown revision" in stderr.lower() or "bad revision" in stderr.lower():
|
|
97
|
+
return err(f"Unknown ref: {stderr}")
|
|
98
|
+
return err(stderr or f"git {args[0]} failed")
|
|
99
|
+
return ok(result.stdout)
|
|
100
|
+
except subprocess.TimeoutExpired:
|
|
101
|
+
return err(f"git command timed out after {timeout}s")
|
|
102
|
+
except OSError as e:
|
|
103
|
+
return err(f"Failed to run git: {e}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def parse_diff_line_ranges(diff_output: str) -> dict[str, list[LineRange]]:
|
|
107
|
+
"""
|
|
108
|
+
Parse unified diff output to extract added line ranges per file.
|
|
109
|
+
|
|
110
|
+
Returns a dict mapping file paths to lists of LineRange for added/modified lines.
|
|
111
|
+
"""
|
|
112
|
+
result: dict[str, list[LineRange]] = {}
|
|
113
|
+
current_file: str | None = None
|
|
114
|
+
|
|
115
|
+
# Match diff header: +++ b/path/to/file
|
|
116
|
+
file_pattern = re.compile(r"^\+\+\+ b/(.+)$")
|
|
117
|
+
# Match hunk header: @@ -old_start,old_count +new_start,new_count @@
|
|
118
|
+
hunk_pattern = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")
|
|
119
|
+
|
|
120
|
+
lines = diff_output.split("\n")
|
|
121
|
+
i = 0
|
|
122
|
+
while i < len(lines):
|
|
123
|
+
line = lines[i]
|
|
124
|
+
|
|
125
|
+
# Check for new file
|
|
126
|
+
file_match = file_pattern.match(line)
|
|
127
|
+
if file_match:
|
|
128
|
+
current_file = file_match.group(1)
|
|
129
|
+
if current_file not in result:
|
|
130
|
+
result[current_file] = []
|
|
131
|
+
i += 1
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
# Check for hunk header
|
|
135
|
+
hunk_match = hunk_pattern.match(line)
|
|
136
|
+
if hunk_match and current_file:
|
|
137
|
+
new_start = int(hunk_match.group(1))
|
|
138
|
+
|
|
139
|
+
# Track which lines within this hunk are additions
|
|
140
|
+
i += 1
|
|
141
|
+
current_line = new_start
|
|
142
|
+
hunk_added_lines: list[int] = []
|
|
143
|
+
|
|
144
|
+
while i < len(lines):
|
|
145
|
+
hunk_line = lines[i]
|
|
146
|
+
if hunk_line.startswith("@@") or hunk_line.startswith("diff "):
|
|
147
|
+
break
|
|
148
|
+
if hunk_line.startswith("+") and not hunk_line.startswith("+++"):
|
|
149
|
+
hunk_added_lines.append(current_line)
|
|
150
|
+
current_line += 1
|
|
151
|
+
elif hunk_line.startswith("-") and not hunk_line.startswith("---"):
|
|
152
|
+
pass # Deleted line, don't increment current_line
|
|
153
|
+
else:
|
|
154
|
+
current_line += 1
|
|
155
|
+
i += 1
|
|
156
|
+
|
|
157
|
+
# Convert consecutive lines to ranges
|
|
158
|
+
if hunk_added_lines:
|
|
159
|
+
ranges = _lines_to_ranges(hunk_added_lines)
|
|
160
|
+
result[current_file].extend(ranges)
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
i += 1
|
|
164
|
+
|
|
165
|
+
return result
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _lines_to_ranges(lines: list[int]) -> list[LineRange]:
|
|
169
|
+
"""Convert a sorted list of line numbers to LineRange objects."""
|
|
170
|
+
if not lines:
|
|
171
|
+
return []
|
|
172
|
+
|
|
173
|
+
ranges: list[LineRange] = []
|
|
174
|
+
start = lines[0]
|
|
175
|
+
end = lines[0]
|
|
176
|
+
|
|
177
|
+
for line in lines[1:]:
|
|
178
|
+
if line == end + 1:
|
|
179
|
+
end = line
|
|
180
|
+
else:
|
|
181
|
+
ranges.append(LineRange(start=start, end=end))
|
|
182
|
+
start = line
|
|
183
|
+
end = line
|
|
184
|
+
|
|
185
|
+
ranges.append(LineRange(start=start, end=end))
|
|
186
|
+
return ranges
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def get_staged_changes(repo_path: str) -> Result[GitContext, str]:
|
|
190
|
+
"""Get staged changes (ready to commit)."""
|
|
191
|
+
# Get diff of staged changes
|
|
192
|
+
diff_result = _run_git(["diff", "--cached", "--unified=0"], repo_path)
|
|
193
|
+
if diff_result.is_err:
|
|
194
|
+
return err(diff_result.error)
|
|
195
|
+
|
|
196
|
+
# Get list of staged files with status
|
|
197
|
+
status_result = _run_git(["diff", "--cached", "--name-status"], repo_path)
|
|
198
|
+
if status_result.is_err:
|
|
199
|
+
return err(status_result.error)
|
|
200
|
+
|
|
201
|
+
changes = _parse_changes(diff_result.value, status_result.value)
|
|
202
|
+
return ok(GitContext(mode="staged", base_ref=None, changes=tuple(changes)))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def get_unstaged_changes(repo_path: str) -> Result[GitContext, str]:
|
|
206
|
+
"""Get unstaged changes (working directory)."""
|
|
207
|
+
diff_result = _run_git(["diff", "--unified=0"], repo_path)
|
|
208
|
+
if diff_result.is_err:
|
|
209
|
+
return err(diff_result.error)
|
|
210
|
+
|
|
211
|
+
status_result = _run_git(["diff", "--name-status"], repo_path)
|
|
212
|
+
if status_result.is_err:
|
|
213
|
+
return err(status_result.error)
|
|
214
|
+
|
|
215
|
+
changes = _parse_changes(diff_result.value, status_result.value)
|
|
216
|
+
return ok(GitContext(mode="unstaged", base_ref=None, changes=tuple(changes)))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def get_branch_diff(repo_path: str, base: str = "main") -> Result[GitContext, str]:
|
|
220
|
+
"""Get diff between current branch and base branch."""
|
|
221
|
+
# Use three-dot diff to show changes since branching
|
|
222
|
+
diff_result = _run_git(["diff", f"{base}...HEAD", "--unified=0"], repo_path)
|
|
223
|
+
if diff_result.is_err:
|
|
224
|
+
return err(diff_result.error)
|
|
225
|
+
|
|
226
|
+
status_result = _run_git(["diff", f"{base}...HEAD", "--name-status"], repo_path)
|
|
227
|
+
if status_result.is_err:
|
|
228
|
+
return err(status_result.error)
|
|
229
|
+
|
|
230
|
+
# Get commit messages since base
|
|
231
|
+
log_result = _run_git(
|
|
232
|
+
["log", f"{base}..HEAD", "--pretty=format:%s", "--reverse"], repo_path
|
|
233
|
+
)
|
|
234
|
+
commit_messages = None
|
|
235
|
+
if log_result.is_ok and log_result.value.strip():
|
|
236
|
+
commit_messages = tuple(log_result.value.strip().split("\n"))
|
|
237
|
+
|
|
238
|
+
changes = _parse_changes(diff_result.value, status_result.value)
|
|
239
|
+
return ok(
|
|
240
|
+
GitContext(
|
|
241
|
+
mode="branch",
|
|
242
|
+
base_ref=base,
|
|
243
|
+
changes=tuple(changes),
|
|
244
|
+
commit_messages=commit_messages,
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def get_recent_commits(repo_path: str, count: int = 1) -> Result[GitContext, str]:
|
|
250
|
+
"""Get changes from recent N commits."""
|
|
251
|
+
if count < 1:
|
|
252
|
+
return err("Commit count must be at least 1")
|
|
253
|
+
|
|
254
|
+
diff_result = _run_git(["diff", f"HEAD~{count}..HEAD", "--unified=0"], repo_path)
|
|
255
|
+
if diff_result.is_err:
|
|
256
|
+
return err(diff_result.error)
|
|
257
|
+
|
|
258
|
+
status_result = _run_git(["diff", f"HEAD~{count}..HEAD", "--name-status"], repo_path)
|
|
259
|
+
if status_result.is_err:
|
|
260
|
+
return err(status_result.error)
|
|
261
|
+
|
|
262
|
+
# Get commit messages
|
|
263
|
+
log_result = _run_git(
|
|
264
|
+
["log", f"HEAD~{count}..HEAD", "--pretty=format:%s", "--reverse"], repo_path
|
|
265
|
+
)
|
|
266
|
+
commit_messages = None
|
|
267
|
+
if log_result.is_ok and log_result.value.strip():
|
|
268
|
+
commit_messages = tuple(log_result.value.strip().split("\n"))
|
|
269
|
+
|
|
270
|
+
changes = _parse_changes(diff_result.value, status_result.value)
|
|
271
|
+
return ok(
|
|
272
|
+
GitContext(
|
|
273
|
+
mode="commits",
|
|
274
|
+
base_ref=f"HEAD~{count}",
|
|
275
|
+
changes=tuple(changes),
|
|
276
|
+
commit_messages=commit_messages,
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _parse_changes(diff_output: str, status_output: str) -> list[GitChange]:
|
|
282
|
+
"""Parse git diff and status output into GitChange objects."""
|
|
283
|
+
# Parse line ranges from diff
|
|
284
|
+
line_ranges = parse_diff_line_ranges(diff_output)
|
|
285
|
+
|
|
286
|
+
# Parse file statuses
|
|
287
|
+
changes: list[GitChange] = []
|
|
288
|
+
for line in status_output.strip().split("\n"):
|
|
289
|
+
if not line:
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
parts = line.split("\t")
|
|
293
|
+
if len(parts) < 2:
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
status = parts[0][0] # First char of status (A, M, D, R)
|
|
297
|
+
path = parts[-1] # Last part is always the current path
|
|
298
|
+
old_path = parts[1] if status == "R" and len(parts) > 2 else None
|
|
299
|
+
|
|
300
|
+
# Get added lines for this file
|
|
301
|
+
added_lines = tuple(line_ranges.get(path, []))
|
|
302
|
+
|
|
303
|
+
changes.append(
|
|
304
|
+
GitChange(
|
|
305
|
+
path=path,
|
|
306
|
+
status=status,
|
|
307
|
+
added_lines=added_lines,
|
|
308
|
+
old_path=old_path,
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
return changes
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def get_changed_files(context: GitContext) -> list[str]:
|
|
316
|
+
"""Get list of changed file paths from a git context (excluding deleted)."""
|
|
317
|
+
return [c.path for c in context.changes if c.status != "D"]
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crucible-mcp
|
|
3
|
+
Version: 0.2.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
|
+
Load your coding patterns into Claude Code.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
├── Personas: Domain-specific thinking (how to approach problems)
|
|
24
|
+
├── Knowledge: Coding patterns and principles (what to apply)
|
|
25
|
+
├── Cascade: Project → User → Bundled (customizable at every level)
|
|
26
|
+
└── Context-aware: Loads relevant skills based on what you're working on
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Personas for domains. Knowledge for patterns. All customizable.**
|
|
30
|
+
|
|
31
|
+
> Not affiliated with Atlassian's Crucible.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install crucible-mcp
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Add to Claude Code (`.mcp.json`):
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"mcpServers": {
|
|
44
|
+
"crucible": {
|
|
45
|
+
"command": "crucible-mcp"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
With hot reload (recommended for customization):
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"crucible": {
|
|
57
|
+
"command": "mcpmon",
|
|
58
|
+
"args": ["--watch", "~/.crucible/", "--", "crucible-mcp"]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## How It Works
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
Code → Detect Domain → Load Personas + Knowledge → Claude with YOUR patterns
|
|
68
|
+
|
|
69
|
+
.sol file → web3 domain → security-engineer + SMART_CONTRACT.md → Knows your security rules
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## MCP Tools
|
|
73
|
+
|
|
74
|
+
| Tool | Purpose |
|
|
75
|
+
|------|---------|
|
|
76
|
+
| `quick_review(path)` | Run analysis, return findings + domains |
|
|
77
|
+
| `get_principles(topic)` | Load engineering knowledge |
|
|
78
|
+
| `delegate_*` | Direct tool access (semgrep, ruff, slither, bandit) |
|
|
79
|
+
| `check_tools()` | Show installed analysis tools |
|
|
80
|
+
|
|
81
|
+
## CLI
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
crucible skills list # List all skills
|
|
85
|
+
crucible skills show <skill> # Show which version is active
|
|
86
|
+
crucible skills init <skill> # Copy to .crucible/ for customization
|
|
87
|
+
|
|
88
|
+
crucible knowledge list # List all knowledge files
|
|
89
|
+
crucible knowledge init <file> # Copy for customization
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
See [ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full flow.
|
|
93
|
+
|
|
94
|
+
## Customization
|
|
95
|
+
|
|
96
|
+
Override skills and knowledge for your project or personal preferences:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Customize a skill for your project
|
|
100
|
+
crucible skills init security-engineer
|
|
101
|
+
# Creates .crucible/skills/security-engineer/SKILL.md
|
|
102
|
+
|
|
103
|
+
# Add project-specific concerns, team conventions, etc.
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Resolution order (first found wins):
|
|
107
|
+
1. `.crucible/` — Project overrides
|
|
108
|
+
2. `~/.claude/crucible/` — User preferences
|
|
109
|
+
3. Bundled — Package defaults
|
|
110
|
+
|
|
111
|
+
See [CUSTOMIZATION.md](docs/CUSTOMIZATION.md) for the full guide.
|
|
112
|
+
|
|
113
|
+
## What's Included
|
|
114
|
+
|
|
115
|
+
**18 Personas** — Domain-specific thinking: security, performance, accessibility, web3, backend, and more.
|
|
116
|
+
|
|
117
|
+
See [SKILLS.md](docs/SKILLS.md) for the full list.
|
|
118
|
+
|
|
119
|
+
**12 Knowledge Files** — Coding patterns and principles for security, testing, APIs, databases, smart contracts, etc.
|
|
120
|
+
|
|
121
|
+
See [KNOWLEDGE.md](docs/KNOWLEDGE.md) for topics covered.
|
|
122
|
+
|
|
123
|
+
## Documentation
|
|
124
|
+
|
|
125
|
+
| Doc | What's In It |
|
|
126
|
+
|-----|--------------|
|
|
127
|
+
| [FEATURES.md](docs/FEATURES.md) | Complete feature reference |
|
|
128
|
+
| [ARCHITECTURE.md](docs/ARCHITECTURE.md) | How MCP, tools, skills, and knowledge fit together |
|
|
129
|
+
| [CUSTOMIZATION.md](docs/CUSTOMIZATION.md) | Override skills and knowledge for your project |
|
|
130
|
+
| [SKILLS.md](docs/SKILLS.md) | All 18 personas with triggers and focus areas |
|
|
131
|
+
| [KNOWLEDGE.md](docs/KNOWLEDGE.md) | All 12 knowledge files with topics covered |
|
|
132
|
+
| [CONTRIBUTING.md](docs/CONTRIBUTING.md) | Adding tools, skills, and knowledge |
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
pip install -e ".[dev]"
|
|
138
|
+
pytest # Run tests (494 tests)
|
|
139
|
+
ruff check src/ --fix # Lint
|
|
140
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
crucible/__init__.py,sha256=ZLZQWKmjTHaeeDijcOl3xmaEgoI2W3a8FCFwcieZGv0,77
|
|
2
|
+
crucible/cli.py,sha256=eCOHbV1jn9SdjtRVB8-PnHah0e-mkrxSOFjMYpMTUCY,51651
|
|
3
|
+
crucible/errors.py,sha256=HrX_yvJEhXJoKodXGo_iY9wqx2J3ONYy0a_LbrVC5As,819
|
|
4
|
+
crucible/models.py,sha256=jaxbiPc1E7bJxKPLadZe1dbSJdq-WINsxjveeSNNqeg,2066
|
|
5
|
+
crucible/server.py,sha256=GnT_wT9QJE5gAQYmYzSIuaj5hG2hKfOrHHK104tPtqA,34493
|
|
6
|
+
crucible/domain/__init__.py,sha256=2fsoB5wH2Pl3vtGRt4voYOSZ04-zLoW8pNq6nvzVMgU,118
|
|
7
|
+
crucible/domain/detection.py,sha256=TNeLB_VQgS1AsT5BKDf_tIpGa47THrFoRXwU4u54VB0,1797
|
|
8
|
+
crucible/hooks/__init__.py,sha256=k5oEWhTJKEQi-QWBfTbp1p6HaKg55_wVCBVD5pZzdqw,271
|
|
9
|
+
crucible/hooks/precommit.py,sha256=OAwvjEACopcrTmWmZMO0S8TqZkvFY_392pJBFCHGSaQ,21561
|
|
10
|
+
crucible/knowledge/__init__.py,sha256=unb7kyO1MtB3Zt-TGx_O8LE79KyrGrNHoFFHgUWUvGU,40
|
|
11
|
+
crucible/knowledge/loader.py,sha256=DD4gqU6xkssaWvEkbymMOu6YtHab7YLEk-tU9cPTeaE,6666
|
|
12
|
+
crucible/skills/__init__.py,sha256=L3heXWF0T3aR9yYLFphs1LNlkxAFSPkPuRFMH-S1taI,495
|
|
13
|
+
crucible/skills/loader.py,sha256=iC0_V1s6CIse5NXyFGtpLbON8xDxYh8xXmHH7hAX5O0,8642
|
|
14
|
+
crucible/synthesis/__init__.py,sha256=CYrkZG4bdAjp8XdOh1smfKscd3YU5lZlaDLGwLE9c-0,46
|
|
15
|
+
crucible/tools/__init__.py,sha256=gFRThTk1E-fHzpe8bB5rtBG6Z6G-ysPzjVEHfKGbEYU,400
|
|
16
|
+
crucible/tools/delegation.py,sha256=_x1y76No3qkmGjjROVvMx1pSKKwU59aRu5R-r07lVFU,12871
|
|
17
|
+
crucible/tools/git.py,sha256=EmxRUt0jSFLa_mm_2Czt5rHdiFC0YK9IpaPDfRwlXVo,10051
|
|
18
|
+
crucible_mcp-0.2.0.dist-info/METADATA,sha256=Jcnnfw7AxZ-vtqNNOCR9j43MaRxzvXn6oW05nR1wccI,3890
|
|
19
|
+
crucible_mcp-0.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
20
|
+
crucible_mcp-0.2.0.dist-info/entry_points.txt,sha256=18BZaH1OlFSFYtKuHq0Z8yYX8Wmx7Ikfqay-P00ZX3Q,83
|
|
21
|
+
crucible_mcp-0.2.0.dist-info/top_level.txt,sha256=4hzuFgqbFPOO-WiU_DYxTm8VYIxTXh7Wlp0gRcWR0Cs,9
|
|
22
|
+
crucible_mcp-0.2.0.dist-info/RECORD,,
|
|
@@ -1,158 +0,0 @@
|
|
|
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
|
-
```
|
|
@@ -1,17 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|