2abet commited on
Commit
7a0e1f3
·
verified ·
1 Parent(s): 2f8deda

Initial commit

Browse files
Files changed (8) hide show
  1. Dockerfile +10 -0
  2. README.md +139 -14
  3. __init__.py +0 -0
  4. app.py +82 -0
  5. github_client.py +51 -0
  6. main.py +36 -0
  7. models.py +20 -0
  8. requirements.txt +6 -0
Dockerfile ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /code
4
+
5
+ COPY ./requirements.txt /code/requirements.txt
6
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
7
+
8
+ COPY ./ /code/
9
+
10
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,14 +1,139 @@
1
- ---
2
- title: Github Mcp Pr
3
- emoji: 🌖
4
- colorFrom: gray
5
- colorTo: green
6
- sdk: gradio
7
- sdk_version: 5.33.1
8
- app_file: app.py
9
- pinned: false
10
- license: mit
11
- short_description: It can helps users find potential PR opportunity
12
- ---
13
-
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: MCP GitHub PR Opportunity Server
3
+ emoji: 🔍
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ sdk_version: 5.33.1
8
+ app_file: app.py
9
+ pinned: false
10
+ tag: mcp-server-track
11
+ license: mit
12
+ ---
13
+
14
+ # MCP GitHub PR Opportunity Server
15
+
16
+ A Gradio-based MCP server that searches GitHub repositories for possible PR opportunities (open issues labeled 'good first issue' or 'help wanted'), supporting search by keyword and topic, with authentication.
17
+
18
+ ## Features
19
+ - Search GitHub repos by keyword and topic
20
+ - Find open issues labeled 'good first issue' or 'help wanted'
21
+ - Supports GitHub authentication via token
22
+ - Gradio MCP server for LLM integration
23
+ - Interactive web interface
24
+ - Compatible with n8n workflows
25
+
26
+ ## Local Setup
27
+
28
+ 1. **Clone the repo and install dependencies:**
29
+ ```bash
30
+ cd mcp_github_server
31
+ pip install -r requirements.txt
32
+ ```
33
+
34
+ 2. **Configure GitHub Token:**
35
+ - Set `GITHUB_TOKEN` in your environment variables, or
36
+ - Enter the token in the web interface
37
+
38
+ 3. **Run the server:**
39
+ ```bash
40
+ python app.py
41
+ ```
42
+
43
+ ## Hugging Face Spaces Deployment
44
+
45
+ 1. **Create a new Space:**
46
+ - Go to [Hugging Face Spaces](https://huggingface.co/spaces)
47
+ - Click "Create new Space"
48
+ - Choose "Gradio" as the SDK
49
+ - Name your space (e.g., "mcp-github-pr-server")
50
+
51
+ 2. **Configure Environment Variables:**
52
+ - In your Space settings, add `GITHUB_TOKEN` as a secret
53
+ - Set your GitHub token as the value
54
+
55
+ 3. **Deploy:**
56
+ - Push your code to the Space repository
57
+ - Hugging Face will automatically build and deploy your application
58
+
59
+ ## Using with MCP Clients
60
+
61
+ Add this configuration to your MCP client (e.g., Claude Desktop, Cursor, or Cline):
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "github-pr-finder": {
67
+ "url": "https://akinyemiar-mcp-github-pr-server.hf.space/gradio_api/mcp/sse"
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ ## Using with n8n
74
+
75
+ 1. **Add HTTP Request Node:**
76
+ - In your n8n workflow, add an "HTTP Request" node
77
+ - Set the URL to your Hugging Face Space URL (e.g., `https://akinyemiar-mcp-github-pr-server.hf.space/api/predict`)
78
+ - Set Method to POST
79
+ - Add Headers:
80
+ ```
81
+ Content-Type: application/json
82
+ ```
83
+ - Set Body to JSON:
84
+ ```json
85
+ {
86
+ "data": [
87
+ "fastapi",
88
+ "python",
89
+ 5,
90
+ 1,
91
+ "YOUR_GITHUB_TOKEN"
92
+ ]
93
+ }
94
+ ```
95
+
96
+ 2. **Process Response:**
97
+ - The response will contain opportunities and total_count
98
+ - Use n8n's "Set" node to process the response data
99
+ - Add additional nodes to handle the opportunities as needed
100
+
101
+ ## API Usage
102
+
103
+ The server provides both a web interface and an API endpoint at `/api/predict`. The API accepts POST requests with the following parameters:
104
+
105
+ ```json
106
+ {
107
+ "data": [
108
+ "keyword", // optional
109
+ "topic", // optional
110
+ "per_page", // default: 5
111
+ "page", // default: 1
112
+ "token" // GitHub token
113
+ ]
114
+ }
115
+ ```
116
+
117
+ Response:
118
+ ```json
119
+ {
120
+ "data": [
121
+ {
122
+ "opportunities": [
123
+ {
124
+ "repo_name": "tiangolo/fastapi",
125
+ "repo_url": "https://github.com/tiangolo/fastapi",
126
+ "issue_title": "Add more examples",
127
+ "issue_url": "https://github.com/tiangolo/fastapi/issues/1234",
128
+ "issue_labels": ["good first issue"],
129
+ "issue_body": "Please add more examples to the docs..."
130
+ }
131
+ ],
132
+ "total_count": 123
133
+ }
134
+ ]
135
+ }
136
+ ```
137
+
138
+ ## License
139
+ MIT
__init__.py ADDED
File without changes
app.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from github_client import GitHubClient
3
+ from models import SearchRequest, SearchResponse
4
+ import os
5
+ from typing import Optional, Dict, Any, List
6
+
7
+
8
+ def search_pr_opportunities(
9
+ keyword: Optional[str] = None,
10
+ topic: Optional[str] = None,
11
+ per_page: int = 5,
12
+ page: int = 1,
13
+ token: Optional[str] = None
14
+ ) -> Dict[str, Any]:
15
+ """Search GitHub for PR opportunities.
16
+
17
+ Args:
18
+ keyword: Keyword to search in repositories (optional)
19
+ topic: Topic to filter repositories (optional)
20
+ per_page: Number of results per page (default: 5)
21
+ page: Page number (default: 1)
22
+ token: GitHub token for authentication
23
+
24
+ Returns:
25
+ A dictionary containing opportunities and total count
26
+ """
27
+ if not token:
28
+ token = os.getenv("GITHUB_TOKEN")
29
+ if not token:
30
+ raise gr.Error("GitHub token required in Authorization header or .env")
31
+
32
+ try:
33
+ client: GitHubClient = GitHubClient(token)
34
+ opportunities, total_count = client.find_pr_opportunities(
35
+ keyword=keyword,
36
+ topic=topic,
37
+ per_page=per_page,
38
+ page=page
39
+ )
40
+
41
+ # The return structure matches a dictionary format, so we type it as Dict[str, Any]
42
+ response: Dict[str, Any] = {
43
+ "opportunities": [
44
+ {
45
+ "repo_name": opp.repo_name,
46
+ "repo_url": opp.repo_url,
47
+ "issue_title": opp.issue_title,
48
+ "issue_url": opp.issue_url,
49
+ "issue_labels": opp.issue_labels,
50
+ "issue_body": opp.issue_body
51
+ } for opp in opportunities
52
+ ],
53
+ "total_count": total_count
54
+ }
55
+ return response
56
+ except Exception as e:
57
+ raise gr.Error(str(e))
58
+
59
+ # Create the Gradio interface
60
+ demo: gr.Interface = gr.Interface(
61
+ fn=search_pr_opportunities,
62
+ inputs=[
63
+ gr.Textbox(label="Keyword", placeholder="e.g., fastapi"),
64
+ gr.Textbox(label="Topic", placeholder="e.g., python"),
65
+ gr.Slider(minimum=1, maximum=20, value=5, step=1, label="Results per page"),
66
+ gr.Slider(minimum=1, maximum=10, value=1, step=1, label="Page number"),
67
+ gr.Textbox(label="GitHub Token", type="password")
68
+ ],
69
+ outputs=gr.JSON(label="Search Results"),
70
+ title="GitHub PR Opportunity Finder",
71
+ description="Search GitHub repositories for PR opportunities (issues labeled 'good first issue' or 'help wanted')",
72
+ examples=[
73
+ ["fastapi", "python", 5, 1, ""],
74
+ ["react", "javascript", 3, 1, ""],
75
+ ["machine-learning", None, 2, 1, ""],
76
+ [None, "web-development", 4, 1, ""]
77
+ ]
78
+ )
79
+
80
+ # Launch with MCP server enabled
81
+ if __name__ == "__main__":
82
+ demo.launch(mcp_server=True)
github_client.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from github import Github
3
+ from typing import List, Optional
4
+ try:
5
+ from .models import PROpportunity
6
+ except ImportError:
7
+ from models import PROpportunity
8
+
9
+ HELP_LABELS = ["good first issue", "help wanted"]
10
+
11
+ class GitHubClient:
12
+ def __init__(self, token: Optional[str] = None):
13
+ self.token = token or os.getenv("GITHUB_TOKEN")
14
+ if not self.token:
15
+ raise ValueError("GitHub token must be provided via argument or GITHUB_TOKEN env var.")
16
+ self.client = Github(self.token)
17
+
18
+ def search_repositories(self, keyword: Optional[str], topic: Optional[str], per_page: int, page: int):
19
+ query = []
20
+ if keyword:
21
+ query.append(keyword)
22
+ if topic:
23
+ query.append(f"topic:{topic}")
24
+ query_str = " ".join(query)
25
+ return self.client.search_repositories(query_str, sort="stars", order="desc")
26
+
27
+ def find_pr_opportunities(self, keyword: Optional[str], topic: Optional[str], per_page: int = 10, page: int = 1) -> List[PROpportunity]:
28
+ repos = self.search_repositories(keyword, topic, per_page, page)
29
+ opportunities = []
30
+ count = 0
31
+ for repo in repos.get_page(page-1):
32
+ issues = repo.get_issues(state="open", labels=HELP_LABELS)
33
+ for issue in issues:
34
+ if issue.pull_request is not None:
35
+ continue # skip PRs
36
+ labels = [l.name for l in issue.labels]
37
+ if any(l.lower() in HELP_LABELS for l in labels):
38
+ opportunities.append(PROpportunity(
39
+ repo_name=repo.full_name,
40
+ repo_url=repo.html_url,
41
+ issue_title=issue.title,
42
+ issue_url=issue.html_url,
43
+ issue_labels=labels,
44
+ issue_body=issue.body
45
+ ))
46
+ count += 1
47
+ if count >= per_page:
48
+ break
49
+ if count >= per_page:
50
+ break
51
+ return opportunities, repos.totalCount
main.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Depends, HTTPException, Header
2
+ from typing import Optional
3
+ try:
4
+ from .models import SearchRequest, SearchResponse
5
+ from .github_client import GitHubClient
6
+ except ImportError:
7
+ from models import SearchRequest, SearchResponse
8
+ from github_client import GitHubClient
9
+ import os
10
+
11
+ app = FastAPI(title="MCP GitHub PR Opportunity Server")
12
+
13
+ def get_github_token(authorization: Optional[str] = Header(None)) -> str:
14
+ if authorization and authorization.startswith("token "):
15
+ return authorization.split(" ", 1)[1]
16
+ token = os.getenv("GITHUB_TOKEN")
17
+ if not token:
18
+ raise HTTPException(status_code=401, detail="GitHub token required in Authorization header or .env")
19
+ return token
20
+
21
+ @app.post("/search_pr_opportunities", response_model=SearchResponse)
22
+ def search_pr_opportunities(
23
+ request: SearchRequest,
24
+ token: str = Depends(get_github_token)
25
+ ):
26
+ try:
27
+ client = GitHubClient(token)
28
+ opportunities, total_count = client.find_pr_opportunities(
29
+ keyword=request.keyword,
30
+ topic=request.topic,
31
+ per_page=request.per_page,
32
+ page=request.page
33
+ )
34
+ return SearchResponse(opportunities=opportunities, total_count=total_count)
35
+ except Exception as e:
36
+ raise HTTPException(status_code=500, detail=str(e))
models.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import List, Optional
3
+
4
+ class SearchRequest(BaseModel):
5
+ keyword: Optional[str] = Field(None, description="Keyword to search in repositories.")
6
+ topic: Optional[str] = Field(None, description="Topic to filter repositories.")
7
+ per_page: int = Field(10, description="Results per page.")
8
+ page: int = Field(1, description="Page number.")
9
+
10
+ class PROpportunity(BaseModel):
11
+ repo_name: str
12
+ repo_url: str
13
+ issue_title: str
14
+ issue_url: str
15
+ issue_labels: List[str]
16
+ issue_body: Optional[str]
17
+
18
+ class SearchResponse(BaseModel):
19
+ opportunities: List[PROpportunity]
20
+ total_count: int
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio[mcp]
2
+ PyGithub
3
+ python-dotenv
4
+ pydantic
5
+ mcp
6
+ gradio