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.
Files changed (3) hide show
  1. package/README.md +68 -0
  2. package/index.js +246 -0
  3. 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
+ }