githunt-mcp 0.1.0
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.
- package/README.md +68 -0
- package/index.js +246 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# githunt-mcp
|
|
2
|
+
|
|
3
|
+
MCP server that wraps [GitHunt's](https://githunt.ai) REST API, giving AI assistants access to GitHunt's GitHub developer search, profile lookup, and AI profile analysis.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Get an API key from your [GitHunt account](https://githunt.ai/account), then run:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx githunt-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Environment variables
|
|
14
|
+
|
|
15
|
+
| Variable | Required | Description |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `GITHUNT_API_KEY` | yes | Your GitHunt API key |
|
|
18
|
+
| `GITHUNT_API_URL` | no | API base URL (default `https://api.githunt.ai`) |
|
|
19
|
+
|
|
20
|
+
### Claude Code
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
claude mcp add githunt -e GITHUNT_API_KEY=your-key-here -- npx githunt-mcp
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Claude Desktop
|
|
27
|
+
|
|
28
|
+
Add to `claude_desktop_config.json`:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"githunt": {
|
|
34
|
+
"command": "npx",
|
|
35
|
+
"args": ["githunt-mcp"],
|
|
36
|
+
"env": {
|
|
37
|
+
"GITHUNT_API_KEY": "your-key-here"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Cursor
|
|
45
|
+
|
|
46
|
+
Add to `.cursor/mcp.json`:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"githunt": {
|
|
52
|
+
"command": "npx",
|
|
53
|
+
"args": ["githunt-mcp"],
|
|
54
|
+
"env": {
|
|
55
|
+
"GITHUNT_API_KEY": "your-key-here"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Tools
|
|
63
|
+
|
|
64
|
+
- **search_developers** — search GitHunt's index of 318k+ GitHub developers by location, role, and skills.
|
|
65
|
+
- **get_developer** — get a single GitHub developer's ranked profile by username.
|
|
66
|
+
- **analyze_profile** — deep AI analysis of a GitHub profile (proficiency, role matches, extracted emails).
|
|
67
|
+
|
|
68
|
+
Full API reference: https://docs.githunt.ai
|
package/index.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
const API_KEY = process.env.GITHUNT_API_KEY;
|
|
7
|
+
if (!API_KEY) {
|
|
8
|
+
console.error(
|
|
9
|
+
"GITHUNT_API_KEY environment variable is required. Get a key at https://githunt.ai/account."
|
|
10
|
+
);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const API_URL = (process.env.GITHUNT_API_URL || "https://api.githunt.ai").replace(/\/+$/, "");
|
|
15
|
+
|
|
16
|
+
async function callApi(path, options = {}) {
|
|
17
|
+
const res = await fetch(`${API_URL}${path}`, {
|
|
18
|
+
...options,
|
|
19
|
+
headers: {
|
|
20
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
...options.headers,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
const body = await res.json().catch(() => null);
|
|
26
|
+
return { ok: res.ok, body };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function truncate(text, max) {
|
|
30
|
+
if (!text) return "";
|
|
31
|
+
const clean = String(text).replace(/\s+/g, " ").trim();
|
|
32
|
+
return clean.length > max ? `${clean.slice(0, max - 3)}...` : clean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function joinPresent(parts, sep = " | ") {
|
|
36
|
+
return parts
|
|
37
|
+
.map((p) => (typeof p === "string" ? p.trim() : p))
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.join(sep);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function relativeActive(dateString) {
|
|
43
|
+
if (!dateString) return "";
|
|
44
|
+
const diffDays = Math.floor((Date.now() - new Date(dateString).getTime()) / 86400000);
|
|
45
|
+
if (Number.isNaN(diffDays)) return "";
|
|
46
|
+
if (diffDays < 1) return "active today";
|
|
47
|
+
if (diffDays < 7) return `active ${diffDays}d ago`;
|
|
48
|
+
if (diffDays < 30) return `active ${Math.floor(diffDays / 7)}w ago`;
|
|
49
|
+
if (diffDays < 365) return `active ${Math.floor(diffDays / 30)}mo ago`;
|
|
50
|
+
return `active ${Math.floor(diffDays / 365)}y ago`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatSearchResults(data) {
|
|
54
|
+
const header = `${data.matchedCount} candidate${data.matchedCount === 1 ? "" : "s"} found`;
|
|
55
|
+
if (!data.results?.length) {
|
|
56
|
+
return `${header}. Try a broader location or fewer skills.`;
|
|
57
|
+
}
|
|
58
|
+
const blocks = data.results.map((r, i) => {
|
|
59
|
+
const identity = joinPresent([
|
|
60
|
+
`${i + 1}. ${r.login}${r.name && r.name !== r.login ? ` (${r.name})` : ""} - score ${r.score}`,
|
|
61
|
+
`profile ${r.profile_score} / tech ${r.tech_stack_score} / activity ${r.activity_score}`,
|
|
62
|
+
]);
|
|
63
|
+
const meta = joinPresent([
|
|
64
|
+
r.github_experience ? `${r.github_experience}y on GitHub` : "",
|
|
65
|
+
r.location,
|
|
66
|
+
r.company ? `@ ${truncate(r.company.replace(/^@/, ""), 40)}` : "",
|
|
67
|
+
typeof r.followers === "number" ? `${r.followers} followers` : "",
|
|
68
|
+
r.hireable === "Yes" ? "hireable" : "",
|
|
69
|
+
r.email,
|
|
70
|
+
r.profile_url,
|
|
71
|
+
]);
|
|
72
|
+
const activity = joinPresent([
|
|
73
|
+
relativeActive(r.last_active_date),
|
|
74
|
+
r.commit_frequency_label && r.commit_frequency_label !== "Unknown"
|
|
75
|
+
? `${r.commit_frequency_label} (${r.commits_per_month || 0} commits/mo)`
|
|
76
|
+
: "",
|
|
77
|
+
r.commit_message_quality_label && r.commit_message_quality_label !== "Insufficient Data"
|
|
78
|
+
? `commit quality ${r.commit_message_quality_label} (${r.semantic_commit_percentage || 0}% semantic)`
|
|
79
|
+
: "",
|
|
80
|
+
]);
|
|
81
|
+
const details = joinPresent([
|
|
82
|
+
r.matching_keywords ? `matched: ${truncate(r.matching_keywords, 90)}` : "",
|
|
83
|
+
r.bio ? `bio: ${truncate(r.bio, 90)}` : "",
|
|
84
|
+
]);
|
|
85
|
+
const repos = (r.top_repositories || [])
|
|
86
|
+
.map((repo) => joinPresent([repo.name, repo.stars ? `${repo.stars}★` : "", repo.language], " "))
|
|
87
|
+
.join(", ");
|
|
88
|
+
const oss = (r.top_oss_contributions || [])
|
|
89
|
+
.map((c) => joinPresent([c.repository, c.tier_label ? `(${c.tier_label})` : ""], " "))
|
|
90
|
+
.join(", ");
|
|
91
|
+
return joinPresent(
|
|
92
|
+
[
|
|
93
|
+
identity,
|
|
94
|
+
` ${meta}`,
|
|
95
|
+
activity ? ` ${activity}` : "",
|
|
96
|
+
details ? ` ${details}` : "",
|
|
97
|
+
repos ? ` repos: ${repos}` : "",
|
|
98
|
+
oss ? ` OSS: ${oss}` : "",
|
|
99
|
+
],
|
|
100
|
+
"\n"
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
return `${header}\n\n${blocks.join("\n\n")}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatDeveloper(r) {
|
|
107
|
+
const repos = (r.top_repositories || [])
|
|
108
|
+
.map((repo) => joinPresent([repo.name, repo.stars ? `${repo.stars}★` : "", repo.language], " "))
|
|
109
|
+
.join(", ");
|
|
110
|
+
const oss = (r.top_oss_contributions || [])
|
|
111
|
+
.map((c) => joinPresent([c.repository, c.tier_label ? `(${c.tier_label})` : ""], " "))
|
|
112
|
+
.join(", ");
|
|
113
|
+
return [
|
|
114
|
+
`${r.username}${r.name && r.name !== r.username ? ` (${r.name})` : ""} - score ${r.score}`,
|
|
115
|
+
joinPresent([
|
|
116
|
+
`profile ${r.profile_score} / tech ${r.tech_stack_score} / activity ${r.activity_score}`,
|
|
117
|
+
r.github_experience ? `${r.github_experience}y on GitHub` : "",
|
|
118
|
+
`hireable: ${r.hireable}`,
|
|
119
|
+
]),
|
|
120
|
+
joinPresent([r.location, r.company, `${r.followers} followers`]),
|
|
121
|
+
joinPresent([
|
|
122
|
+
relativeActive(r.last_active_date),
|
|
123
|
+
r.commit_frequency_label && r.commit_frequency_label !== "Unknown"
|
|
124
|
+
? `${r.commit_frequency_label} (${r.commits_per_month || 0} commits/mo)`
|
|
125
|
+
: "",
|
|
126
|
+
r.commit_message_quality_label && r.commit_message_quality_label !== "Insufficient Data"
|
|
127
|
+
? `commit quality ${r.commit_message_quality_label} (${r.semantic_commit_percentage || 0}% semantic)`
|
|
128
|
+
: "",
|
|
129
|
+
]),
|
|
130
|
+
joinPresent([r.email, r.blog, r.twitter_username ? `@${r.twitter_username}` : ""]),
|
|
131
|
+
r.matching_keywords ? `keywords: ${truncate(r.matching_keywords, 120)}` : "",
|
|
132
|
+
r.bio ? `bio: ${truncate(r.bio, 160)}` : "",
|
|
133
|
+
repos ? `repos: ${repos}` : "",
|
|
134
|
+
oss ? `OSS: ${oss}` : "",
|
|
135
|
+
r.profile_url,
|
|
136
|
+
].filter(Boolean).join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function formatAnalysis(data) {
|
|
140
|
+
const prof = data.proficiency || {};
|
|
141
|
+
const roles = (data.roleMatch || []).slice(0, 3);
|
|
142
|
+
const emails = data.emails?.emails || [];
|
|
143
|
+
return [
|
|
144
|
+
`Proficiency: ${prof.level || "unknown"} (score ${prof.score ?? "n/a"}, ~${prof.experienceYears ?? "?"}y experience)`,
|
|
145
|
+
roles.length
|
|
146
|
+
? `Role fit: ${roles.map((r) => `${r.displayName || r.role} (${r.score})`).join(", ")}`
|
|
147
|
+
: "",
|
|
148
|
+
emails.length
|
|
149
|
+
? `Emails: ${emails.map((e) => `${e.address}${e.isPrimary ? " (primary)" : ""}`).join(", ")}`
|
|
150
|
+
: "Emails: none found",
|
|
151
|
+
data.scores?.matchingKeywords?.length
|
|
152
|
+
? `Keywords: ${truncate(data.scores.matchingKeywords.join(", "), 120)}`
|
|
153
|
+
: "",
|
|
154
|
+
].filter(Boolean).join("\n");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatSuccess(data, meta, formatter) {
|
|
158
|
+
const lines = [formatter ? formatter(data) : JSON.stringify(data, null, 2)];
|
|
159
|
+
if (meta?.quota) {
|
|
160
|
+
lines.push(`Quota: ${meta.quota.used}/${meta.quota.limit} for ${meta.quota.month}`);
|
|
161
|
+
}
|
|
162
|
+
return { content: [{ type: "text", text: lines.join("\n\n") }] };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function formatError(body, networkErrorMessage) {
|
|
166
|
+
if (networkErrorMessage) {
|
|
167
|
+
return { isError: true, content: [{ type: "text", text: `Network error: ${networkErrorMessage}` }] };
|
|
168
|
+
}
|
|
169
|
+
const error = body?.error || {};
|
|
170
|
+
const code = error.code || "unknown_error";
|
|
171
|
+
const message = error.message || "Request failed.";
|
|
172
|
+
const text =
|
|
173
|
+
code === "quota_exceeded"
|
|
174
|
+
? `Quota exceeded: your monthly or daily quota is exhausted. Upgrade at https://githunt.ai/pricing.`
|
|
175
|
+
: `${code}: ${message}`;
|
|
176
|
+
return { isError: true, content: [{ type: "text", text }] };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function handleApiCall(path, options = {}, formatter = null) {
|
|
180
|
+
let result;
|
|
181
|
+
try {
|
|
182
|
+
result = await callApi(path, options);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
return formatError(null, err.message);
|
|
185
|
+
}
|
|
186
|
+
if (!result.ok || !result.body?.success) {
|
|
187
|
+
return formatError(result.body);
|
|
188
|
+
}
|
|
189
|
+
return formatSuccess(result.body.data, result.body.meta, formatter);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const server = new McpServer({ name: "githunt", version: "0.1.0" });
|
|
193
|
+
|
|
194
|
+
server.registerTool(
|
|
195
|
+
"search_developers",
|
|
196
|
+
{
|
|
197
|
+
description:
|
|
198
|
+
"Search GitHunt's index of 318k+ GitHub developers by location, role, and skills. Returns ranked candidates with scores and contact info.",
|
|
199
|
+
inputSchema: {
|
|
200
|
+
location: z.string(),
|
|
201
|
+
role: z.string().optional(),
|
|
202
|
+
skills: z.array(z.string()).max(20).optional(),
|
|
203
|
+
languages: z.array(z.string()).max(10).optional(),
|
|
204
|
+
minExperienceYears: z.number().min(0).max(50).optional(),
|
|
205
|
+
isHireable: z.boolean().optional(),
|
|
206
|
+
strictSkills: z.boolean().optional(),
|
|
207
|
+
maxResults: z.number().min(1).max(100).default(25).optional(),
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
async (args) =>
|
|
211
|
+
handleApiCall("/v1/search", {
|
|
212
|
+
method: "POST",
|
|
213
|
+
body: JSON.stringify(args),
|
|
214
|
+
}, formatSearchResults)
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
server.registerTool(
|
|
218
|
+
"get_developer",
|
|
219
|
+
{
|
|
220
|
+
description:
|
|
221
|
+
"Get a single GitHub developer's ranked profile by username (score, experience, contact info).",
|
|
222
|
+
inputSchema: {
|
|
223
|
+
login: z.string(),
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
async ({ login }) => handleApiCall(`/v1/users/${encodeURIComponent(login)}`, {}, formatDeveloper)
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
server.registerTool(
|
|
230
|
+
"analyze_profile",
|
|
231
|
+
{
|
|
232
|
+
description:
|
|
233
|
+
"Deep AI analysis of a GitHub profile: proficiency level, role matches, extracted emails. Slower and more expensive than get_developer.",
|
|
234
|
+
inputSchema: {
|
|
235
|
+
username: z.string(),
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
async (args) =>
|
|
239
|
+
handleApiCall("/v1/profile/analyze", {
|
|
240
|
+
method: "POST",
|
|
241
|
+
body: JSON.stringify(args),
|
|
242
|
+
}, formatAnalysis)
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const transport = new StdioServerTransport();
|
|
246
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "githunt-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "MCP server for GitHunt's GitHub talent search API",
|
|
6
|
+
"bin": {
|
|
7
|
+
"githunt-mcp": "index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/mordka/githunt.git",
|
|
16
|
+
"directory": "mcp"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://docs.githunt.ai",
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"model-context-protocol",
|
|
22
|
+
"github",
|
|
23
|
+
"developer-search",
|
|
24
|
+
"recruiting",
|
|
25
|
+
"githunt"
|
|
26
|
+
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
35
|
+
"zod": "^4.4.3"
|
|
36
|
+
}
|
|
37
|
+
}
|