github-talent-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- github_talent_mcp-0.1.0/LICENSE +21 -0
- github_talent_mcp-0.1.0/PKG-INFO +218 -0
- github_talent_mcp-0.1.0/README.md +189 -0
- github_talent_mcp-0.1.0/pyproject.toml +51 -0
- github_talent_mcp-0.1.0/setup.cfg +4 -0
- github_talent_mcp-0.1.0/src/github_talent_mcp/__init__.py +0 -0
- github_talent_mcp-0.1.0/src/github_talent_mcp/__main__.py +3 -0
- github_talent_mcp-0.1.0/src/github_talent_mcp/github_client.py +191 -0
- github_talent_mcp-0.1.0/src/github_talent_mcp/models.py +96 -0
- github_talent_mcp-0.1.0/src/github_talent_mcp/scoring.py +169 -0
- github_talent_mcp-0.1.0/src/github_talent_mcp/server.py +127 -0
- github_talent_mcp-0.1.0/src/github_talent_mcp/tools/__init__.py +0 -0
- github_talent_mcp-0.1.0/src/github_talent_mcp/tools/contributors.py +61 -0
- github_talent_mcp-0.1.0/src/github_talent_mcp/tools/profile.py +219 -0
- github_talent_mcp-0.1.0/src/github_talent_mcp/tools/rank.py +91 -0
- github_talent_mcp-0.1.0/src/github_talent_mcp/tools/search.py +43 -0
- github_talent_mcp-0.1.0/src/github_talent_mcp.egg-info/PKG-INFO +218 -0
- github_talent_mcp-0.1.0/src/github_talent_mcp.egg-info/SOURCES.txt +22 -0
- github_talent_mcp-0.1.0/src/github_talent_mcp.egg-info/dependency_links.txt +1 -0
- github_talent_mcp-0.1.0/src/github_talent_mcp.egg-info/entry_points.txt +2 -0
- github_talent_mcp-0.1.0/src/github_talent_mcp.egg-info/requires.txt +4 -0
- github_talent_mcp-0.1.0/src/github_talent_mcp.egg-info/top_level.txt +1 -0
- github_talent_mcp-0.1.0/tests/test_scoring.py +237 -0
- github_talent_mcp-0.1.0/tests/test_search.py +67 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Carolina Cherry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: github-talent-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for searching, scoring, and ranking GitHub developers for technical recruiting
|
|
5
|
+
Author: Carolina Cherry
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/carolinacherry/github-talent-mcp
|
|
8
|
+
Project-URL: Repository, https://github.com/carolinacherry/github-talent-mcp
|
|
9
|
+
Project-URL: Issues, https://github.com/carolinacherry/github-talent-mcp/issues
|
|
10
|
+
Keywords: mcp,github,recruiting,developer-tools,claude
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: mcp>=1.0.0
|
|
25
|
+
Requires-Dist: httpx>=0.27.0
|
|
26
|
+
Requires-Dist: pydantic>=2.0.0
|
|
27
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# github-talent-mcp
|
|
31
|
+
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
[](https://www.python.org)
|
|
34
|
+
[](https://modelcontextprotocol.io)
|
|
35
|
+
[](https://claude.ai)
|
|
36
|
+
[](https://docs.github.com/en/rest)
|
|
37
|
+
|
|
38
|
+
MCP server that searches, scores, and ranks GitHub developers for technical recruiting.
|
|
39
|
+
|
|
40
|
+
## Demo
|
|
41
|
+
|
|
42
|
+
https://github.com/user-attachments/assets/2dfd82b4-3eb5-4f2b-bc0a-2580b95043e4
|
|
43
|
+
|
|
44
|
+
### Profile deep dive
|
|
45
|
+
|
|
46
|
+
> Get the full developer profile and activity score for torvalds on GitHub
|
|
47
|
+
|
|
48
|
+
Claude calls `get_developer_profile("torvalds")` and returns:
|
|
49
|
+
|
|
50
|
+
| Field | Value |
|
|
51
|
+
|---|---|
|
|
52
|
+
| **Activity Score** | **150** (reputation floor applied) |
|
|
53
|
+
| Location | Portland, OR |
|
|
54
|
+
| Followers | 293,321 |
|
|
55
|
+
| Stars Received | 235,068 |
|
|
56
|
+
| Primary Language | C (98.1%) |
|
|
57
|
+
| Commits (90d) | 0 |
|
|
58
|
+
| PRs (90d) | 0 |
|
|
59
|
+
| Notable Repos | linux (183K stars), libdc-for-dirk, subsurface-for-dirk, uemacs, pesern-resolve |
|
|
60
|
+
| Profile README | No |
|
|
61
|
+
| Hireable | No |
|
|
62
|
+
|
|
63
|
+
Torvalds has zero recent GitHub activity because kernel development flows through mailing lists, not GitHub PRs. The **reputation floor** (293K followers) overrides the behavioral score and sets it to 150.
|
|
64
|
+
|
|
65
|
+
### Repo contributor ranking
|
|
66
|
+
|
|
67
|
+
> Get the top contributors to huggingface/transformers and rank them for a founding ML engineer role at an AI startup
|
|
68
|
+
|
|
69
|
+
Claude calls `get_repo_contributors("huggingface/transformers")` → `rank_candidates` on the top 24 contributors:
|
|
70
|
+
|
|
71
|
+
| Rank | Developer | Combined Score | Activity | Relevance | Strengths |
|
|
72
|
+
|---|---|---|---|---|---|
|
|
73
|
+
| 1 | stas00 | 83.4 | 150 | 72 | 4,553 stars, contributes to major OSS, MIT-licensed repos |
|
|
74
|
+
| 2 | cyyever | 80.8 | 120 | 64 | 1,217 followers, active contributor, profile README |
|
|
75
|
+
| 3 | Cyrilvallez | 77.2 | 120 | 56 | Active: 13 commits + 57 PRs in 90 days, strong OSS presence |
|
|
76
|
+
| 4 | ArthurZucker | 74.4 | 120 | 48 | 37 PRs in 90 days, contributes to huggingface/transformers |
|
|
77
|
+
| 5 | ydshieh | 72.0 | 120 | 40 | Active: 9 commits + 40 PRs in 90 days |
|
|
78
|
+
|
|
79
|
+
Combined score = activity × 0.4 + relevance × 0.6. Relevance is keyword overlap with the job description (ML, AI, startup, engineer, etc.).
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
### 1. Clone and install
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
git clone https://github.com/carolinacherry/github-talent-mcp.git
|
|
87
|
+
cd github-talent-mcp
|
|
88
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
89
|
+
pip install -e .
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 2. Create a GitHub personal access token
|
|
93
|
+
|
|
94
|
+
Go to [github.com/settings/tokens](https://github.com/settings/tokens) and create a **fine-grained** or **classic** token with these scopes:
|
|
95
|
+
|
|
96
|
+
| Scope | Why |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `read:user` | Read user profiles and search users |
|
|
99
|
+
| `public_repo` | Read public repo data, languages, contributors |
|
|
100
|
+
|
|
101
|
+
Create a `.env` file in the project root:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
GITHUB_TOKEN=ghp_xxxxxxxxxxxx
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 3. Connect to Claude
|
|
108
|
+
|
|
109
|
+
#### Claude Code (CLI)
|
|
110
|
+
|
|
111
|
+
One command:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
claude mcp add github-talent -- /path/to/github-talent-mcp/.venv/bin/python3 -m github_talent_mcp
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Then set the token as an environment variable. Either:
|
|
118
|
+
- Export it in your shell: `export GITHUB_TOKEN=ghp_xxxxxxxxxxxx`
|
|
119
|
+
- Or keep it in the `.env` file — the server reads it via `python-dotenv` on startup
|
|
120
|
+
|
|
121
|
+
Restart Claude Code to pick up the new server. Verify with `/mcp` — you should see 4 tools under `github-talent`.
|
|
122
|
+
|
|
123
|
+
#### Claude Desktop
|
|
124
|
+
|
|
125
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"mcpServers": {
|
|
130
|
+
"github-talent": {
|
|
131
|
+
"command": "/path/to/github-talent-mcp/.venv/bin/python3",
|
|
132
|
+
"args": ["-m", "github_talent_mcp"],
|
|
133
|
+
"cwd": "/path/to/github-talent-mcp",
|
|
134
|
+
"env": {
|
|
135
|
+
"GITHUB_TOKEN": "ghp_xxxxxxxxxxxx"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Restart Claude Desktop. The tools will appear in the toolbox icon.
|
|
143
|
+
|
|
144
|
+
## Try It
|
|
145
|
+
|
|
146
|
+
Once installed, paste these prompts to verify everything works:
|
|
147
|
+
|
|
148
|
+
**Basic search:**
|
|
149
|
+
> Find Python developers in Raleigh active in the last 60 days
|
|
150
|
+
|
|
151
|
+
**Profile deep dive:**
|
|
152
|
+
> Get the full developer profile and activity score for torvalds on GitHub
|
|
153
|
+
|
|
154
|
+
**Full workflow:**
|
|
155
|
+
> Find 10 ML engineers in San Francisco active in the last 30 days, then rank them for a senior LLM inference engineer role
|
|
156
|
+
|
|
157
|
+
**Repo contributors:**
|
|
158
|
+
> Get the top contributors to huggingface/transformers and rank them for a founding ML engineer role at an AI startup
|
|
159
|
+
|
|
160
|
+
## Tools
|
|
161
|
+
|
|
162
|
+
| Tool | Description |
|
|
163
|
+
|---|---|
|
|
164
|
+
| `search_developers` | Search GitHub users by language, location, activity, followers. For topic-based sourcing, use `get_repo_contributors` on relevant repos instead. |
|
|
165
|
+
| `get_developer_profile` | Deep profile enrichment: languages, stars, commits + PRs, OSS contributions, license breakdown, profile README, and activity score with breakdown. |
|
|
166
|
+
| `rank_candidates` | Rank usernames against a job description. Returns sorted candidates with combined score, strengths, gaps, and reasoning. |
|
|
167
|
+
| `get_repo_contributors` | Top contributors for any repo. Accepts `owner/repo` or full URL. The fastest way to source for a specific domain. |
|
|
168
|
+
|
|
169
|
+
## Scoring
|
|
170
|
+
|
|
171
|
+
The activity score combines two layers: **behavioral signals** (what you did recently) and a **reputation floor** (what you've built over time).
|
|
172
|
+
|
|
173
|
+
### Behavioral Score (0-205)
|
|
174
|
+
|
|
175
|
+
| Signal | Max Points | How |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| Commits + PRs (last 90 days) | 60 | Push commits + PR opens (PRs weighted x3). Captures both push-based and PR-based workflows. |
|
|
178
|
+
| Stars on repos | 40 | Personal repo stars + stars on repos you contribute to. Org repo maintainers get credit. |
|
|
179
|
+
| Profile README | 20 | Presence of a profile README (github.com/username/username). |
|
|
180
|
+
| Followers | 20 | Capped at 20. |
|
|
181
|
+
| Repos with descriptions | 20 | Ratio of repos that have descriptions. Signal of care and polish. |
|
|
182
|
+
| Permissive license repos | 15 | Has at least one repo with MIT, Apache-2.0, BSD, ISC, or Unlicense. |
|
|
183
|
+
| Major OSS contributions | 30 | PRs, pushes, or issues on repos you don't own. Capped at 3 repos (10 pts each). |
|
|
184
|
+
|
|
185
|
+
### Reputation Floor
|
|
186
|
+
|
|
187
|
+
The behavioral score alone penalizes developers whose work doesn't produce GitHub events — Torvalds works through mailing lists, senior maintainers merge via org bots, and many engineers work in private repos.
|
|
188
|
+
|
|
189
|
+
The reputation floor ensures cumulative impact isn't erased by a quiet quarter:
|
|
190
|
+
|
|
191
|
+
| Threshold | Floor |
|
|
192
|
+
|---|---|
|
|
193
|
+
| 10K+ followers **or** 50K+ stars | 150 |
|
|
194
|
+
| 1K+ followers **or** 5K+ stars | 120 |
|
|
195
|
+
| 500+ followers **or** 1K+ stars | 100 |
|
|
196
|
+
| 100+ followers **or** 200+ stars | 80 |
|
|
197
|
+
|
|
198
|
+
The final score is `max(behavioral_score, reputation_floor)`. If the floor is applied, the breakdown includes a `reputation_floor` field so you know.
|
|
199
|
+
|
|
200
|
+
### Score Tiers
|
|
201
|
+
|
|
202
|
+
- **150+** — exceptional (top OSS maintainers, well-known engineers)
|
|
203
|
+
- **120-149** — strong signal, worth reaching out
|
|
204
|
+
- **80-119** — solid developer with meaningful public work
|
|
205
|
+
- **40-79** — active but limited public signal
|
|
206
|
+
- **<40** — low signal (likely private work or junior)
|
|
207
|
+
|
|
208
|
+
### Ranking
|
|
209
|
+
|
|
210
|
+
`rank_candidates` combines the activity score with a **relevance score** (0-100) based on keyword overlap between the job description and the candidate's profile (bio, languages, repo topics, README). The combined score weights relevance at 60% and activity at 40% — a high-activity developer with no overlap to the job shouldn't outrank a relevant one.
|
|
211
|
+
|
|
212
|
+
## Rate Limits
|
|
213
|
+
|
|
214
|
+
GitHub REST API: 5,000 requests/hour with token. A typical workflow (search + enrich 5 candidates + rank) uses ~60-100 API calls. Profile results are cached within a session to avoid redundant calls during ranking.
|
|
215
|
+
|
|
216
|
+
## License
|
|
217
|
+
|
|
218
|
+
MIT
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# github-talent-mcp
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://www.python.org)
|
|
5
|
+
[](https://modelcontextprotocol.io)
|
|
6
|
+
[](https://claude.ai)
|
|
7
|
+
[](https://docs.github.com/en/rest)
|
|
8
|
+
|
|
9
|
+
MCP server that searches, scores, and ranks GitHub developers for technical recruiting.
|
|
10
|
+
|
|
11
|
+
## Demo
|
|
12
|
+
|
|
13
|
+
https://github.com/user-attachments/assets/2dfd82b4-3eb5-4f2b-bc0a-2580b95043e4
|
|
14
|
+
|
|
15
|
+
### Profile deep dive
|
|
16
|
+
|
|
17
|
+
> Get the full developer profile and activity score for torvalds on GitHub
|
|
18
|
+
|
|
19
|
+
Claude calls `get_developer_profile("torvalds")` and returns:
|
|
20
|
+
|
|
21
|
+
| Field | Value |
|
|
22
|
+
|---|---|
|
|
23
|
+
| **Activity Score** | **150** (reputation floor applied) |
|
|
24
|
+
| Location | Portland, OR |
|
|
25
|
+
| Followers | 293,321 |
|
|
26
|
+
| Stars Received | 235,068 |
|
|
27
|
+
| Primary Language | C (98.1%) |
|
|
28
|
+
| Commits (90d) | 0 |
|
|
29
|
+
| PRs (90d) | 0 |
|
|
30
|
+
| Notable Repos | linux (183K stars), libdc-for-dirk, subsurface-for-dirk, uemacs, pesern-resolve |
|
|
31
|
+
| Profile README | No |
|
|
32
|
+
| Hireable | No |
|
|
33
|
+
|
|
34
|
+
Torvalds has zero recent GitHub activity because kernel development flows through mailing lists, not GitHub PRs. The **reputation floor** (293K followers) overrides the behavioral score and sets it to 150.
|
|
35
|
+
|
|
36
|
+
### Repo contributor ranking
|
|
37
|
+
|
|
38
|
+
> Get the top contributors to huggingface/transformers and rank them for a founding ML engineer role at an AI startup
|
|
39
|
+
|
|
40
|
+
Claude calls `get_repo_contributors("huggingface/transformers")` → `rank_candidates` on the top 24 contributors:
|
|
41
|
+
|
|
42
|
+
| Rank | Developer | Combined Score | Activity | Relevance | Strengths |
|
|
43
|
+
|---|---|---|---|---|---|
|
|
44
|
+
| 1 | stas00 | 83.4 | 150 | 72 | 4,553 stars, contributes to major OSS, MIT-licensed repos |
|
|
45
|
+
| 2 | cyyever | 80.8 | 120 | 64 | 1,217 followers, active contributor, profile README |
|
|
46
|
+
| 3 | Cyrilvallez | 77.2 | 120 | 56 | Active: 13 commits + 57 PRs in 90 days, strong OSS presence |
|
|
47
|
+
| 4 | ArthurZucker | 74.4 | 120 | 48 | 37 PRs in 90 days, contributes to huggingface/transformers |
|
|
48
|
+
| 5 | ydshieh | 72.0 | 120 | 40 | Active: 9 commits + 40 PRs in 90 days |
|
|
49
|
+
|
|
50
|
+
Combined score = activity × 0.4 + relevance × 0.6. Relevance is keyword overlap with the job description (ML, AI, startup, engineer, etc.).
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
### 1. Clone and install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://github.com/carolinacherry/github-talent-mcp.git
|
|
58
|
+
cd github-talent-mcp
|
|
59
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
60
|
+
pip install -e .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 2. Create a GitHub personal access token
|
|
64
|
+
|
|
65
|
+
Go to [github.com/settings/tokens](https://github.com/settings/tokens) and create a **fine-grained** or **classic** token with these scopes:
|
|
66
|
+
|
|
67
|
+
| Scope | Why |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `read:user` | Read user profiles and search users |
|
|
70
|
+
| `public_repo` | Read public repo data, languages, contributors |
|
|
71
|
+
|
|
72
|
+
Create a `.env` file in the project root:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
GITHUB_TOKEN=ghp_xxxxxxxxxxxx
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 3. Connect to Claude
|
|
79
|
+
|
|
80
|
+
#### Claude Code (CLI)
|
|
81
|
+
|
|
82
|
+
One command:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
claude mcp add github-talent -- /path/to/github-talent-mcp/.venv/bin/python3 -m github_talent_mcp
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Then set the token as an environment variable. Either:
|
|
89
|
+
- Export it in your shell: `export GITHUB_TOKEN=ghp_xxxxxxxxxxxx`
|
|
90
|
+
- Or keep it in the `.env` file — the server reads it via `python-dotenv` on startup
|
|
91
|
+
|
|
92
|
+
Restart Claude Code to pick up the new server. Verify with `/mcp` — you should see 4 tools under `github-talent`.
|
|
93
|
+
|
|
94
|
+
#### Claude Desktop
|
|
95
|
+
|
|
96
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"mcpServers": {
|
|
101
|
+
"github-talent": {
|
|
102
|
+
"command": "/path/to/github-talent-mcp/.venv/bin/python3",
|
|
103
|
+
"args": ["-m", "github_talent_mcp"],
|
|
104
|
+
"cwd": "/path/to/github-talent-mcp",
|
|
105
|
+
"env": {
|
|
106
|
+
"GITHUB_TOKEN": "ghp_xxxxxxxxxxxx"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Restart Claude Desktop. The tools will appear in the toolbox icon.
|
|
114
|
+
|
|
115
|
+
## Try It
|
|
116
|
+
|
|
117
|
+
Once installed, paste these prompts to verify everything works:
|
|
118
|
+
|
|
119
|
+
**Basic search:**
|
|
120
|
+
> Find Python developers in Raleigh active in the last 60 days
|
|
121
|
+
|
|
122
|
+
**Profile deep dive:**
|
|
123
|
+
> Get the full developer profile and activity score for torvalds on GitHub
|
|
124
|
+
|
|
125
|
+
**Full workflow:**
|
|
126
|
+
> Find 10 ML engineers in San Francisco active in the last 30 days, then rank them for a senior LLM inference engineer role
|
|
127
|
+
|
|
128
|
+
**Repo contributors:**
|
|
129
|
+
> Get the top contributors to huggingface/transformers and rank them for a founding ML engineer role at an AI startup
|
|
130
|
+
|
|
131
|
+
## Tools
|
|
132
|
+
|
|
133
|
+
| Tool | Description |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `search_developers` | Search GitHub users by language, location, activity, followers. For topic-based sourcing, use `get_repo_contributors` on relevant repos instead. |
|
|
136
|
+
| `get_developer_profile` | Deep profile enrichment: languages, stars, commits + PRs, OSS contributions, license breakdown, profile README, and activity score with breakdown. |
|
|
137
|
+
| `rank_candidates` | Rank usernames against a job description. Returns sorted candidates with combined score, strengths, gaps, and reasoning. |
|
|
138
|
+
| `get_repo_contributors` | Top contributors for any repo. Accepts `owner/repo` or full URL. The fastest way to source for a specific domain. |
|
|
139
|
+
|
|
140
|
+
## Scoring
|
|
141
|
+
|
|
142
|
+
The activity score combines two layers: **behavioral signals** (what you did recently) and a **reputation floor** (what you've built over time).
|
|
143
|
+
|
|
144
|
+
### Behavioral Score (0-205)
|
|
145
|
+
|
|
146
|
+
| Signal | Max Points | How |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| Commits + PRs (last 90 days) | 60 | Push commits + PR opens (PRs weighted x3). Captures both push-based and PR-based workflows. |
|
|
149
|
+
| Stars on repos | 40 | Personal repo stars + stars on repos you contribute to. Org repo maintainers get credit. |
|
|
150
|
+
| Profile README | 20 | Presence of a profile README (github.com/username/username). |
|
|
151
|
+
| Followers | 20 | Capped at 20. |
|
|
152
|
+
| Repos with descriptions | 20 | Ratio of repos that have descriptions. Signal of care and polish. |
|
|
153
|
+
| Permissive license repos | 15 | Has at least one repo with MIT, Apache-2.0, BSD, ISC, or Unlicense. |
|
|
154
|
+
| Major OSS contributions | 30 | PRs, pushes, or issues on repos you don't own. Capped at 3 repos (10 pts each). |
|
|
155
|
+
|
|
156
|
+
### Reputation Floor
|
|
157
|
+
|
|
158
|
+
The behavioral score alone penalizes developers whose work doesn't produce GitHub events — Torvalds works through mailing lists, senior maintainers merge via org bots, and many engineers work in private repos.
|
|
159
|
+
|
|
160
|
+
The reputation floor ensures cumulative impact isn't erased by a quiet quarter:
|
|
161
|
+
|
|
162
|
+
| Threshold | Floor |
|
|
163
|
+
|---|---|
|
|
164
|
+
| 10K+ followers **or** 50K+ stars | 150 |
|
|
165
|
+
| 1K+ followers **or** 5K+ stars | 120 |
|
|
166
|
+
| 500+ followers **or** 1K+ stars | 100 |
|
|
167
|
+
| 100+ followers **or** 200+ stars | 80 |
|
|
168
|
+
|
|
169
|
+
The final score is `max(behavioral_score, reputation_floor)`. If the floor is applied, the breakdown includes a `reputation_floor` field so you know.
|
|
170
|
+
|
|
171
|
+
### Score Tiers
|
|
172
|
+
|
|
173
|
+
- **150+** — exceptional (top OSS maintainers, well-known engineers)
|
|
174
|
+
- **120-149** — strong signal, worth reaching out
|
|
175
|
+
- **80-119** — solid developer with meaningful public work
|
|
176
|
+
- **40-79** — active but limited public signal
|
|
177
|
+
- **<40** — low signal (likely private work or junior)
|
|
178
|
+
|
|
179
|
+
### Ranking
|
|
180
|
+
|
|
181
|
+
`rank_candidates` combines the activity score with a **relevance score** (0-100) based on keyword overlap between the job description and the candidate's profile (bio, languages, repo topics, README). The combined score weights relevance at 60% and activity at 40% — a high-activity developer with no overlap to the job shouldn't outrank a relevant one.
|
|
182
|
+
|
|
183
|
+
## Rate Limits
|
|
184
|
+
|
|
185
|
+
GitHub REST API: 5,000 requests/hour with token. A typical workflow (search + enrich 5 candidates + rank) uses ~60-100 API calls. Profile results are cached within a session to avoid redundant calls during ranking.
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "github-talent-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server for searching, scoring, and ranking GitHub developers for technical recruiting"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Carolina Cherry"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["mcp", "github", "recruiting", "developer-tools", "claude"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Topic :: Software Development :: Libraries",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"mcp>=1.0.0",
|
|
30
|
+
"httpx>=0.27.0",
|
|
31
|
+
"pydantic>=2.0.0",
|
|
32
|
+
"python-dotenv>=1.0.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/carolinacherry/github-talent-mcp"
|
|
37
|
+
Repository = "https://github.com/carolinacherry/github-talent-mcp"
|
|
38
|
+
Issues = "https://github.com/carolinacherry/github-talent-mcp/issues"
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
github-talent-mcp = "github_talent_mcp.server:main"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["src"]
|
|
45
|
+
|
|
46
|
+
[tool.ruff]
|
|
47
|
+
target-version = "py310"
|
|
48
|
+
line-length = 100
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
asyncio_mode = "auto"
|
|
File without changes
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
GITHUB_API = "https://api.github.com"
|
|
11
|
+
PERMISSIVE_LICENSES = frozenset({
|
|
12
|
+
"mit", "apache-2.0", "bsd-2-clause", "bsd-3-clause", "isc", "unlicense",
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
log = logging.getLogger("github-talent-mcp")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GitHubClient:
|
|
19
|
+
def __init__(self, token: str | None = None):
|
|
20
|
+
self._token = token or os.environ.get("GITHUB_TOKEN", "")
|
|
21
|
+
self._cache: dict[str, tuple[float, Any]] = {}
|
|
22
|
+
self._cache_ttl = 300 # 5 minutes
|
|
23
|
+
headers: dict[str, str] = {
|
|
24
|
+
"Accept": "application/vnd.github+json",
|
|
25
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
26
|
+
}
|
|
27
|
+
if self._token:
|
|
28
|
+
headers["Authorization"] = f"Bearer {self._token}"
|
|
29
|
+
self._client = httpx.AsyncClient(
|
|
30
|
+
base_url=GITHUB_API,
|
|
31
|
+
headers=headers,
|
|
32
|
+
timeout=30.0,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
async def close(self) -> None:
|
|
36
|
+
await self._client.aclose()
|
|
37
|
+
|
|
38
|
+
# -- Cache helpers --
|
|
39
|
+
|
|
40
|
+
def _cache_get(self, key: str) -> Any | None:
|
|
41
|
+
if key in self._cache:
|
|
42
|
+
ts, data = self._cache[key]
|
|
43
|
+
if time.monotonic() - ts < self._cache_ttl:
|
|
44
|
+
return data
|
|
45
|
+
del self._cache[key]
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
def _cache_set(self, key: str, data: Any) -> None:
|
|
49
|
+
self._cache[key] = (time.monotonic(), data)
|
|
50
|
+
|
|
51
|
+
def _check_rate_limit(self, resp: httpx.Response) -> None:
|
|
52
|
+
remaining = resp.headers.get("X-RateLimit-Remaining")
|
|
53
|
+
if remaining and int(remaining) < 100:
|
|
54
|
+
reset = resp.headers.get("X-RateLimit-Reset", "unknown")
|
|
55
|
+
log.warning(f"Rate limit low: {remaining} remaining, resets at {reset}")
|
|
56
|
+
|
|
57
|
+
# -- API methods --
|
|
58
|
+
|
|
59
|
+
async def search_users(
|
|
60
|
+
self,
|
|
61
|
+
*,
|
|
62
|
+
languages: list[str] | None = None,
|
|
63
|
+
location: str | None = None,
|
|
64
|
+
min_followers: int | None = None,
|
|
65
|
+
min_repos: int | None = None,
|
|
66
|
+
per_page: int = 30,
|
|
67
|
+
page: int = 1,
|
|
68
|
+
) -> dict:
|
|
69
|
+
parts: list[str] = ["type:user"]
|
|
70
|
+
if languages:
|
|
71
|
+
for lang in languages:
|
|
72
|
+
parts.append(f"language:{lang}")
|
|
73
|
+
if location:
|
|
74
|
+
parts.append(f"location:{location}")
|
|
75
|
+
if min_followers is not None:
|
|
76
|
+
parts.append(f"followers:>={min_followers}")
|
|
77
|
+
if min_repos is not None:
|
|
78
|
+
parts.append(f"repos:>={min_repos}")
|
|
79
|
+
# Note: pushed:> is NOT a valid qualifier for /search/users — it silently
|
|
80
|
+
# returns 0 results. Use created:> for account age filtering instead.
|
|
81
|
+
# Recent activity should be verified via get_developer_profile.
|
|
82
|
+
|
|
83
|
+
q = " ".join(parts)
|
|
84
|
+
resp = await self._client.get(
|
|
85
|
+
"/search/users",
|
|
86
|
+
params={"q": q, "per_page": per_page, "page": page, "sort": "followers", "order": "desc"},
|
|
87
|
+
)
|
|
88
|
+
self._check_rate_limit(resp)
|
|
89
|
+
resp.raise_for_status()
|
|
90
|
+
return resp.json()
|
|
91
|
+
|
|
92
|
+
async def get_repo_info(self, owner: str, repo: str) -> dict:
|
|
93
|
+
cache_key = f"repo:{owner}/{repo}"
|
|
94
|
+
cached = self._cache_get(cache_key)
|
|
95
|
+
if cached is not None:
|
|
96
|
+
return cached
|
|
97
|
+
resp = await self._client.get(f"/repos/{owner}/{repo}")
|
|
98
|
+
self._check_rate_limit(resp)
|
|
99
|
+
resp.raise_for_status()
|
|
100
|
+
data = resp.json()
|
|
101
|
+
self._cache_set(cache_key, data)
|
|
102
|
+
return data
|
|
103
|
+
|
|
104
|
+
async def get_user(self, username: str) -> dict:
|
|
105
|
+
cache_key = f"user:{username}"
|
|
106
|
+
cached = self._cache_get(cache_key)
|
|
107
|
+
if cached is not None:
|
|
108
|
+
return cached
|
|
109
|
+
resp = await self._client.get(f"/users/{username}")
|
|
110
|
+
self._check_rate_limit(resp)
|
|
111
|
+
resp.raise_for_status()
|
|
112
|
+
data = resp.json()
|
|
113
|
+
self._cache_set(cache_key, data)
|
|
114
|
+
return data
|
|
115
|
+
|
|
116
|
+
async def get_user_repos(self, username: str, per_page: int = 100) -> list[dict]:
|
|
117
|
+
cache_key = f"repos:{username}"
|
|
118
|
+
cached = self._cache_get(cache_key)
|
|
119
|
+
if cached is not None:
|
|
120
|
+
return cached
|
|
121
|
+
resp = await self._client.get(
|
|
122
|
+
f"/users/{username}/repos",
|
|
123
|
+
params={"per_page": per_page, "sort": "pushed", "direction": "desc", "type": "owner"},
|
|
124
|
+
)
|
|
125
|
+
self._check_rate_limit(resp)
|
|
126
|
+
resp.raise_for_status()
|
|
127
|
+
data = resp.json()
|
|
128
|
+
self._cache_set(cache_key, data)
|
|
129
|
+
return data
|
|
130
|
+
|
|
131
|
+
async def get_repo_languages(self, owner: str, repo: str) -> dict[str, int]:
|
|
132
|
+
cache_key = f"langs:{owner}/{repo}"
|
|
133
|
+
cached = self._cache_get(cache_key)
|
|
134
|
+
if cached is not None:
|
|
135
|
+
return cached
|
|
136
|
+
resp = await self._client.get(f"/repos/{owner}/{repo}/languages")
|
|
137
|
+
self._check_rate_limit(resp)
|
|
138
|
+
resp.raise_for_status()
|
|
139
|
+
data = resp.json()
|
|
140
|
+
self._cache_set(cache_key, data)
|
|
141
|
+
return data
|
|
142
|
+
|
|
143
|
+
async def get_user_events(self, username: str, max_pages: int = 3) -> list[dict]:
|
|
144
|
+
cache_key = f"events:{username}"
|
|
145
|
+
cached = self._cache_get(cache_key)
|
|
146
|
+
if cached is not None:
|
|
147
|
+
return cached
|
|
148
|
+
all_events: list[dict] = []
|
|
149
|
+
for page in range(1, max_pages + 1):
|
|
150
|
+
resp = await self._client.get(
|
|
151
|
+
f"/users/{username}/events/public",
|
|
152
|
+
params={"per_page": 100, "page": page},
|
|
153
|
+
)
|
|
154
|
+
self._check_rate_limit(resp)
|
|
155
|
+
resp.raise_for_status()
|
|
156
|
+
events = resp.json()
|
|
157
|
+
if not events:
|
|
158
|
+
break
|
|
159
|
+
all_events.extend(events)
|
|
160
|
+
self._cache_set(cache_key, all_events)
|
|
161
|
+
return all_events
|
|
162
|
+
|
|
163
|
+
async def get_profile_readme(self, username: str) -> str | None:
|
|
164
|
+
cache_key = f"readme:{username}"
|
|
165
|
+
cached = self._cache_get(cache_key)
|
|
166
|
+
if cached is not None:
|
|
167
|
+
return cached
|
|
168
|
+
try:
|
|
169
|
+
resp = await self._client.get(
|
|
170
|
+
f"/repos/{username}/{username}/readme",
|
|
171
|
+
headers={"Accept": "application/vnd.github.raw+json"},
|
|
172
|
+
)
|
|
173
|
+
if resp.status_code == 200:
|
|
174
|
+
content = resp.text[:3000]
|
|
175
|
+
self._cache_set(cache_key, content)
|
|
176
|
+
return content
|
|
177
|
+
except httpx.HTTPError:
|
|
178
|
+
pass
|
|
179
|
+
self._cache_set(cache_key, None)
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
async def get_repo_contributors(
|
|
183
|
+
self, owner: str, repo: str, per_page: int = 30,
|
|
184
|
+
) -> list[dict]:
|
|
185
|
+
resp = await self._client.get(
|
|
186
|
+
f"/repos/{owner}/{repo}/contributors",
|
|
187
|
+
params={"per_page": per_page},
|
|
188
|
+
)
|
|
189
|
+
self._check_rate_limit(resp)
|
|
190
|
+
resp.raise_for_status()
|
|
191
|
+
return resp.json()
|