วันนี้ผมจะมาแชร์ประสบการณ์จริงในการสร้างระบบ Code Review อัตโนมัติด้วย MCP Server และ GitHub API ซึ่งเป็นโปรเจกต์ที่ทีมของผมพัฒนาขึ้นมาเพื่อลดภาระงานการตรวจโค้ดแบบ Manual ลงอย่างมาก

เริ่มต้น: ปัญหาที่ทำให้เราต้องหาทางออก

ตอนแรกทีมของผมมีปัญหาหลายอย่างกับการ Review Code:

ผมลองใช้วิธีต่างๆ จนกระทั่งเจอปัญหานี้ตอนที่พยายามเชื่อมต่อ GitHub API โดยตรง:

ConnectionError: timeout occurred while connecting to github.com
HTTPSConnectionPool(host='api.github.com', port=443): 
Max retries exceeded with url: /repos/owner/repo/pulls (Caused by 
ConnectTimeoutError(<urllib3.connection.HTTPSConnection object at 
0x7f...>, 'Connection timed out after 30 seconds'))

หลังจากแก้ปัญหานี้ได้ ผมตัดสินใจสร้าง MCP Server ขึ้นมาเพื่อจัดการเรื่องนี้ทั้งหมด โดยใช้ HolySheep AI เป็น Engine หลักในการวิเคราะห์โค้ด ซึ่งมีความเร็วตอบสนองน้อยกว่า 50ms และราคาถูกกว่าวิธีอื่นมาก

MCP Server คืออะไร และทำไมต้องใช้?

MCP (Model Context Protocol) Server คือ Bridge ที่เชื่อมต่อระหว่าง AI Model กับ External Tools ต่างๆ อย่าง GitHub API ให้สามารถทำงานร่วมกันได้อย่างมีประสิทธิภาพ ซึ่งช่วยให้เราสามารถ:

การติดตั้ง MCP Server

ก่อนอื่นให้เราสร้าง Project และติดตั้ง Dependencies กันก่อน:

# สร้างโฟลเดอร์สำหรับโปรเจกต์
mkdir mcp-github-reviewer
cd mcp-github-reviewer

สร้าง Virtual Environment

python -m venv venv source venv/bin/activate # สำหรับ Linux/Mac

หรือ venv\Scripts\activate # สำหรับ Windows

ติดตั้ง Dependencies

pip install fastapi uvicorn httpx python-dotenv pip install mcp PyJWT

สร้าง GitHub API Integration

ต่อไปเราจะสร้าง Module สำหรับเชื่อมต่อกับ GitHub API:

import httpx
import os
from typing import List, Dict, Optional

class GitHubAPIClient:
    def __init__(self, token: str):
        self.token = token
        self.base_url = "https://api.github.com"
        self.headers = {
            "Authorization": f"Bearer {token}",
            "Accept": "application/vnd.github.v3+json",
            "X-GitHub-Api-Version": "2022-11-28"
        }
    
    async def get_pull_request(self, owner: str, repo: str, pr_number: int) -> Dict:
        """ดึงข้อมูล Pull Request"""
        async with httpx.AsyncClient(timeout=30.0) as client:
            url = f"{self.base_url}/repos/{owner}/{repo}/pulls/{pr_number}"
            response = await client.get(url, headers=self.headers)
            
            if response.status_code == 401:
                raise Exception("401 Unauthorized: GitHub Token ไม่ถูกต้องหรือหมดอายุ")
            elif response.status_code == 404:
                raise Exception(f"404 Not Found: PR #{pr_number} ไม่พบใน Repository")
            
            response.raise_for_status()
            return response.json()
    
    async def get_pr_files(self, owner: str, repo: str, pr_number: int) -> List[Dict]:
        """ดึงไฟล์ที่ถูกแก้ไขใน Pull Request"""
        async with httpx.AsyncClient(timeout=60.0) as client:
            url = f"{self.base_url}/repos/{owner}/{repo}/pulls/{pr_number}/files"
            response = await client.get(url, headers=self.headers)
            response.raise_for_status()
            return response.json()
    
    async def get_file_content(self, owner: str, repo: str, path: str, ref: str) -> str:
        """ดึงเนื้อหาของไฟล์"""
        async with httpx.AsyncClient(timeout=30.0) as client:
            url = f"{self.base_url}/repos/{owner}/{repo}/contents/{path}"
            params = {"ref": ref}
            response = await client.get(url, headers=self.headers, params=params)
            response.raise_for_status()
            import base64
            data = response.json()
            return base64.b64decode(data["content"]).decode("utf-8")
    
    async def create_review_comment(self, owner: str, repo: str, pr_number: int, 
                                   body: str, commit_id: str, path: str, 
                                   line: Optional[int] = None) -> Dict:
        """สร้าง Comment บน Pull Request"""
        async with httpx.AsyncClient(timeout=30.0) as client:
            url = f"{self.base_url}/repos/{owner}/{repo}/pulls/{pr_number}/comments"
            
            comment_data = {
                "body": body,
                "commit_id": commit_id,
                "path": path
            }
            
            if line:
                comment_data["line"] = line
                comment_data["side"] = "RIGHT"
            
            response = await client.post(url, headers=self.headers, json=comment_data)
            response.raise_for_status()
            return response.json()

สร้าง MCP Server สำหรับ Code Review

ต่อไปจะเป็นหัวใจหลักของระบบ นั่นคือ MCP Server ที่ใช้ HolySheep AI ในการวิเคราะห์โค้ด:

import httpx
from mcp.server import Server
from mcp.types import Tool, CallToolResult
from pydantic import AnyUrl
import json

ตั้งค่า HolySheep AI

HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1" HOLYSHEEP_API_KEY = os.getenv("YOUR_HOLYSHEEP_API_KEY") server = Server("github-code-reviewer") github_client = GitHubAPIClient(os.getenv("GITHUB_TOKEN")) @server.list_tools() async def list_tools() -> List[Tool]: """ประกาศ Tools ที่ MCP Server รองรับ""" return [ Tool( name="review_pull_request", description="วิเคราะห์ Pull Request และให้คำแนะนำ", inputSchema={ "type": "object", "properties": { "owner": {"type": "string", "description": "ชื่อ Owner ของ Repository"}, "repo": {"type": "string", "description": "ชื่อ Repository"}, "pr_number": {"type": "integer", "description": "หมายเลข Pull Request"} }, "required": ["owner", "repo", "pr_number"] } ), Tool( name="get_pr_diff", description="ดึงการเปลี่ยนแปลงของ Pull Request", inputSchema={ "type": "object", "properties": { "owner": {"type": "string"}, "repo": {"type": "string"}, "pr_number": {"type": "integer"} }, "required": ["owner", "repo", "pr_number"] } ) ] @server.call_tool() async def call_tool(name: str, arguments: dict) -> CallToolResult: """ประมวลผล Tool Calls""" if name == "review_pull_request": return await review_pull_request( arguments["owner"], arguments["repo"], arguments["pr_number"] ) elif name == "get_pr_diff": return await get_pr_diff( arguments["owner"], arguments["repo"], arguments["pr_number"] ) raise ValueError(f"Unknown tool: {name}") async def review_pull_request(owner: str, repo: str, pr_number: int) -> CallToolResult: """วิเคราะห์ Pull Request ด้วย HolySheep AI""" # ดึงข้อมูล PR และไฟล์ที่เปลี่ยนแปลง pr_data = await github_client.get_pull_request(owner, repo, pr_number) pr_files = await github_client.get_pr_files(owner, repo, pr_number) # รวบรวมข้อมูลสำหรับวิเคราะห์ files_content = [] for file in pr_files[:10]: # จำกัด 10 ไฟล์แรกเพื่อประหยัด Cost files_content.append({ "filename": file["filename"], "status": file["status"], "additions": file["additions"], "deletions": file["deletions"], "patch": file.get("patch", "") }) # ส่งไปวิเคราะห์ที่ HolySheep AI prompt = f"""คุณคือ Senior Code Reviewer ที่มีประสบการณ์มากกว่า 10 ปี จงวิเคราะห์ Pull Request นี้และให้คำแนะนำ: Title: {pr_data['title']} Description: {pr_data.get('body', 'No description')} Files Changed: {json.dumps(files_content, indent=2, ensure_ascii=False)} ให้คะแนนในแต่ละหมวด: 1. Code Quality (1-10) 2. Security Issues (1-10, สูง = ปลอดภัย) 3. Performance Impact (1-10) 4. Overall Recommendation (Approve/Request Changes/Comment) พร้อมรายละเอียดปัญหาที่พบ (ถ้ามี) ในรูปแบบ: - [file] บรรทัดที่ X: ปัญหา - คำแนะนำ """ try: async with httpx.AsyncClient(timeout=120.0) as client: response = await client.post( f"{HOLYSHEEP_BASE_URL}/chat/completions", headers={ "Authorization": f"Bearer {HOLYSHEEP_API_KEY}", "Content-Type": "application/json" }, json={ "model": "gpt-4.1", "messages": [ {"role": "system", "content": "You are an expert code reviewer."}, {"role": "user", "content": prompt} ], "temperature": 0.3 } ) if response.status_code == 401: return CallToolResult( content=[{"type": "text", "text": "❌ ข้อผิดพลาด: HolySheep API Key ไม่ถูกต้อง"}], isError=True ) response.raise_for_status() result = response.json() review_text = result["choices"][0]["message"]["content"] return CallToolResult( content=[{"type": "text", "text": review_text}] ) except httpx.TimeoutException: return CallToolResult( content=[{"type": "text", "text": "❌ ConnectionError: timeout - HolySheheep AI ไม่ตอบสนองภายใน 120 วินาที"}], isError=True ) async def get_pr_diff(owner: str, repo: str, pr_number: int) -> CallToolResult: """ดึง Diff ของ Pull Request""" pr_files = await github_client.get_pr_files(owner, repo, pr_number) diff_text = f"## Pull Request #{pr_number}\n\n" diff_text += f"Total Files: {len(pr_files)}\n\n" for file in pr_files: diff_text += f"### {file['filename']} ({file['status']})\n" diff_text += f"Additions: +{file['additions']} | Deletions: -{file['deletions']}\n\n" if file.get('patch'): diff_text += f"``diff\n{file['patch']}\n``\n\n" return CallToolResult(content=[{"type": "text", "text": diff_text}])

สร้าง FastAPI Server สำหรับ Webhook

เพื่อให้ระบบทำงานอัตโนมัติเมื่อมี PR ใหม่ เราจะสร้าง Webhook Endpoint:

from fastapi import FastAPI, Request, HTTPException
from pydantic import BaseModel
import hmac
import hashlib

app = FastAPI(title="GitHub Code Reviewer API")

class ReviewRequest(BaseModel):
    owner: str
    repo: str
    pr_number: int

@app.post("/webhook/github")
async def github_webhook(request: Request):
    """Webhook สำหรับรับ Events จาก GitHub"""
    
    # ตรวจสอบ GitHub Webhook Signature
    signature = request.headers.get("X-Hub-Signature-256")
    if signature:
        body = await request.body()
        secret = os.getenv("GITHUB_WEBHOOK_SECRET", "").encode()
        expected_signature = "sha256=" + hmac.new(
            secret, body, hashlib.sha256
        ).hexdigest()
        
        if not hmac.compare_digest(signature, expected_signature):
            raise HTTPException(status_code=401, detail="Invalid signature")
    
    # ดึงข้อมูล Event
    event = request.headers.get("X-GitHub-Event", "ping")
    payload = await request.json()
    
    if event == "pull_request":
        action = payload.get("action")
        pr = payload.get("pull_request")
        
        if action in ["opened", "reopened", "synchronize"]:
            # Trigger Review อัตโนมัติ
            from mcp.server.stdio import create_server
            import asyncio
            
            # เรียก MCP Server เพื่อทำ Review
            result = await review_pull_request(
                payload["repository"]["owner"]["login"],
                payload["repository"]["name"],
                pr["number"]
            )
            
            # ส่ง Review กลับไปที่ GitHub
            for content in result.content:
                if hasattr(content, 'text'):
                    await github_client.create_review_comment(
                        owner=payload["repository"]["owner"]["login"],
                        repo=payload["repository"]["name"],
                        pr_number=pr["number"],
                        body=content.text,
                        commit_id=pr["head"]["sha"],
                        path="",  # General comment
                        line=None
                    )
            
            return {"status": "review_completed", "pr": pr["number"]}