review-ready-mcp 0.1.0
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.
- package/README.md +96 -0
- package/dist/checks.d.ts +26 -0
- package/dist/checks.d.ts.map +1 -0
- package/dist/checks.js +117 -0
- package/dist/gitDiff.d.ts +7 -0
- package/dist/gitDiff.d.ts.map +1 -0
- package/dist/gitDiff.js +88 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +169 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# review-ready-mcp
|
|
2
|
+
|
|
3
|
+
**MCP server for [Review Ready](https://github.com/yurukusa/vscode-review-ready) — run pre-PR checks from Claude.**
|
|
4
|
+
|
|
5
|
+
Ask Claude to check your code before opening a pull request. Claude will scan your changed files and flag debug statements, hardcoded secrets, TODO debt, and complexity spikes.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
### Claude Desktop
|
|
10
|
+
|
|
11
|
+
Add to your `claude_desktop_config.json`:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"mcpServers": {
|
|
16
|
+
"review-ready": {
|
|
17
|
+
"command": "npx",
|
|
18
|
+
"args": ["review-ready-mcp"]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Claude Code
|
|
25
|
+
|
|
26
|
+
Add to your project's `.mcp.json` or global MCP settings:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"review-ready": {
|
|
32
|
+
"command": "npx",
|
|
33
|
+
"args": ["review-ready-mcp"]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then ask Claude: *"Check my changes before I push"* or *"Run review-ready on /path/to/my/repo"*
|
|
40
|
+
|
|
41
|
+
## Tools
|
|
42
|
+
|
|
43
|
+
### `check_changes`
|
|
44
|
+
|
|
45
|
+
Runs all checks on the changed files in a git repository.
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Input:
|
|
49
|
+
repo_path: string — Absolute path to the git repo root
|
|
50
|
+
base_sha?: string — Optional base commit SHA (for CI use)
|
|
51
|
+
head_sha?: string — Optional head commit SHA (for CI use)
|
|
52
|
+
complexity_threshold?: number — Branch count threshold (default: 10)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### `check_content`
|
|
56
|
+
|
|
57
|
+
Checks a code snippet directly — no git required.
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
Input:
|
|
61
|
+
content: string — The code to check
|
|
62
|
+
filename: string — Filename for language-specific rules (e.g., "auth.ts")
|
|
63
|
+
complexity_threshold?: number — Branch count threshold (default: 10)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## What it checks
|
|
67
|
+
|
|
68
|
+
| Check | What it catches |
|
|
69
|
+
|-------|----------------|
|
|
70
|
+
| **Debug statements** | `console.log`, `debugger`, `print()`, `puts`, `fmt.Print`, `println!`, `var_dump`, `dd()` |
|
|
71
|
+
| **TODO/FIXME debt** | `TODO`, `FIXME`, `HACK`, `XXX`, `TEMP`, `WTF`, `BUG` in new code |
|
|
72
|
+
| **Secrets** | AWS keys, GitHub PATs, OpenAI keys, Slack tokens, hardcoded credentials |
|
|
73
|
+
| **Large files** | Files over 500KB accidentally staged |
|
|
74
|
+
| **Missing tests** | Source files changed without a test file (via `check_changes`) |
|
|
75
|
+
| **Complexity** | High cyclomatic complexity in changed JS/TS code |
|
|
76
|
+
|
|
77
|
+
## Example usage in Claude
|
|
78
|
+
|
|
79
|
+
> **You:** Check my changes before I push. The repo is at /home/me/my-project.
|
|
80
|
+
>
|
|
81
|
+
> **Claude:** *(calls `check_changes` with repo_path="/home/me/my-project")*
|
|
82
|
+
>
|
|
83
|
+
> Found 2 errors, 1 warning, 0 info
|
|
84
|
+
>
|
|
85
|
+
> ✗ [no-debug-statements] Debug statement found: console.log(userData) [src/auth.ts:42]
|
|
86
|
+
> ✗ [no-secrets] Possible API key detected [src/config.ts:8]
|
|
87
|
+
> ⚠ [no-todo-in-changes] Unresolved marker: // TODO: validate token expiry [src/auth.ts:67]
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
Also available as:
|
|
92
|
+
- **GitHub Action**: `uses: yurukusa/review-ready@v0.1.0`
|
|
93
|
+
- **VS Code Extension**: Search "Review Ready" in the marketplace
|
|
94
|
+
- **npm library**: `npm install review-ready`
|
|
95
|
+
|
|
96
|
+
Source: [github.com/yurukusa/vscode-review-ready](https://github.com/yurukusa/vscode-review-ready)
|
package/dist/checks.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* checks.ts — Review Ready check functions (copied from review-ready package)
|
|
3
|
+
* Pure functions: (FileChanges) → CheckResult[]
|
|
4
|
+
* No external dependencies — runs anywhere Node.js is installed.
|
|
5
|
+
*/
|
|
6
|
+
export interface CheckResult {
|
|
7
|
+
rule: string;
|
|
8
|
+
severity: 'error' | 'warning' | 'info';
|
|
9
|
+
message: string;
|
|
10
|
+
line?: number;
|
|
11
|
+
file?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface FileChanges {
|
|
14
|
+
filename: string;
|
|
15
|
+
addedLines: string[];
|
|
16
|
+
addedLineNumbers: number[];
|
|
17
|
+
isNewFile: boolean;
|
|
18
|
+
sizeBytes: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function checkDebugStatements(changes: FileChanges): CheckResult[];
|
|
21
|
+
export declare function checkTodos(changes: FileChanges): CheckResult[];
|
|
22
|
+
export declare function checkSecrets(changes: FileChanges): CheckResult[];
|
|
23
|
+
export declare function checkLargeFile(changes: FileChanges): CheckResult[];
|
|
24
|
+
export declare function checkTestExists(changes: FileChanges, allFiles: Set<string>): CheckResult[];
|
|
25
|
+
export declare function checkComplexity(changes: FileChanges, threshold: number): CheckResult[];
|
|
26
|
+
//# sourceMappingURL=checks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../src/checks.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAcD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,WAAW,GAAG,WAAW,EAAE,CAaxE;AAKD,wBAAgB,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,WAAW,EAAE,CAQ9D;AAaD,wBAAgB,YAAY,CAAC,OAAO,EAAE,WAAW,GAAG,WAAW,EAAE,CAehE;AAGD,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,WAAW,EAAE,CAKlE;AAWD,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,WAAW,EAAE,CAY1F;AAKD,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,EAAE,CAUtF"}
|
package/dist/checks.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* checks.ts — Review Ready check functions (copied from review-ready package)
|
|
3
|
+
* Pure functions: (FileChanges) → CheckResult[]
|
|
4
|
+
* No external dependencies — runs anywhere Node.js is installed.
|
|
5
|
+
*/
|
|
6
|
+
// ── Debug statements ─────────────────────────────────────────────────────────
|
|
7
|
+
const DEBUG_PATTERNS = [
|
|
8
|
+
/\bconsole\.(log|debug|warn|error|trace|dir|table)\s*\(/,
|
|
9
|
+
/\bdebugger\b/,
|
|
10
|
+
/\bprint\s*\(/,
|
|
11
|
+
/\bputs\s/,
|
|
12
|
+
/\bfmt\.Print/,
|
|
13
|
+
/\bprintln!\s*\(/,
|
|
14
|
+
/\bvar_dump\s*\(/,
|
|
15
|
+
/\bdd\s*\(/,
|
|
16
|
+
];
|
|
17
|
+
export function checkDebugStatements(changes) {
|
|
18
|
+
const results = [];
|
|
19
|
+
changes.addedLines.forEach((line, idx) => {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('*'))
|
|
22
|
+
return;
|
|
23
|
+
for (const pattern of DEBUG_PATTERNS) {
|
|
24
|
+
if (pattern.test(line)) {
|
|
25
|
+
results.push({ rule: 'no-debug-statements', severity: 'error', message: `Debug statement found: ${line.trim()}`, line: changes.addedLineNumbers[idx], file: changes.filename });
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return results;
|
|
31
|
+
}
|
|
32
|
+
// ── TODO/FIXME ───────────────────────────────────────────────────────────────
|
|
33
|
+
const TODO_PATTERN = /\b(TODO|FIXME|HACK|XXX|TEMP|WTF|BUG)[\s:]/i;
|
|
34
|
+
export function checkTodos(changes) {
|
|
35
|
+
const results = [];
|
|
36
|
+
changes.addedLines.forEach((line, idx) => {
|
|
37
|
+
if (TODO_PATTERN.test(line)) {
|
|
38
|
+
results.push({ rule: 'no-todo-in-changes', severity: 'warning', message: `Unresolved marker in new code: ${line.trim()}`, line: changes.addedLineNumbers[idx], file: changes.filename });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
return results;
|
|
42
|
+
}
|
|
43
|
+
// ── Secrets ──────────────────────────────────────────────────────────────────
|
|
44
|
+
const SECRET_PATTERNS = [
|
|
45
|
+
{ pattern: /['"][A-Za-z0-9+/]{40,}['"]/, label: 'long base64-like string' },
|
|
46
|
+
{ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"][^'"]{8,}['"]/i, label: 'API key' },
|
|
47
|
+
{ pattern: /(?:secret|password|passwd|pwd)\s*[:=]\s*['"][^'"]{4,}['"]/i, label: 'credential' },
|
|
48
|
+
{ pattern: /(?:AKIA|ASIA)[A-Z0-9]{16}/, label: 'AWS access key' },
|
|
49
|
+
{ pattern: /ghp_[A-Za-z0-9]{36}/, label: 'GitHub personal access token' },
|
|
50
|
+
{ pattern: /sk-[A-Za-z0-9]{48}/, label: 'OpenAI API key' },
|
|
51
|
+
{ pattern: /xox[baprs]-[A-Za-z0-9-]{10,}/, label: 'Slack token' },
|
|
52
|
+
];
|
|
53
|
+
export function checkSecrets(changes) {
|
|
54
|
+
const lowerName = changes.filename.toLowerCase();
|
|
55
|
+
if (lowerName.includes('test') || lowerName.includes('spec') || lowerName.includes('mock') || lowerName.includes('fixture') || lowerName.includes('example') || lowerName.endsWith('.md'))
|
|
56
|
+
return [];
|
|
57
|
+
const results = [];
|
|
58
|
+
changes.addedLines.forEach((line, idx) => {
|
|
59
|
+
const trimmed = line.trim();
|
|
60
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('#'))
|
|
61
|
+
return;
|
|
62
|
+
for (const { pattern, label } of SECRET_PATTERNS) {
|
|
63
|
+
if (pattern.test(line)) {
|
|
64
|
+
results.push({ rule: 'no-secrets', severity: 'error', message: `Possible ${label} detected`, line: changes.addedLineNumbers[idx], file: changes.filename });
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
71
|
+
// ── Large file ───────────────────────────────────────────────────────────────
|
|
72
|
+
export function checkLargeFile(changes) {
|
|
73
|
+
if (changes.sizeBytes > 500 * 1024) {
|
|
74
|
+
return [{ rule: 'no-large-files', severity: 'warning', message: `File is ${(changes.sizeBytes / 1024).toFixed(0)} KB — is this intentional?`, file: changes.filename }];
|
|
75
|
+
}
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
// ── Test coverage ─────────────────────────────────────────────────────────────
|
|
79
|
+
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.py', '.rb', '.go', '.java']);
|
|
80
|
+
const TEST_CONVENTIONS = [
|
|
81
|
+
(f) => f.replace(/\.(ts|tsx|js|jsx)$/, '.test.$1'),
|
|
82
|
+
(f) => f.replace(/\.(ts|tsx|js|jsx)$/, '.spec.$1'),
|
|
83
|
+
(f) => f.replace('/src/', '/tests/').replace(/\.(ts|tsx|js|jsx)$/, '.test.$1'),
|
|
84
|
+
(f) => { const r = f.replace(/\.(py)$/, '_test.$1'); return r !== f ? r : ''; },
|
|
85
|
+
];
|
|
86
|
+
export function checkTestExists(changes, allFiles) {
|
|
87
|
+
const ext = '.' + changes.filename.split('.').pop();
|
|
88
|
+
if (!SOURCE_EXTENSIONS.has(ext))
|
|
89
|
+
return [];
|
|
90
|
+
if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(changes.filename))
|
|
91
|
+
return [];
|
|
92
|
+
if (changes.filename.includes('__tests__'))
|
|
93
|
+
return [];
|
|
94
|
+
const base = changes.filename.split('/').pop() ?? '';
|
|
95
|
+
if (/^(index|types|constants|config|main|app)\.(ts|tsx|js|jsx)$/.test(base))
|
|
96
|
+
return [];
|
|
97
|
+
const hasTest = TEST_CONVENTIONS.some(fn => { const r = fn(changes.filename); return r && allFiles.has(r); });
|
|
98
|
+
if (!hasTest) {
|
|
99
|
+
return [{ rule: 'test-file-exists', severity: 'info', message: `No test file found for ${base}`, file: changes.filename }];
|
|
100
|
+
}
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
// ── Complexity ───────────────────────────────────────────────────────────────
|
|
104
|
+
const BRANCH_KEYWORDS = /\b(if|else if|while|for|case|catch|\?\s*[^:]+:|\&\&|\|\|)\b/g;
|
|
105
|
+
export function checkComplexity(changes, threshold) {
|
|
106
|
+
const ext = '.' + changes.filename.split('.').pop();
|
|
107
|
+
if (!['.ts', '.tsx', '.js', '.jsx'].includes(ext))
|
|
108
|
+
return [];
|
|
109
|
+
if (changes.addedLines.length < 20)
|
|
110
|
+
return [];
|
|
111
|
+
const matches = changes.addedLines.join('\n').match(BRANCH_KEYWORDS);
|
|
112
|
+
const approxCC = (matches?.length ?? 0) + 1;
|
|
113
|
+
if (approxCC > threshold) {
|
|
114
|
+
return [{ rule: 'complexity-threshold', severity: 'warning', message: `New code has high apparent complexity (~${approxCC} branches) — consider breaking it up`, file: changes.filename }];
|
|
115
|
+
}
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gitDiff.ts — Parse git diff output into FileChanges objects
|
|
3
|
+
*/
|
|
4
|
+
import type { FileChanges } from './checks.js';
|
|
5
|
+
export declare function getChangedFiles(repoRoot: string, baseSha?: string, headSha?: string): FileChanges[];
|
|
6
|
+
export declare function getAllProjectFiles(repoRoot: string): Set<string>;
|
|
7
|
+
//# sourceMappingURL=gitDiff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitDiff.d.ts","sourceRoot":"","sources":["../src/gitDiff.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE,CA2CnG;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAKhE"}
|
package/dist/gitDiff.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gitDiff.ts — Parse git diff output into FileChanges objects
|
|
3
|
+
*/
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
export function getChangedFiles(repoRoot, baseSha, headSha) {
|
|
8
|
+
const results = [];
|
|
9
|
+
let changedFilenames = [];
|
|
10
|
+
try {
|
|
11
|
+
if (baseSha && headSha) {
|
|
12
|
+
// GitHub Actions / specified range mode
|
|
13
|
+
const output = execSync(`git diff --name-only ${baseSha}..${headSha}`, { cwd: repoRoot, encoding: 'utf8' });
|
|
14
|
+
changedFilenames = output.trim().split('\n').filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
// Local mode: staged + unstaged
|
|
18
|
+
const staged = execSync('git diff --cached --name-only', { cwd: repoRoot, encoding: 'utf8' });
|
|
19
|
+
const unstaged = execSync('git diff --name-only', { cwd: repoRoot, encoding: 'utf8' });
|
|
20
|
+
const all = new Set([...staged.trim().split('\n'), ...unstaged.trim().split('\n')].filter(Boolean));
|
|
21
|
+
changedFilenames = [...all];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
for (const filename of changedFilenames) {
|
|
28
|
+
const fullPath = path.join(repoRoot, filename);
|
|
29
|
+
let sizeBytes = 0;
|
|
30
|
+
try {
|
|
31
|
+
sizeBytes = fs.statSync(fullPath).size;
|
|
32
|
+
}
|
|
33
|
+
catch { /* deleted */ }
|
|
34
|
+
let diffOutput = '';
|
|
35
|
+
try {
|
|
36
|
+
if (baseSha && headSha) {
|
|
37
|
+
diffOutput = execSync(`git diff ${baseSha}..${headSha} -U0 -- "${filename}"`, { cwd: repoRoot, encoding: 'utf8' });
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
diffOutput = execSync(`git diff --cached -U0 -- "${filename}"`, { cwd: repoRoot, encoding: 'utf8' });
|
|
41
|
+
if (!diffOutput.trim()) {
|
|
42
|
+
diffOutput = execSync(`git diff -U0 -- "${filename}"`, { cwd: repoRoot, encoding: 'utf8' });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const { addedLines, addedLineNumbers, isNewFile } = parseDiff(diffOutput);
|
|
50
|
+
results.push({ filename, addedLines, addedLineNumbers, isNewFile, sizeBytes });
|
|
51
|
+
}
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
export function getAllProjectFiles(repoRoot) {
|
|
55
|
+
try {
|
|
56
|
+
const out = execSync('git ls-files', { cwd: repoRoot, encoding: 'utf8' });
|
|
57
|
+
return new Set(out.trim().split('\n').filter(Boolean));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return new Set();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function parseDiff(diff) {
|
|
64
|
+
const lines = diff.split('\n');
|
|
65
|
+
const addedLines = [];
|
|
66
|
+
const addedLineNumbers = [];
|
|
67
|
+
let isNewFile = false;
|
|
68
|
+
let cur = 0;
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
if (line.startsWith('new file mode')) {
|
|
71
|
+
isNewFile = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const hunk = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
75
|
+
if (hunk) {
|
|
76
|
+
cur = parseInt(hunk[1], 10);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
80
|
+
addedLines.push(line.slice(1));
|
|
81
|
+
addedLineNumbers.push(cur++);
|
|
82
|
+
}
|
|
83
|
+
else if (line.startsWith(' ')) {
|
|
84
|
+
cur++;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { addedLines, addedLineNumbers, isNewFile };
|
|
88
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* review-ready-mcp — MCP server for Review Ready pre-PR checks
|
|
4
|
+
*
|
|
5
|
+
* Exposes these tools to Claude and other MCP clients:
|
|
6
|
+
* - check_changes: Run all Review Ready checks on a git repo's changed files
|
|
7
|
+
* - check_content: Check a code snippet directly (no git required)
|
|
8
|
+
*
|
|
9
|
+
* Transport: stdio (local tool, intended to run on developer's machine)
|
|
10
|
+
*
|
|
11
|
+
* Usage in claude_desktop_config.json:
|
|
12
|
+
* {
|
|
13
|
+
* "mcpServers": {
|
|
14
|
+
* "review-ready": {
|
|
15
|
+
* "command": "npx",
|
|
16
|
+
* "args": ["review-ready-mcp"]
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
*/
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;GAkBG"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* review-ready-mcp — MCP server for Review Ready pre-PR checks
|
|
4
|
+
*
|
|
5
|
+
* Exposes these tools to Claude and other MCP clients:
|
|
6
|
+
* - check_changes: Run all Review Ready checks on a git repo's changed files
|
|
7
|
+
* - check_content: Check a code snippet directly (no git required)
|
|
8
|
+
*
|
|
9
|
+
* Transport: stdio (local tool, intended to run on developer's machine)
|
|
10
|
+
*
|
|
11
|
+
* Usage in claude_desktop_config.json:
|
|
12
|
+
* {
|
|
13
|
+
* "mcpServers": {
|
|
14
|
+
* "review-ready": {
|
|
15
|
+
* "command": "npx",
|
|
16
|
+
* "args": ["review-ready-mcp"]
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
*/
|
|
21
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
22
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
import * as path from 'path';
|
|
25
|
+
import { checkDebugStatements, checkTodos, checkSecrets, checkLargeFile, checkTestExists, checkComplexity, } from './checks.js';
|
|
26
|
+
import { getChangedFiles, getAllProjectFiles } from './gitDiff.js';
|
|
27
|
+
// ── Server setup ──────────────────────────────────────────────────────────────
|
|
28
|
+
const server = new McpServer({
|
|
29
|
+
name: 'review-ready-mcp-server',
|
|
30
|
+
version: '0.1.0',
|
|
31
|
+
});
|
|
32
|
+
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
33
|
+
function formatResults(results) {
|
|
34
|
+
if (results.length === 0)
|
|
35
|
+
return '✓ All checks passed — ready to review!';
|
|
36
|
+
const errors = results.filter(r => r.severity === 'error');
|
|
37
|
+
const warnings = results.filter(r => r.severity === 'warning');
|
|
38
|
+
const infos = results.filter(r => r.severity === 'info');
|
|
39
|
+
const lines = [
|
|
40
|
+
`Found ${errors.length} error(s), ${warnings.length} warning(s), ${infos.length} info\n`,
|
|
41
|
+
];
|
|
42
|
+
for (const r of results) {
|
|
43
|
+
const icon = r.severity === 'error' ? '✗' : r.severity === 'warning' ? '⚠' : 'ℹ';
|
|
44
|
+
const loc = r.file ? `${r.file}${r.line ? `:${r.line}` : ''}` : '';
|
|
45
|
+
const locStr = loc ? ` [${loc}]` : '';
|
|
46
|
+
lines.push(`${icon} [${r.rule}] ${r.message}${locStr}`);
|
|
47
|
+
}
|
|
48
|
+
return lines.join('\n');
|
|
49
|
+
}
|
|
50
|
+
function runAllChecks(changedFiles, allProjectFiles, complexityThreshold) {
|
|
51
|
+
const all = [];
|
|
52
|
+
for (const file of changedFiles) {
|
|
53
|
+
all.push(...checkDebugStatements(file));
|
|
54
|
+
all.push(...checkTodos(file));
|
|
55
|
+
all.push(...checkSecrets(file));
|
|
56
|
+
all.push(...checkLargeFile(file));
|
|
57
|
+
all.push(...checkTestExists(file, allProjectFiles));
|
|
58
|
+
all.push(...checkComplexity(file, complexityThreshold));
|
|
59
|
+
}
|
|
60
|
+
return all.sort((a, b) => {
|
|
61
|
+
const order = { error: 0, warning: 1, info: 2 };
|
|
62
|
+
return order[a.severity] - order[b.severity];
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// ── Tool: check_changes ────────────────────────────────────────────────────────
|
|
66
|
+
server.registerTool('check_changes', {
|
|
67
|
+
title: 'Check Changed Files',
|
|
68
|
+
description: `Run Review Ready pre-PR checks on all changed files in a git repository.
|
|
69
|
+
|
|
70
|
+
Detects:
|
|
71
|
+
- Debug statements (console.log, debugger, print(), etc.)
|
|
72
|
+
- TODO/FIXME/HACK markers in newly added lines
|
|
73
|
+
- Potential secrets (API keys, AWS credentials, tokens, passwords)
|
|
74
|
+
- Accidentally staged large files (>500KB)
|
|
75
|
+
- Source files missing corresponding test files
|
|
76
|
+
- High cyclomatic complexity in JS/TS additions
|
|
77
|
+
|
|
78
|
+
Returns a summary of issues with file paths and line numbers.`,
|
|
79
|
+
inputSchema: {
|
|
80
|
+
repo_path: z.string().describe('Absolute path to the git repository root. Use the workspace root when checking changes before opening a PR.'),
|
|
81
|
+
base_sha: z.string().optional().describe('Base commit SHA for diff (optional). If provided with head_sha, diffs between two commits instead of staged/unstaged.'),
|
|
82
|
+
head_sha: z.string().optional().describe('Head commit SHA for diff (optional). Used with base_sha.'),
|
|
83
|
+
complexity_threshold: z.number().optional().describe('Cyclomatic complexity threshold. Flag functions exceeding this branch count. Default: 10.'),
|
|
84
|
+
},
|
|
85
|
+
annotations: {
|
|
86
|
+
readOnlyHint: true,
|
|
87
|
+
destructiveHint: false,
|
|
88
|
+
idempotentHint: true,
|
|
89
|
+
},
|
|
90
|
+
}, async ({ repo_path, base_sha, head_sha, complexity_threshold = 10 }) => {
|
|
91
|
+
const repoRoot = path.resolve(repo_path);
|
|
92
|
+
const changedFiles = getChangedFiles(repoRoot, base_sha, head_sha);
|
|
93
|
+
if (changedFiles.length === 0) {
|
|
94
|
+
const output = { status: 'ok', message: 'No changed files detected', results: [] };
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: 'text', text: '✓ No changed files detected — nothing to check.' }],
|
|
97
|
+
structuredContent: output,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const allProjectFiles = getAllProjectFiles(repoRoot);
|
|
101
|
+
const results = runAllChecks(changedFiles, allProjectFiles, complexity_threshold);
|
|
102
|
+
const structured = {
|
|
103
|
+
status: results.some(r => r.severity === 'error') ? 'error' : results.some(r => r.severity === 'warning') ? 'warning' : 'ok',
|
|
104
|
+
filesChecked: changedFiles.length,
|
|
105
|
+
totalIssues: results.length,
|
|
106
|
+
errors: results.filter(r => r.severity === 'error').length,
|
|
107
|
+
warnings: results.filter(r => r.severity === 'warning').length,
|
|
108
|
+
infos: results.filter(r => r.severity === 'info').length,
|
|
109
|
+
results,
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
content: [{ type: 'text', text: formatResults(results) }],
|
|
113
|
+
structuredContent: structured,
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
// ── Tool: check_content ───────────────────────────────────────────────────────
|
|
117
|
+
server.registerTool('check_content', {
|
|
118
|
+
title: 'Check Code Content',
|
|
119
|
+
description: `Run Review Ready checks on a code snippet directly — no git repository needed.
|
|
120
|
+
|
|
121
|
+
Useful when reviewing a code block that Claude just generated, or when checking a file's content before committing.
|
|
122
|
+
|
|
123
|
+
Detects: debug statements, TODO markers, secrets, and complexity issues.
|
|
124
|
+
Note: test coverage check requires knowing all project files, so it's skipped in this mode.`,
|
|
125
|
+
inputSchema: {
|
|
126
|
+
content: z.string().describe('The code content to check. Can be a full file or a snippet.'),
|
|
127
|
+
filename: z.string().describe('The filename (e.g., "src/auth.ts") — used to apply language-specific rules and detect if it is a test file.'),
|
|
128
|
+
complexity_threshold: z.number().optional().describe('Cyclomatic complexity threshold. Default: 10.'),
|
|
129
|
+
},
|
|
130
|
+
annotations: {
|
|
131
|
+
readOnlyHint: true,
|
|
132
|
+
destructiveHint: false,
|
|
133
|
+
idempotentHint: true,
|
|
134
|
+
},
|
|
135
|
+
}, async ({ content, filename, complexity_threshold = 10 }) => {
|
|
136
|
+
const lines = content.split('\n');
|
|
137
|
+
const changes = {
|
|
138
|
+
filename,
|
|
139
|
+
addedLines: lines,
|
|
140
|
+
addedLineNumbers: lines.map((_, i) => i + 1),
|
|
141
|
+
isNewFile: true,
|
|
142
|
+
sizeBytes: Buffer.byteLength(content, 'utf8'),
|
|
143
|
+
};
|
|
144
|
+
const results = [
|
|
145
|
+
...checkDebugStatements(changes),
|
|
146
|
+
...checkTodos(changes),
|
|
147
|
+
...checkSecrets(changes),
|
|
148
|
+
...checkLargeFile(changes),
|
|
149
|
+
...checkComplexity(changes, complexity_threshold),
|
|
150
|
+
// Note: checkTestExists skipped — requires allProjectFiles context
|
|
151
|
+
].sort((a, b) => {
|
|
152
|
+
const order = { error: 0, warning: 1, info: 2 };
|
|
153
|
+
return order[a.severity] - order[b.severity];
|
|
154
|
+
});
|
|
155
|
+
const structured = {
|
|
156
|
+
status: results.some(r => r.severity === 'error') ? 'error' : results.some(r => r.severity === 'warning') ? 'warning' : 'ok',
|
|
157
|
+
filename,
|
|
158
|
+
linesChecked: lines.length,
|
|
159
|
+
totalIssues: results.length,
|
|
160
|
+
results,
|
|
161
|
+
};
|
|
162
|
+
return {
|
|
163
|
+
content: [{ type: 'text', text: formatResults(results) }],
|
|
164
|
+
structuredContent: structured,
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
// ── Start server ──────────────────────────────────────────────────────────────
|
|
168
|
+
const transport = new StdioServerTransport();
|
|
169
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "review-ready-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Review Ready — run pre-PR checks (debug statements, secrets, TODO debt, complexity) from Claude",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"review-ready-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"mcp",
|
|
12
|
+
"model-context-protocol",
|
|
13
|
+
"code-review",
|
|
14
|
+
"pr",
|
|
15
|
+
"git",
|
|
16
|
+
"claude"
|
|
17
|
+
],
|
|
18
|
+
"author": "yurukusa",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/yurukusa/review-ready-mcp"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist/"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"dev": "tsc -watch",
|
|
30
|
+
"start": "node dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.8.0",
|
|
34
|
+
"zod": "^4.3.6"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^20.0.0",
|
|
38
|
+
"typescript": "^5.3.0"
|
|
39
|
+
}
|
|
40
|
+
}
|