/ 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()