-
-
Notifications
You must be signed in to change notification settings - Fork 140
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 3 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 |
|---|---|---|
| @@ -1,9 +1,9 @@ | ||
| GITHUB_INTENT_ANALYSIS_PROMPT = """You are an expert GitHub DevRel AI assistant. Analyze the user query and classify the intent. | ||
|
|
||
| AVAILABLE FUNCTIONS: | ||
| - web_search: Search the web for information | ||
| - contributor_recommendation: Finding the right people to review PRs, assign issues, or collaborate (supports both issue URLs and general queries) | ||
| - repo_support: Questions about codebase structure, dependencies, impact analysis, architecture | ||
| - web_search: Search the web for general information not available through GitHub API | ||
|
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. No need to specify "not available through GitHub API", this might lead the agent to use GitHub API first, maybe, and might lead to hallucinations. It's best if it decides on its own. |
||
| - contributor_recommendation: Finding the right people to review PRs, assign issues, or collaborate | ||
| - repo_support: Questions about repository information, structure, stats, issues, stars, forks, description, or any repository metadata | ||
|
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. could you please rename this to somewhat like GitHub support? |
||
| - issue_creation: Creating bug reports, feature requests, or tracking items | ||
| - documentation_generation: Generating docs, READMEs, API docs, guides, or explanations | ||
| - find_good_first_issues: Finding beginner-friendly issues to work on across repositories | ||
|
|
@@ -12,24 +12,26 @@ | |
| USER QUERY: {user_query} | ||
|
|
||
| Classification guidelines: | ||
| - repo_support: ANY questions about repository information, stats, issues count, stars, forks, description, URL, or repository metadata. This includes "how many issues", "what are the stars", "repository details", etc. | ||
| - contributor_recommendation: | ||
| * "who should review this PR/issue?" | ||
| * "find experts in React/Python/ML" | ||
| * "recommend assignees for stripe integration" | ||
| * "best people for database optimization" | ||
| * URLs like github.com/owner/repo/issues/123 | ||
| * "I need help with RabbitMQ, can you suggest some people?" | ||
| - repo_support: Code structure, dependencies, impact analysis, architecture | ||
| - issue_creation: Creating bugs, features, tracking items | ||
| - documentation_generation: Docs, READMEs, guides, explanations | ||
| - find_good_first_issues: Beginners, newcomers, "good first issue" | ||
| - web_search: General information needing external search | ||
| - web_search: Only for information that cannot be found through GitHub API (like news, articles, external documentation) | ||
| - general_github_help: General GitHub questions not covered above | ||
|
|
||
| IMPORTANT: Repository information queries (issues count, stars, forks, description) should ALWAYS use repo_support, not web_search. | ||
|
|
||
| CRITICAL: Return ONLY raw JSON. No markdown, no code blocks, no explanation text. | ||
|
|
||
| {{ | ||
| "classification": "function_name_from_list_above", | ||
| "reasoning": "Brief explanation of why you chose this function", | ||
| "confidence": "high|medium|low" | ||
| }}""" | ||
| }}""" | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,71 @@ | ||||||||||||||||
| import logging | ||||||||||||||||
| from typing import Dict, Any, Optional | ||||||||||||||||
| 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 | ||||||||||||||||
|
|
||||||||||||||||
| 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_repo_info(self, owner: str, repo: str) -> Dict[str, Any]: | ||||||||||||||||
|
|
||||||||||||||||
| if not self.session: | ||||||||||||||||
| raise RuntimeError("Client not initialized. Use async context manager.") | ||||||||||||||||
|
|
||||||||||||||||
| try: | ||||||||||||||||
| payload = { | ||||||||||||||||
| "owner": owner, | ||||||||||||||||
| "repo": repo | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| async with self.session.post( | ||||||||||||||||
| f"{self.mcp_server_url}/repo_info", | ||||||||||||||||
| 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)}"} | ||||||||||||||||
|
Comment on lines
+56
to
+61
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. 🛠️ Refactor suggestion Use logging.exception and narrow excepts. Preserve tracebacks; avoid blanket Exception unless re-raised. - except aiohttp.ClientError as e:
- logger.error(f"Error communicating with MCP server: {e}")
+ 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.error(f"Unexpected error: {e}")
+ except Exception as e:
+ logger.exception("Unexpected error: %s", e)
return {"error": f"Unexpected error: {str(e)}"}Apply the same pattern to list_org_repos. Also applies to: 85-90 🧰 Tools🪛 Ruff (0.12.2)59-59: Use Replace with (TRY400) 60-60: Use explicit conversion flag Replace with conversion flag (RUF010) 61-61: Do not catch blind exception: (BLE001) 62-62: Use Replace with (TRY400) 63-63: Use explicit conversion flag Replace with conversion flag (RUF010) 🤖 Prompt for AI Agents |
||||||||||||||||
|
|
||||||||||||||||
| async def is_server_available(self) -> bool: | ||||||||||||||||
|
|
||||||||||||||||
| 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,96 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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.error(f"Failed to initialize GitHub service: {e}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| github_service = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+33
to
+35
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. 🛠️ Refactor suggestion Improve diagnostics: use logger.exception and chain errors. Capture stack traces and preserve causality. @@
-except Exception as e:
- logger.error(f"Failed to initialize GitHub service: {e}")
- github_service = None
+except Exception as e:
+ logger.exception("Failed to initialize GitHub service")
+ github_service = None
@@
- except Exception as e:
- logger.error(f"Error listing org repos: {e}")
- raise HTTPException(status_code=500, detail=str(e))
+ except Exception as e:
+ logger.exception("Error listing org repos")
+ raise HTTPException(status_code=502, detail="Upstream GitHub call failed") from e
@@
- except Exception as e:
- logger.error(f"Error getting repo info: {e}")
- raise HTTPException(status_code=500, detail=str(e))
+ except Exception as e:
+ logger.exception("Error getting repo info")
+ raise HTTPException(status_code=502, detail="Upstream GitHub call failed") from eAlso applies to: 65-67, 82-84 🧰 Tools🪛 Ruff (0.12.2)32-32: Do not catch blind exception: (BLE001) 33-33: Use Replace with (TRY400) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class RepoInfoRequest(BaseModel): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| owner: str | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| repo: str | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @app.post("/mcp") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def mcp_endpoint(request: dict): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """MCP protocol endpoint""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method = request.get("method") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| params = request.get("params", {}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if method == "tools/call": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tool_name = params.get("name") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arguments = params.get("arguments", {}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if tool_name == "get_repo_info": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| owner = arguments.get("owner") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| repo = arguments.get("repo") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not owner or not repo: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return {"error": "Missing owner or repo parameter"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not github_service: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return {"error": "GitHub service not available"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = github_service.repo_query(owner, repo) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return {"result": result} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return {"error": f"Unknown tool: {tool_name}"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return {"error": f"Unknown method: {method}"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.exception("Error in MCP endpoint") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return {"error": str(e)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @app.post("/repo_info") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def get_repo_info(request: RepoInfoRequest): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not github_service: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise HTTPException(status_code=500, detail="GitHub service not available") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = github_service.repo_query(request.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.error(f"Error getting repo info: {e}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise HTTPException(status_code=500, detail=str(e)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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. 💡 Verification agent 🧩 Analysis chainHarden error handling and avoid blocking in /repo_info.
Apply: @@
- if not github_service:
- raise HTTPException(status_code=500, detail="GitHub service not available")
+ if not github_service:
+ raise HTTPException(status_code=503, detail="GitHub service not available")
@@
- result = github_service.repo_query(request.owner, request.repo)
+ import asyncio
+ result = await asyncio.to_thread(github_service.repo_query, request.owner, request.repo)
@@
- except Exception as e:
- logger.error(f"Error getting repo info: {e}")
- raise HTTPException(status_code=500, detail=str(e))
+ except Exception as e:
+ logger.exception("Error getting repo info")
+ raise HTTPException(status_code=502, detail="Upstream GitHub service failed") from eIf you adopt the async service refactor, replace I’d like to verify whether 🏁 Script executed: #!/bin/bash
# Locate the definition of repo_query to determine if it’s sync or async
rg -nP "def repo_query" -C3 --glob="*.py"
# Find where github_service is instantiated or imported
rg -n "github_service" -C3 --glob="*.py"Length of output: 4539 Harden
Apply this refactor: @@ async def get_repo_info(request: RepoInfoRequest):
- if not github_service:
- raise HTTPException(status_code=500, detail="GitHub service not available")
+ if not github_service:
+ # Service not ready—respond with 503 Service Unavailable
+ raise HTTPException(status_code=503, detail="GitHub service not available")
- result = github_service.repo_query(request.owner, request.repo)
+ # Offload blocking I/O to thread pool
+ import asyncio
+ result = await asyncio.to_thread(
+ github_service.repo_query,
+ request.owner,
+ request.repo,
+ )
@@
- except Exception as e:
- logger.error(f"Error getting repo info: {e}")
- raise HTTPException(status_code=500, detail=str(e))
+ except Exception as e:
+ # Log full traceback and chain exception
+ logger.exception("Error fetching GitHub repo info")
+ # Upstream failure—respond with 502 Bad Gateway
+ raise HTTPException(
+ status_code=502,
+ detail="Failed to fetch repository information from GitHub",
+ ) from e📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.12.2)92-92: Within an (B904)
Contributor
Author
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. your current fix blocks the /repo_info to perform action leading to error 502 will keep using the same code
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. @DhruvK278, you're absolutely right to reject my suggestion. I apologize for the error - my analysis incorrectly assumed Your original synchronous code is working correctly, and you should definitely keep it as is. The current implementation handles the GitHub API calls appropriately for your use case. ✏️ Learnings added
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if __name__ == "__main__": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import uvicorn | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uvicorn.run(app, host="0.0.0.0", port=8001) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import os | ||
| import requests | ||
| from dotenv import load_dotenv | ||
|
|
||
| dotenv_path = os.path.join(os.path.dirname(__file__), '..', '..', '..', '..', '..', '.env') | ||
|
|
||
| load_dotenv(dotenv_path=dotenv_path) | ||
|
|
||
| class GitHubMCPService: | ||
| def __init__(self, token: str = None): | ||
| """ | ||
| Initializes the GitHub MCP Service. | ||
| It retrieves the GitHub token from the environment variables. | ||
| """ | ||
| self.token = token or os.getenv("GITHUB_TOKEN") | ||
| if not self.token: | ||
| raise ValueError("GitHub token required; export as GITHUB_TOKEN or place in backend/.env file") | ||
| self.base_url = "https://api.github.com" | ||
|
|
||
| def repo_query(self, owner: str, repo: str) -> dict: | ||
|
|
||
| url = f"{self.base_url}/repos/{owner}/{repo}" | ||
| headers = { | ||
| "Authorization": f"Bearer {self.token}", | ||
| "Accept": "application/vnd.github+json", | ||
| "X-GitHub-Api-Version": "2022-11-28", | ||
| "User-Agent": "DevrAI-GitHubMCPService/0.1" | ||
| } | ||
|
|
||
| try: | ||
| resp = requests.get(url, headers=headers, timeout=15) | ||
| resp.raise_for_status() | ||
| except requests.exceptions.RequestException as e: | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| return {"error": "Request failed", "message": str(e)} | ||
|
|
||
| data = resp.json() | ||
|
|
||
| license_info = data.get("license") | ||
| license_name = license_info.get("name") if license_info else "No license specified" | ||
|
|
||
| return { | ||
| # Core Info | ||
| "full_name": data.get("full_name"), | ||
| "description": data.get("description"), | ||
| "html_url": data.get("html_url"), | ||
| "homepage": data.get("homepage"), | ||
|
|
||
| # Stats | ||
| "stars": data.get("stargazers_count"), | ||
| "forks": data.get("forks_count"), | ||
| "watchers": data.get("watchers_count"), | ||
| "open_issues": data.get("open_issues_count"), | ||
|
|
||
| # Details | ||
| "language": data.get("language"), | ||
| "topics": data.get("topics", []), | ||
| "default_branch": data.get("default_branch"), | ||
| "license": license_name, | ||
|
|
||
| # Timestamps | ||
| "created_at": data.get("created_at"), | ||
| "updated_at": data.get("updated_at"), | ||
| "pushed_at": data.get("pushed_at"), | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| from ..services.github_mcp_client import GitHubMCPClient | ||
| import re | ||
|
|
||
| #GitHub URL forms: https(s)://github.com/owner/repo[.git][...], git@github.com:owner/repo[.git] | ||
| GH_URL_RE = re.compile( | ||
| r'(?:https?://|git@)github\.com[/:]' | ||
| r'([A-Za-z0-9](?:-?[A-Za-z0-9]){0,38})/' | ||
| r'([A-Za-z0-9._-]+?)(?:\.git)?(?:/|$)', | ||
| re.IGNORECASE, | ||
| ) | ||
|
|
||
| OWNER_REPO_RE = re.compile( | ||
| r'\b([A-Za-z0-9](?:-?[A-Za-z0-9]){0,38})/([A-Za-z0-9._-]{1,100})\b' | ||
| ) | ||
|
|
||
| async def handle_repo_query(user_query: str) -> dict: | ||
| m = GH_URL_RE.search(user_query) or OWNER_REPO_RE.search(user_query) | ||
| if not m: | ||
| return {"status": "error", "message": "Usage: include a GitHub owner/repo (e.g., AOSSIE-Org/Devr.AI) or a GitHub URL."} | ||
|
|
||
| owner, repo = m.group(1), m.group(2) | ||
|
|
||
| # Use the GitHub MCP client to communicate with the MCP server | ||
| async with GitHubMCPClient() as client: | ||
| if not await client.is_server_available(): | ||
| return { | ||
| "status": "error", | ||
| "message": "GitHub MCP server not available. Please ensure the MCP server is running." | ||
| } | ||
|
|
||
| result = await client.get_repo_info(owner, repo) | ||
|
|
||
| if "error" in result: | ||
| return { | ||
| "status": "error", | ||
| "owner": owner, | ||
| "repo": repo, | ||
| "message": result["error"] | ||
| } | ||
|
|
||
| return { | ||
| "status": "success", | ||
| "owner": owner, | ||
| "repo": repo, | ||
| "data": result | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you please elaborate on what this regex is for? seems to me like for extracting repo/org from the user query.
But won't it work without regex as used specifically?
A bit confused cuz github_support.py too has regex defined.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually this regex is different than github_support.py, github support regex extracts repo/org from user query but, this toolkit regex takes LLM response (react supervisor) and extract json payloads from it. without this the pipeline would fail if there is a slightly malformed response basically it is a safeguard for structured output parsing.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I get it. But the previous JSON logic works pretty well. Did you face any case where this didn't work? Initially, I faced cuz the model used to output ```json{} somewhat like a structure, but later it was fixed after changing the prompt. So, I guess no need for this. Can you please revert this change @DhruvK278 ?