-
Notifications
You must be signed in to change notification settings - Fork 114
Add GitHub MCP microservice for repository queries #131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e90de9c
60916e0
ea97cbe
cf4c24e
ff48364
28e4e48
c52a4bf
a247ba9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,101 @@ | ||||||||||||||||
| import logging | ||||||||||||||||
| import os | ||||||||||||||||
| from typing import Dict, Any, Optional, List, Union | ||||||||||||||||
| import aiohttp | ||||||||||||||||
| import asyncio | ||||||||||||||||
|
|
||||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||||
|
|
||||||||||||||||
| class GitHubMCPClient: | ||||||||||||||||
| """Client for communicating with the GitHub MCP server.""" | ||||||||||||||||
|
|
||||||||||||||||
| def __init__(self, mcp_server_url: str = "http://localhost:8001"): | ||||||||||||||||
| self.mcp_server_url = mcp_server_url | ||||||||||||||||
| self.session: Optional[aiohttp.ClientSession] = None | ||||||||||||||||
| # Default org pulled from environment | ||||||||||||||||
| self.org = os.getenv("GITHUB_ORG", "Aossie-org") | ||||||||||||||||
|
|
||||||||||||||||
| async def __aenter__(self): | ||||||||||||||||
| # Async context manager entry | ||||||||||||||||
| self.session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=15)) | ||||||||||||||||
| return self | ||||||||||||||||
|
|
||||||||||||||||
| async def __aexit__(self, exc_type, exc_val, exc_tb): | ||||||||||||||||
| # Async context manager exit | ||||||||||||||||
| if self.session: | ||||||||||||||||
| await self.session.close() | ||||||||||||||||
|
|
||||||||||||||||
| async def get_github_supp(self, repo: str, owner: Optional[str] = None) -> Dict[str, Any]: | ||||||||||||||||
| """ | ||||||||||||||||
| Fetch metadata for a single repository. | ||||||||||||||||
| Owner defaults to org from environment if not provided. | ||||||||||||||||
| """ | ||||||||||||||||
| if not self.session: | ||||||||||||||||
| raise RuntimeError("Client not initialized. Use async context manager.") | ||||||||||||||||
|
|
||||||||||||||||
| owner = owner or self.org | ||||||||||||||||
|
|
||||||||||||||||
| try: | ||||||||||||||||
| payload = {"owner": owner, "repo": repo} | ||||||||||||||||
|
|
||||||||||||||||
| async with self.session.post( | ||||||||||||||||
| f"{self.mcp_server_url}/github_support", | ||||||||||||||||
| json=payload, | ||||||||||||||||
| headers={"Content-Type": "application/json"}, | ||||||||||||||||
| ) as response: | ||||||||||||||||
| if response.status == 200: | ||||||||||||||||
| result = await response.json() | ||||||||||||||||
| if result.get("status") == "success": | ||||||||||||||||
| return result.get("data", {}) | ||||||||||||||||
| else: | ||||||||||||||||
| return {"error": result.get("error", "Unknown error")} | ||||||||||||||||
| else: | ||||||||||||||||
| logger.error(f"MCP server error: {response.status}") | ||||||||||||||||
| return {"error": f"MCP server error: {response.status}"} | ||||||||||||||||
|
|
||||||||||||||||
| except aiohttp.ClientError as e: | ||||||||||||||||
| logger.exception("Error communicating with MCP server: %s", e) | ||||||||||||||||
| return {"error": f"Communication error: {str(e)}"} | ||||||||||||||||
| except Exception as e: | ||||||||||||||||
| logger.exception("Unexpected error: %s", e) | ||||||||||||||||
| return {"error": f"Unexpected error: {str(e)}"} | ||||||||||||||||
|
|
||||||||||||||||
| async def list_org_repos(self, org: str) -> Union[List[Dict[str, Any]], Dict[str, Any]]: | ||||||||||||||||
| if not self.session: | ||||||||||||||||
| raise RuntimeError("Client not initialized. Use async context manager.") | ||||||||||||||||
|
|
||||||||||||||||
| try: | ||||||||||||||||
| payload = {"org": org} | ||||||||||||||||
| async with self.session.post( | ||||||||||||||||
| f"{self.mcp_server_url}/list_org_repos", | ||||||||||||||||
| json=payload, | ||||||||||||||||
| headers={"Content-Type": "application/json"}, | ||||||||||||||||
| ) as response: | ||||||||||||||||
| if response.status == 200: | ||||||||||||||||
| result = await response.json() | ||||||||||||||||
| if result.get("status") == "success": | ||||||||||||||||
| return result.get("data", []) | ||||||||||||||||
| else: | ||||||||||||||||
| return {"error": result.get("error", "Unknown error")} | ||||||||||||||||
| else: | ||||||||||||||||
| logger.error(f"MCP server error: {response.status}") | ||||||||||||||||
| return {"error": f"MCP server error: {response.status}"} | ||||||||||||||||
| except aiohttp.ClientError as e: | ||||||||||||||||
| logger.error(f"Error communicating with MCP server: {e}") | ||||||||||||||||
| return {"error": f"Communication error: {str(e)}"} | ||||||||||||||||
| except Exception as e: | ||||||||||||||||
| logger.error(f"Unexpected error: {e}") | ||||||||||||||||
| return {"error": f"Unexpected error: {str(e)}"} | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| async def is_server_available(self) -> bool: | ||||||||||||||||
| """Health check for MCP server.""" | ||||||||||||||||
| if not self.session: | ||||||||||||||||
| return False | ||||||||||||||||
|
|
||||||||||||||||
| try: | ||||||||||||||||
| async with self.session.get(f"{self.mcp_server_url}/health", timeout=5) as response: | ||||||||||||||||
| return response.status == 200 | ||||||||||||||||
|
Comment on lines
+97
to
+98
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Request timeout misuse: aiohttp expects ClientTimeout, not an int. Passing timeout=5 raises TypeError at runtime. Use ClientTimeout or rely on the session default. - async with self.session.get(f"{self.mcp_server_url}/health", timeout=5) as response:
+ async with self.session.get(
+ f"{self.mcp_server_url}/health",
+ timeout=aiohttp.ClientTimeout(total=5),
+ ) as response:
return response.status == 200📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| except (aiohttp.ClientError, asyncio.TimeoutError) as e: | ||||||||||||||||
| logger.debug(f"Health check failed: {e}") | ||||||||||||||||
| return False | ||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| import os | ||
| import logging | ||
| import asyncio | ||
| from dotenv import load_dotenv, find_dotenv | ||
| from fastapi import FastAPI, HTTPException | ||
| from pydantic import BaseModel | ||
| from .github_mcp_service import GitHubMCPService | ||
| from typing import Optional | ||
|
|
||
| dotenv_path = find_dotenv(usecwd=True) | ||
| if dotenv_path: | ||
| load_dotenv(dotenv_path=dotenv_path) | ||
| else: | ||
| load_dotenv() | ||
|
|
||
| logging.basicConfig(level=logging.INFO) | ||
| logger = logging.getLogger(__name__) | ||
|
|
||
| app = FastAPI(title="GitHub MCP Server", version="1.0.0") | ||
|
|
||
| # Load env vars | ||
| GITHUB_ORG = os.getenv("GITHUB_ORG") | ||
| if not GITHUB_ORG: | ||
| logger.warning("GITHUB_ORG not set in .env — defaulting to manual owner input") | ||
|
|
||
| github_service: Optional[GitHubMCPService] = None | ||
| try: | ||
| token = os.getenv("GITHUB_TOKEN") or os.getenv("GH_TOKEN") | ||
| if not token: | ||
| logger.warning("GITHUB_TOKEN/GH_TOKEN not set; GitHub API calls may be rate-limited or fail.") | ||
| github_service = GitHubMCPService(token=token) | ||
| logger.info("GitHub service initialized successfully") | ||
| except Exception as e: | ||
| logger.exception("Failed to initialize GitHub service") | ||
| github_service = None | ||
|
|
||
| class RepoInfoRequest(BaseModel): | ||
| repo: str | ||
| owner: Optional[str] = None | ||
|
|
||
| class RepoInfoResponse(BaseModel): | ||
| status: str | ||
| data: dict | ||
| error: str = None | ||
|
|
||
| @app.get("/health") | ||
| async def health_check(): | ||
| """Health check endpoint""" | ||
| return {"status": "healthy", "service": "github-mcp"} | ||
|
|
||
| class OrgInfoRequest(BaseModel): | ||
| org: str | ||
|
|
||
| @app.post("/list_org_repos") | ||
| async def list_org_repos(request: OrgInfoRequest): | ||
| try: | ||
| if not github_service: | ||
| raise HTTPException(status_code=503, detail="GitHub service not available") | ||
|
|
||
| result = await asyncio.to_thread(github_service.list_org_repos, request.org) | ||
|
|
||
| if "error" in result: | ||
| return {"status": "error", "data": {}, "error": result["error"]} | ||
|
|
||
| return {"status": "success", "data": result} | ||
|
|
||
| except Exception as e: | ||
| logger.exception("Error listing org repos") | ||
| raise HTTPException(status_code=500, detail=str(e)) | ||
|
|
||
| @app.post("/github_support") | ||
| async def get_github_supp(request: RepoInfoRequest): | ||
| """Get repo details, using fixed org from env""" | ||
| if not github_service: | ||
| raise HTTPException(status_code=503, detail="GitHub service not available") | ||
| owner = request.owner or GITHUB_ORG | ||
| if not owner: | ||
| raise HTTPException(status_code=400, detail="Missing owner; provide 'owner' or set GITHUB_ORG") | ||
|
|
||
| try: | ||
| result = await asyncio.to_thread(github_service.repo_query, owner, request.repo) | ||
| if "error" in result: | ||
| return RepoInfoResponse(status="error", data={}, error=result["error"]) | ||
| return RepoInfoResponse(status="success", data=result) | ||
| except Exception as e: | ||
| logger.exception("Error getting repo info") | ||
| raise HTTPException(status_code=500, detail=str(e)) | ||
|
|
||
| if __name__ == "__main__": | ||
| import uvicorn | ||
| uvicorn.run(app, host="0.0.0.0", port=8001) |
Uh oh!
There was an error while loading. Please reload this page.