/ server.py
server.py
1 #!/usr/bin/env python3 2 """MCP server — exposes PR-review tools to Claude.""" 3 4 import sys 5 from pathlib import Path 6 7 # Force UTF-8 on Windows (MCP stdio protocol requires it) 8 if sys.platform == "win32": 9 sys.stdout.reconfigure(encoding="utf-8") 10 sys.stderr.reconfigure(encoding="utf-8") 11 12 from mcp.server.fastmcp import FastMCP 13 14 from file_filter import should_include 15 from logger import log_error, log_debug 16 import github_client 17 18 mcp = FastMCP("pr-review") 19 20 21 @mcp.tool() 22 def get_pr_diff(pr_number: int) -> str: 23 """Fetch code diff from a GitHub Pull Request, skipping Unity binary/asset files.""" 24 try: 25 resp = github_client.github_get(f"pulls/{pr_number}/files", per_page=100) 26 27 if resp.status_code == 401: 28 return "Error: GitHub token invalid or expired. Run: python server.py --reset" 29 if resp.status_code == 404: 30 return f"PR #{pr_number} not found in {github_client.REPO}" 31 resp.raise_for_status() 32 33 files = resp.json() 34 included = [] 35 skipped = [] 36 37 for f in files: 38 filename = f["filename"] 39 if not should_include(filename): 40 skipped.append(Path(filename).name) 41 continue 42 43 patch = f.get("patch") 44 if not patch: 45 skipped.append(Path(filename).name) 46 continue 47 48 included.append(f"## `{filename}` ({f['status']})\n```diff\n{patch}\n```") 49 50 if not included: 51 return f"PR #{pr_number}: no reviewable code files found. Skipped: {', '.join(skipped)}" 52 53 result = f"# PR #{pr_number} — {github_client.REPO}\n\n" 54 result += f"**Skipped (assets/binary):** {', '.join(skipped)}\n\n---\n\n" 55 result += "\n\n".join(included) 56 57 # Stats 58 chars = len(result) 59 tokens_approx = chars // 4 60 diff_lines = sum(p.count("\n") for p in included) 61 result += f"\n\n---\n**Stats:** {len(included)} files, {diff_lines} diff lines, ~{tokens_approx:,} tokens" 62 63 log_debug("get_pr_diff", f"PR #{pr_number}: {len(included)} files, {len(skipped)} skipped") 64 return result 65 except Exception as e: 66 return log_error("get_pr_diff", e) 67 68 69 @mcp.tool() 70 def list_open_prs() -> str: 71 """List open Pull Requests in the repository.""" 72 try: 73 resp = github_client.github_get("pulls", state="open", per_page=20) 74 75 if resp.status_code == 401: 76 return "Error: GitHub token invalid or expired. Run: python server.py --reset" 77 resp.raise_for_status() 78 79 prs = resp.json() 80 if not prs: 81 return "No open PRs found." 82 83 lines = [f"# Open PRs in {github_client.REPO}\n"] 84 for pr in prs: 85 lines.append( 86 f"- **#{pr['number']}** {pr['title']} " 87 f"— `{pr['user']['login']}` → `{pr['base']['ref']}`" 88 ) 89 90 log_debug("list_open_prs", f"Found {len(prs)} open PRs") 91 return "\n".join(lines) 92 except Exception as e: 93 return log_error("list_open_prs", e) 94 95 96 if __name__ == "__main__": 97 if "--reset" in sys.argv: 98 github_client.reset_secrets() 99 else: 100 github_client.init_secrets() 101 mcp.run()