server.pyā¢15.4 kB
"""GitHub MCP Server - Download and analyze GitHub repository files."""
import os
import re
import requests
import json
from typing import Dict, List, Any, Optional
from urllib.parse import urlparse, unquote
from fastmcp import FastMCP
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
APP_NAME = "github-file-analyzer"
APP_DESCRIPTION = "MCP server for downloading and analyzing GitHub repository files."
app = FastMCP(name=APP_NAME, instructions=APP_DESCRIPTION)
# API configuration
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
GITHUB_API_BASE = "https://api.github.com"
RAW_CONTENT_BASE = "https://raw.githubusercontent.com"
OPENAI_API_BASE = "https://api.openai.com/v1"
# Set up headers for GitHub API
HEADERS = {
"Accept": "application/vnd.github.v3+json",
"User-Agent": "github-file-analyzer/1.0"
}
if GITHUB_TOKEN:
HEADERS["Authorization"] = f"token {GITHUB_TOKEN}"
# Set up headers for OpenAI API
OPENAI_HEADERS = {
"Content-Type": "application/json",
"Authorization": f"Bearer {OPENAI_API_KEY}"
}
def parse_github_url(url: str) -> Optional[Dict[str, str]]:
"""Parse GitHub URL to extract repository information.
Args:
url: GitHub URL (blob, raw, or api format)
Returns:
Dictionary with parsed URL components or None if invalid
"""
try:
# Handle raw.githubusercontent.com URLs
if "raw.githubusercontent.com" in url:
parts = url.replace("https://raw.githubusercontent.com/", "").split("/")
if len(parts) >= 3:
return {
"owner": parts[0],
"repo": parts[1],
"branch": parts[2],
"file_path": "/".join(parts[3:]) if len(parts) > 3 else "",
"url_type": "raw"
}
# Handle github.com/blob URLs
elif "github.com" in url and "/blob/" in url:
# Extract path after github.com
path_match = re.search(r'github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.+)', url)
if path_match:
return {
"owner": path_match.group(1),
"repo": path_match.group(2),
"branch": path_match.group(3),
"file_path": path_match.group(4),
"url_type": "blob"
}
# Handle github.com API URLs
elif "api.github.com" in url and "/contents/" in url:
# Extract from API URL pattern
path_match = re.search(r'api\.github\.com/repos/([^/]+)/([^/]+)/contents/(.+)', url)
if path_match:
return {
"owner": path_match.group(1),
"repo": path_match.group(2),
"file_path": path_match.group(3),
"url_type": "api"
}
return None
except Exception:
return None
def get_raw_file_url(owner: str, repo: str, branch: str, file_path: str) -> str:
"""Convert GitHub URL components to raw content URL."""
return f"{RAW_CONTENT_BASE}/{owner}/{repo}/{branch}/{file_path}"
def download_file_content(url: str) -> Dict[str, Any]:
"""Download content from a GitHub file URL.
Args:
url: GitHub file URL
Returns:
Dictionary containing file content and metadata
"""
try:
# Parse the URL
parsed = parse_github_url(url)
if not parsed:
return {
"success": False,
"error": f"Invalid GitHub URL format: {url}",
"url": url
}
# Get raw content URL
if parsed["url_type"] == "raw":
raw_url = url
else:
raw_url = get_raw_file_url(
parsed["owner"],
parsed["repo"],
parsed["branch"],
parsed["file_path"]
)
# Download the file content
response = requests.get(raw_url, headers=HEADERS, timeout=30)
response.raise_for_status()
# Get file information
filename = parsed["file_path"].split("/")[-1] if parsed["file_path"] else "unknown"
file_extension = filename.split(".")[-1] if "." in filename else ""
return {
"success": True,
"content": response.text,
"metadata": {
"filename": filename,
"file_path": parsed["file_path"],
"owner": parsed["owner"],
"repo": parsed["repo"],
"branch": parsed["branch"],
"file_extension": file_extension,
"size_bytes": len(response.content),
"size_chars": len(response.text),
"url": url,
"raw_url": raw_url
}
}
except requests.exceptions.RequestException as e:
return {
"success": False,
"error": f"Failed to download file: {str(e)}",
"url": url
}
except Exception as e:
return {
"success": False,
"error": f"Unexpected error: {str(e)}",
"url": url
}
@app.tool()
def download_github_files(urls: List[str]) -> Dict[str, Any]:
"""Download content from multiple GitHub file URLs.
Args:
urls: List of GitHub file URLs (supports blob, raw, and API formats)
Returns:
Dictionary containing downloaded files with content and metadata
"""
if not urls:
return {"error": "No URLs provided"}
if len(urls) > 20: # Reasonable limit
return {"error": "Too many URLs provided (max 20)"}
results = []
successful_downloads = 0
failed_downloads = 0
for url in urls:
if not url.strip():
continue
result = download_file_content(url.strip())
results.append(result)
if result["success"]:
successful_downloads += 1
else:
failed_downloads += 1
return {
"summary": {
"total_urls": len(urls),
"successful_downloads": successful_downloads,
"failed_downloads": failed_downloads,
"success_rate": f"{(successful_downloads / len(urls) * 100):.1f}%" if urls else "0%"
},
"files": results
}
# Style-based code review functionality
ACCEPTED_STYLES = {
"trump": "Speak like Donald Trump - use superlatives constantly ('the best', 'tremendous', 'nobody knows more than me'), be extremely confident and boastful, frequently mention how great things are, use simple but emphatic language, and occasionally go off on tangents about how amazing everything you do is.",
"shakespeare": "Write in Shakespearean English - use thee, thou, thy, wherefore, hither, and other Early Modern English terms. Speak in iambic pentameter when possible, use elaborate metaphors and poetic flourishes, reference the stars and fate, and express everything with dramatic theatrical flair.",
"pirate": "Talk like a stereotypical pirate - use 'arr', 'matey', 'ye', 'aye', mention the seven seas, treasure, plunder, and sailing. Pepper speech with nautical terms, threaten to make people walk the plank, and be generally rowdy and swashbuckling.",
"chaucer": "Write in the style of Geoffrey Chaucer's Middle English - use archaic spellings like 'whan', 'tellen', 'maken', frequent use of 'y-' prefix for past participles. Be verbose and tell everything as if it's part of a grand Canterbury Tale, with moral undertones and references to pilgrims, knights, and medieval life.",
"cowboy": "Speak like an Old West cowboy - use 'partner', 'reckon', 'yonder', 'howdy', reference cattle, horses, and the dusty trail. Use folksy wisdom, be laconic and direct, and pepper speech with western idioms like 'rode hard and put away wet', 'this ain't my first rodeo'.",
"yoda": "Speak like Yoda from Star Wars - invert sentence structure consistently, placing verbs and objects before subjects. Use wise and cryptic phrasing, reference the Force frequently, speak slowly and thoughtfully, use 'hmm' and 'yes' often. Example: 'Strong with the Force, this code is.'",
"tupac": "Write in Tupac Shakur's style - use 90s hip-hop slang, be poetic yet street-smart, reference the struggle and keeping it real. Be introspective and philosophical while maintaining street credibility. Reference thug life, the streets, and staying true to yourself. Use rhythmic, almost rap-like phrasing.",
"venture-capitalist": "Speak like a Silicon Valley venture capitalist - constantly talk about disruption, synergy, paradigm shifts, hockey stick growth, and unicorns. Obsess over market size, scalability, and ROI. Ask about the moat, the go-to-market strategy, and whether this will 10x. Use phrases like 'circle back', 'double-click on that', and 'move the needle'.",
"corporate-speak": "Use maximum corporate jargon - leverage synergies, circle back, touch base, move the needle, shift paradigms, think outside the box, drill down, get our ducks in a row, low-hanging fruit, boil the ocean. Make everything sound important and strategic while saying very little of actual substance. Schedule follows ups and action items for everything.",
"gen-z-slang": "Talk in Gen Z internet slang - use 'no cap', 'fr fr', 'slaps', 'hits different', 'bussin', 'sheesh', 'based', 'mid', 'ate and left no crumbs', 'understood the assignment', 'main character energy', 'it's giving...', 'the way that...', and skull emoji energy. Be extremely online and reference TikTok culture."
}
def generate_style_review(code: str, style: str = "trump") -> str:
"""Generate a code review in the specified style using OpenAI API.
Args:
code: The code to review
style: The speaking style to use
Returns:
A styled code review
"""
if style.lower() not in ACCEPTED_STYLES:
return f"Invalid style '{style}'. Available styles: {', '.join(ACCEPTED_STYLES.keys())}"
if not OPENAI_API_KEY:
return "ā OpenAI API key not configured. Please add OPENAI_API_KEY to your .env file."
style_description = ACCEPTED_STYLES[style.lower()]
try:
# Prepare the OpenAI API request
payload = {
"model": "gpt-4o-mini",
"messages": [
{
"role": "system",
"content": f"""You are a code reviewer. {style_description}
Review and comment on the provided code while maintaining this speaking style throughout. Be entertaining and stay fully in character. Provide:
1. Overall assessment of the code
2. Specific feedback on code quality, structure, and best practices
3. Suggestions for improvement
4. Any potential issues or bugs you notice
Keep the review detailed but concise, and maintain the character style consistently."""
},
{
"role": "user",
"content": f"Please review this code:\n\n```\n{code}\n```"
}
],
"max_tokens": 1000,
"temperature": 0.7
}
# Make the API request
response = requests.post(
f"{OPENAI_API_BASE}/chat/completions",
headers=OPENAI_HEADERS,
json=payload,
timeout=30
)
if response.status_code == 200:
data = response.json()
review_content = data["choices"][0]["message"]["content"]
# Format the review with metadata
formatted_review = f"""š **Code Review in {style.title()} Style** š
**Style Applied:** {style.title()}
**AI Model:** GPT-4o-mini
**Review:**
{review_content}
**Technical Details:**
- File Length: {len(code.splitlines())} lines
- Character Count: {len(code)} characters
- Complexity: {'High' if len(code) > 1000 else 'Medium' if len(code) > 500 else 'Low'}
"""
return formatted_review
else:
error_msg = f"OpenAI API error: {response.status_code} - {response.text}"
return f"ā Failed to generate review: {error_msg}"
except requests.exceptions.RequestException as e:
return f"ā Network error calling OpenAI API: {str(e)}"
except Exception as e:
return f"ā Error generating review: {str(e)}"
@app.tool()
def download_and_review_github_files(urls: List[str], style: str = "trump") -> Dict[str, Any]:
"""Download GitHub files and generate styled code reviews for each file.
Args:
urls: List of GitHub file URLs to download and review
style: Speaking style for the reviews (default: "trump")
Returns:
Dictionary containing downloaded files with styled reviews
"""
if not urls:
return {"error": "No URLs provided"}
if len(urls) > 10: # Reasonable limit for reviews
return {"error": "Too many URLs provided (max 10 for reviews)"}
# Validate style
if style.lower() not in ACCEPTED_STYLES:
return {
"error": f"Invalid style '{style}'. Available styles: {', '.join(ACCEPTED_STYLES.keys())}"
}
# Download files using existing functionality
download_result = download_github_files(urls)
if "error" in download_result:
return download_result
# Generate reviews for each successfully downloaded file
reviews = []
for file_data in download_result["files"]:
if file_data["success"]:
# Generate review for this file
review = generate_style_review(file_data["content"], style)
reviews.append({
"filename": file_data["metadata"]["filename"],
"file_path": file_data["metadata"]["file_path"],
"repository": f"{file_data['metadata']['owner']}/{file_data['metadata']['repo']}",
"file_extension": file_data["metadata"]["file_extension"],
"size_bytes": file_data["metadata"]["size_bytes"],
"review": review,
"style_used": style
})
else:
reviews.append({
"filename": "unknown",
"error": file_data["error"],
"review": f"ā Could not review this file due to download error: {file_data['error']}"
})
return {
"summary": {
"total_files": len(urls),
"successful_reviews": len([r for r in reviews if "error" not in r]),
"failed_reviews": len([r for r in reviews if "error" in r]),
"style_used": style,
"available_styles": list(ACCEPTED_STYLES.keys())
},
"reviews": reviews
}
@app.tool()
def list_review_styles() -> Dict[str, Any]:
"""List all available speaking styles for code reviews.
Returns:
Dictionary containing all available styles with descriptions
"""
return {
"available_styles": ACCEPTED_STYLES,
"total_styles": len(ACCEPTED_STYLES),
"default_style": "trump"
}
if __name__ == "__main__":
app.run()