@sargonpiraev/hh-mcp-server 1.13.0 → 1.15.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 +280 -33
- package/build/main.d.ts +7 -0
- package/build/main.js +295 -0
- package/build/server.d.ts +27 -0
- package/build/server.js +3062 -0
- package/package.json +28 -19
- package/build/index.d.ts +0 -2
- package/build/index.js +0 -499
package/README.md
CHANGED
|
@@ -1,65 +1,312 @@
|
|
|
1
|
-
# HeadHunter MCP Server
|
|
1
|
+
# HeadHunter API MCP Server 🔧
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
[](https://discord.gg/ZsWGxRGj)
|
|
4
10
|
|
|
5
|
-
##
|
|
11
|
+
## Features
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
- 🔌 **Seamless AI Integration**: Direct HeadHunter API API access from Claude, Cursor, and VS Code
|
|
14
|
+
- 🤖 **Automated Workflows**: Automate HeadHunter API operations and data access
|
|
15
|
+
- 📊 **Complete API Coverage**: 167+ tools covering all major HeadHunter API features
|
|
16
|
+
- ⚡ **Real-time Access**: Access HeadHunter API data instantly from AI assistants
|
|
17
|
+
- 🔧 **Professional Integration**: Error handling, validation, and comprehensive logging
|
|
18
|
+
|
|
19
|
+
## Get Your Credentials
|
|
20
|
+
|
|
21
|
+
Before installation, you'll need a HeadHunter API API key:
|
|
22
|
+
|
|
23
|
+
1. Open HeadHunter API app or web interface
|
|
24
|
+
2. Go to **Settings → Account → API Access**
|
|
25
|
+
3. Generate new API key or copy existing one
|
|
26
|
+
4. Save this key for the installation steps below
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- Node.js >= v18.0.0
|
|
31
|
+
- HeadHunter API API key
|
|
32
|
+
- Cursor, VS Code, Claude Desktop or another MCP Client
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
<details>
|
|
37
|
+
<summary><b>Installing via Smithery</b></summary>
|
|
38
|
+
|
|
39
|
+
To install HeadHunter API MCP Server for any client automatically via [Smithery](https://smithery.ai):
|
|
8
40
|
|
|
9
41
|
```bash
|
|
10
|
-
|
|
42
|
+
npx -y @smithery/cli@latest install @sargonpiraev/hh-mcp-server --client <CLIENT_NAME>
|
|
11
43
|
```
|
|
12
44
|
|
|
13
|
-
|
|
45
|
+
</details>
|
|
14
46
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
47
|
+
<details>
|
|
48
|
+
<summary><b>Install in Cursor</b></summary>
|
|
49
|
+
|
|
50
|
+
#### Cursor One-Click Installation
|
|
51
|
+
|
|
52
|
+
[](https://cursor.com/install-mcp?name=@sargonpiraev/hh-mcp-server&config=)
|
|
53
|
+
|
|
54
|
+
#### Manual Configuration
|
|
55
|
+
|
|
56
|
+
Add to your Cursor `~/.cursor/mcp.json` file:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"mcpServers": {
|
|
61
|
+
"hh-mcp-server": {
|
|
62
|
+
"command": "npx",
|
|
63
|
+
"args": ["-y", "@sargonpiraev/hh-mcp-server"],
|
|
64
|
+
"env": {
|
|
65
|
+
"HH_CLIENT_ID": "your-hh_client_id",
|
|
66
|
+
"HH_CLIENT_SECRET": "your-hh_client_secret",
|
|
67
|
+
"HH_USER_AGENT": "your-hh_user_agent",
|
|
68
|
+
"HH_REDIRECT_URI": "your-hh_redirect_uri"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
18
73
|
```
|
|
19
74
|
|
|
20
|
-
|
|
75
|
+
</details>
|
|
21
76
|
|
|
22
|
-
|
|
23
|
-
|
|
77
|
+
<details>
|
|
78
|
+
<summary><b>Install in VS Code</b></summary>
|
|
79
|
+
|
|
80
|
+
[](vscode:mcp/install?%7B%22name%22%3A%22hh-mcp-server%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22@sargonpiraev/hh-mcp-server%22%5D%7D)
|
|
81
|
+
|
|
82
|
+
Or add manually to your VS Code settings:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
"mcp": {
|
|
86
|
+
"servers": {
|
|
87
|
+
"hh-mcp-server": {
|
|
88
|
+
"type": "stdio",
|
|
89
|
+
"command": "npx",
|
|
90
|
+
"args": ["-y", "@sargonpiraev/hh-mcp-server"],
|
|
91
|
+
"env": {
|
|
92
|
+
"HH_CLIENT_ID": "your-hh_client_id",
|
|
93
|
+
"HH_CLIENT_SECRET": "your-hh_client_secret",
|
|
94
|
+
"HH_USER_AGENT": "your-hh_user_agent",
|
|
95
|
+
"HH_REDIRECT_URI": "your-hh_redirect_uri"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
24
100
|
```
|
|
25
101
|
|
|
26
|
-
|
|
102
|
+
</details>
|
|
27
103
|
|
|
28
|
-
|
|
104
|
+
<details>
|
|
105
|
+
<summary><b>Install in Claude Desktop</b></summary>
|
|
106
|
+
|
|
107
|
+
Add to your `claude_desktop_config.json`:
|
|
29
108
|
|
|
30
109
|
```json
|
|
31
110
|
{
|
|
32
111
|
"mcpServers": {
|
|
33
112
|
"hh-mcp-server": {
|
|
34
|
-
"command": "
|
|
35
|
-
"args": ["/
|
|
113
|
+
"command": "npx",
|
|
114
|
+
"args": ["-y", "@sargonpiraev/hh-mcp-server"],
|
|
36
115
|
"env": {
|
|
37
|
-
"
|
|
116
|
+
"HH_CLIENT_ID": "your-hh_client_id",
|
|
117
|
+
"HH_CLIENT_SECRET": "your-hh_client_secret",
|
|
118
|
+
"HH_USER_AGENT": "your-hh_user_agent",
|
|
119
|
+
"HH_REDIRECT_URI": "your-hh_redirect_uri"
|
|
38
120
|
}
|
|
39
121
|
}
|
|
40
122
|
}
|
|
41
123
|
}
|
|
42
124
|
```
|
|
43
125
|
|
|
126
|
+
</details>
|
|
127
|
+
|
|
44
128
|
## Available Tools
|
|
45
129
|
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
130
|
+
- **`confirm-phone-in-resume`**: Verify phone with a code
|
|
131
|
+
- **`get-manager-settings`**: Manager preferences
|
|
132
|
+
- **`get-employer-manager-limits`**: Daily limit of resume views for current manager
|
|
133
|
+
- **`get-employer-addresses`**: Directory of employer's addresses
|
|
134
|
+
- **`get-employer-managers`**: Directory of employer's managers
|
|
135
|
+
- **`add-employer-manager`**: Adding a manager
|
|
136
|
+
- **`get-employer-manager-types`**: Directory of manager types and privileges
|
|
137
|
+
- **`get-manager-accounts`**: Manager's work accounts
|
|
138
|
+
- **`get-applicant-phone-info`**: Get information about the applicant's phone number
|
|
139
|
+
- **`get-address`**: Get address by ID
|
|
140
|
+
- **`edit-employer-manager`**: Editing a manager
|
|
141
|
+
- **`get-employer-manager`**: Getting information about a manager
|
|
142
|
+
- **`delete-employer-manager`**: Deleting a manager
|
|
143
|
+
- **`send-code-for-verify-phone-in-resume`**: Send verification code to the phone number on CV
|
|
144
|
+
- **`authorize`**: Getting an access-token
|
|
145
|
+
- **`invalidate-token`**: Access token invalidation
|
|
146
|
+
- **`get-current-user-info`**: Info on current authorized user
|
|
147
|
+
- **`edit-current-user-info`**: Editing information on the authorized user
|
|
148
|
+
- **`get-locales-for-resume`**: The list of available resume locales
|
|
149
|
+
- **`get-locales`**: The list of available locales
|
|
150
|
+
- **`get-positions-suggestions`**: Resume position suggestions
|
|
151
|
+
- **`get-educational-institutions-suggests`**: Educational institution name suggestions
|
|
152
|
+
- **`get-area-leaves-suggests`**: Suggestions for all regions that are leaves in the region tree
|
|
153
|
+
- **`get-skill-set-suggests`**: Key skills suggestions
|
|
154
|
+
- **`get-vacancy-positions-suggests`**: Vacancy position suggestions
|
|
155
|
+
- **`get-professional-roles-suggests`**: Professional role suggestions
|
|
156
|
+
- **`get-resume-search-keywords-suggests`**: Suggestions for resume search key words
|
|
157
|
+
- **`get-areas-suggests`**: Suggestions for all regions
|
|
158
|
+
- **`get-vacancy-search-keywords`**: Suggestions for vacancy search key words
|
|
159
|
+
- **`get-fields-of-study-suggestions`**: Specialization suggestions
|
|
160
|
+
- **`get-registered-companies-suggests`**: Organization suggestions
|
|
161
|
+
- **`read-resume-profile`**: Получение схемы резюме-профиля соискателя для резюме
|
|
162
|
+
- **`update-resume-profile`**: Обновление резюме-профиля соискателя
|
|
163
|
+
- **`create-resume-profile`**: Создание резюме-профиля соискателя
|
|
164
|
+
- **`get-resume-profile-dictionaries`**: Получение cловарей резюме-профиля
|
|
165
|
+
- **`get-payable-api-actions`**: Information about active API services for payable methods
|
|
166
|
+
- **`get-payable-api-method-access`**: Checking access to the paid methods
|
|
167
|
+
- **`get-saved-vacancy-searches`**: List of saved vacancy searches
|
|
168
|
+
- **`create-saved-vacancy-search`**: Creating new saved vacancy search
|
|
169
|
+
- **`get-vacancy-visitors`**: Vacancy visitors
|
|
170
|
+
- **`get-vacancy`**: View a vacancy
|
|
171
|
+
- **`edit-vacancy`**: Editing vacancies
|
|
172
|
+
- **`get-blacklisted-vacancies`**: List of hidden vacancies
|
|
173
|
+
- **`publish-vacancy`**: Publishing job vacancies
|
|
174
|
+
- **`get-vacancies`**: Search for vacancies
|
|
175
|
+
- **`get-vacancies-related-to-vacancy`**: Search for vacancies related to a vacancy
|
|
176
|
+
- **`get-saved-vacancy-search`**: Obtaining single saved vacancy search
|
|
177
|
+
- **`update-saved-vacancy-search`**: Updating saved vacancy search
|
|
178
|
+
- **`delete-saved-vacancy-search`**: Deleting saved vacancy search
|
|
179
|
+
- **`get-vacancies-similar-to-vacancy`**: Search for vacancies similar to a vacancy
|
|
180
|
+
- **`get-vacancy-upgrade-list`**: List of vacancy upgrades
|
|
181
|
+
- **`get-vacancies-similar-to-resume`**: Search for vacancies similar to a resume
|
|
182
|
+
- **`get-favorite-vacancies`**: List of favorited vacancies
|
|
183
|
+
- **`add-vacancy-to-blacklisted`**: Adding a vacancy in the blacklist
|
|
184
|
+
- **`delete-vacancy-from-blacklisted`**: Deleting a vacancy from the blacklist
|
|
185
|
+
- **`get-active-vacancy-list`**: View a published vacancy list
|
|
186
|
+
- **`get-hidden-vacancies`**: Deleted vacancy list
|
|
187
|
+
- **`add-vacancy-to-hidden`**: Deleting vacancies
|
|
188
|
+
- **`restore-vacancy-from-hidden`**: Restoring deleted vacancies
|
|
189
|
+
- **`get-vacancy-conditions`**: Conditions for filling out fields when publishing and editing vacancies
|
|
190
|
+
- **`get-prolongation-vacancy-info`**: Information about vacancy prolongation possibility
|
|
191
|
+
- **`vacancy-prolongation`**: Vacancy prolongation
|
|
192
|
+
- **`add-vacancy-to-archive`**: Archiving vacancies
|
|
193
|
+
- **`get-pref-negotiations-order`**: Viewing preferred options for sorting responses
|
|
194
|
+
- **`put-pref-negotiations-order`**: Changing preferred options for sorting responses
|
|
195
|
+
- **`add-vacancy-to-favorite`**: Add a vacancy in favorited
|
|
196
|
+
- **`delete-vacancy-from-favorite`**: Delete a vacancy from favorited
|
|
197
|
+
- **`get-available-vacancy-types`**: Possible options available to current manager for publishing of vacancies
|
|
198
|
+
- **`get-vacancy-stats`**: Vacancy statistics
|
|
199
|
+
- **`get-archived-vacancies`**: Archived vacancy list
|
|
200
|
+
- **`get-artifacts-portfolio-conditions`**: Conditions for uploading portfolio
|
|
201
|
+
- **`edit-artifact`**: Editing an artifact
|
|
202
|
+
- **`delete-artifact`**: Deleting an artifact
|
|
203
|
+
- **`load-artifact`**: Uploading an artifact
|
|
204
|
+
- **`get-artifacts-portfolio`**: Getting portfolios
|
|
205
|
+
- **`get-artifact-photos-conditions`**: Conditions for uploading photos
|
|
206
|
+
- **`get-artifact-photos`**: Getting photos
|
|
207
|
+
- **`get-dictionaries`**: Directories of fields
|
|
208
|
+
- **`get-languages`**: The list of all languages
|
|
209
|
+
- **`get-educational-institutions-dictionary`**: Basic information about educational institutions
|
|
210
|
+
- **`get-skills`**: The list of key skills
|
|
211
|
+
- **`get-professional-roles-dictionary`**: Professional role directory
|
|
212
|
+
- **`get-faculties`**: List of educational institution faculties
|
|
213
|
+
- **`get-industries`**: Industries
|
|
214
|
+
- **`change-negotiation-action`**: Actions with collection response/invitation
|
|
215
|
+
- **`apply-to-vacancy`**: Apply for a vacancy
|
|
216
|
+
- **`get-negotiations`**: Negotiation list
|
|
217
|
+
- **`get-negotiations-statistics-manager`**: Negotiation statistics for the manager
|
|
218
|
+
- **`get-active-negotiations`**: Active negotiation list
|
|
219
|
+
- **`get-negotiation-message-templates`**: Template list for the negotiation
|
|
220
|
+
- **`get-collection-negotiations-list`**: Negotiation list of the collection
|
|
221
|
+
- **`invite-applicant-to-vacancy`**: Invite applicant for a vacancy
|
|
222
|
+
- **`get-negotiation-test-results`**: Get test results attached to the vacancy
|
|
223
|
+
- **`edit-negotiation-message`**: Edit messages in the response
|
|
224
|
+
- **`post-negotiations-topics-read`**: Mark responses as read
|
|
225
|
+
- **`hide-active-response`**: Hide response
|
|
226
|
+
- **`get-negotiation-item`**: Viewing the response/invitation
|
|
227
|
+
- **`put-negotiations-collection-to-next-state`**: Actions with responses/invitations
|
|
228
|
+
- **`get-negotiations-statistics-employer`**: Negotiation statistics for the company
|
|
229
|
+
- **`send-negotiation-message`**: Sending new message
|
|
230
|
+
- **`get-negotiation-messages`**: View the list of messages in the negotiation
|
|
231
|
+
- **`get-vacancy-draft`**: Obtaining a vacancy draft
|
|
232
|
+
- **`change-vacancy-draft`**: Editing a vacancy draft
|
|
233
|
+
- **`delete-vacancy-draft`**: Deleting a vacancy draft
|
|
234
|
+
- **`publish-vacancy-from-draft`**: Publishing a vacancy from draft
|
|
235
|
+
- **`search-for-vacancy-draft-duplicates`**: Checking for duplicates of a vacancy draft
|
|
236
|
+
- **`create-vacancy-draft`**: Creating vacancy draft
|
|
237
|
+
- **`get-vacancy-draft-list`**: Getting a list of vacancy drafts
|
|
238
|
+
- **`disable-automatic-vacancy-publication`**: Canceling vacancy auto publication
|
|
239
|
+
- **`change-webhook-subscription`**: Change a subscription on notifications
|
|
240
|
+
- **`cancel-webhook-subscription`**: Delete a subscription on notifications
|
|
241
|
+
- **`post-webhook-subscription`**: Subscription to notifications
|
|
242
|
+
- **`get-webhook-subscriptions`**: Obtain the list of notifications that the user is subscripted
|
|
243
|
+
- **`get-tests-dictionary`**: Employer's test directory
|
|
244
|
+
- **`get-employer-vacancy-areas`**: List of regions with active vacancies
|
|
245
|
+
- **`get-employer-info`**: Employer info
|
|
246
|
+
- **`add-employer-to-blacklisted`**: Adding an employer to the blacklist
|
|
247
|
+
- **`delete-employer-from-blacklisted`**: Deleting an employer from the blacklist
|
|
248
|
+
- **`search-employer`**: Employer search
|
|
249
|
+
- **`get-employer-departments`**: Employer's department directory
|
|
250
|
+
- **`get-vacancy-branded-templates-list`**: Employer's branded vacancy templates
|
|
251
|
+
- **`get-blacklisted-employers`**: List of hidden employers
|
|
252
|
+
- **`get-all-districts`**: List of available city districts
|
|
253
|
+
- **`get-salary-evaluation`**: Salary assessment without forecasts
|
|
254
|
+
- **`get-metro-stations`**: The list of metro stations in all cities
|
|
255
|
+
- **`get-metro-stations-in-city`**: The list of metro stations in the specified city
|
|
256
|
+
- **`move-saved-resume-search`**: Moving saved resumes search to other manager
|
|
257
|
+
- **`get-resumes-by-status`**: Resumes grouped by the possibility of application for a given job
|
|
258
|
+
- **`get-resume-status`**: Resume status and readiness for publication
|
|
259
|
+
- **`get-resume-negotiations-history`**: History of responses/invitations for a resume
|
|
260
|
+
- **`get-saved-resume-search`**: Getting single saved resume search
|
|
261
|
+
- **`update-saved-resume-search`**: Updating saved resume search
|
|
262
|
+
- **`delete-saved-resume-search`**: Deleting saved resume search
|
|
263
|
+
- **`create-resume`**: Resume creating
|
|
264
|
+
- **`search-for-resumes`**: Resume search
|
|
265
|
+
- **`get-mine-resumes`**: List of resumes for current user
|
|
266
|
+
- **`publish-resume`**: Resume publication
|
|
267
|
+
- **`get-new-resume-conditions`**: Conditions to fill in the fields of a new resume
|
|
268
|
+
- **`get-suitable-resumes`**: List of resumes suitable for job application
|
|
269
|
+
- **`get-resume-conditions`**: Conditions to fill in the fields of an existent resume
|
|
270
|
+
- **`get-resume-view-history`**: History of resume views
|
|
271
|
+
- **`get-resume`**: View a resume
|
|
272
|
+
- **`delete-resume`**: Deleting a resume
|
|
273
|
+
- **`edit-resume`**: Resume updating
|
|
274
|
+
- **`get-resume-creation-availability`**: Availability of resume creation
|
|
275
|
+
- **`get-saved-resume-searches`**: List of Saved resume searches
|
|
276
|
+
- **`create-saved-resume-search`**: Creating new saved resumes search
|
|
277
|
+
- **`get-resume-access-types`**: Retrieving a list of resume visibility types
|
|
278
|
+
- **`update-applicant-comment`**: Update a comment
|
|
279
|
+
- **`delete-applicant-comment`**: Delete a comment
|
|
280
|
+
- **`get-applicant-comments-list`**: List of comments
|
|
281
|
+
- **`add-applicant-comment`**: Add a comment
|
|
282
|
+
- **`put-mail-templates-item`**: Edit a template for response to an applicant
|
|
283
|
+
- **`get-mail-templates`**: List of available templates for response to an applicant
|
|
284
|
+
- **`get-clickme-statistics`**: Getting info about Clickme ad campaign statistics
|
|
285
|
+
- **`get-countries`**: Countries
|
|
286
|
+
- **`get-areas`**: Tree view of all regions
|
|
287
|
+
- **`get-areas-from-specified`**: Region directory, starting from the specified region
|
|
288
|
+
- **`get-salary-employee-levels`**: Competency levels
|
|
289
|
+
- **`get-salary-salary-areas`**: Regions and cities
|
|
290
|
+
- **`get-salary-professional-areas`**: Professions and specializations
|
|
291
|
+
- **`get-salary-industries`**: Industries and fields of expertise
|
|
292
|
+
- **`get-resume-visibility-employers-list`**: Searching for employers to add to the visibility list
|
|
293
|
+
- **`get-resume-visibility-list`**: Getting visibility lists
|
|
294
|
+
- **`add-resume-visibility-list`**: Adding employers to the visibility list
|
|
295
|
+
- **`delete-resume-visibility-list`**: Clearing the visibility list
|
|
296
|
+
- **`delete-employer-from-resume-visibility-list`**: Removing employers from the visibility list
|
|
54
297
|
|
|
55
|
-
|
|
298
|
+
**Total: 167 tools available** 🎯
|
|
56
299
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
300
|
+
## Support This Project
|
|
301
|
+
|
|
302
|
+
Hi! I'm Sargon, a software engineer passionate about AI tools and automation. I create open-source MCP servers to help developers integrate AI assistants with their favorite services.
|
|
303
|
+
|
|
304
|
+
Your support helps me continue developing and maintaining these tools, and motivates me to create new integrations that make AI assistants even more powerful! 🚀
|
|
305
|
+
|
|
306
|
+
[](https://boosty.to/sargonpiraev)
|
|
62
307
|
|
|
63
|
-
##
|
|
308
|
+
## Connect with Author
|
|
64
309
|
|
|
65
|
-
|
|
310
|
+
- 🌐 Visit [sargonpiraev.com](https://sargonpiraev.com)
|
|
311
|
+
- 📧 Email: [sargonpiraev@gmail.com](mailto:sargonpiraev@gmail.com)
|
|
312
|
+
- 💬 Join [Discord](https://discord.gg/ZsWGxRGj)
|
package/build/main.d.ts
ADDED
package/build/main.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import cors from 'cors';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { mcpServer, envSchema as baseEnvSchema } from './server.js';
|
|
7
|
+
const envSchema = baseEnvSchema.extend({
|
|
8
|
+
PORT: z.string().optional().default('3000'),
|
|
9
|
+
HOST: z.string().optional().default('127.0.0.1'),
|
|
10
|
+
HH_CLIENT_ID: z.string(),
|
|
11
|
+
HH_CLIENT_SECRET: z.string(),
|
|
12
|
+
HH_USER_AGENT: z.string(),
|
|
13
|
+
HH_REDIRECT_URI: z.string(),
|
|
14
|
+
});
|
|
15
|
+
const env = envSchema.parse(process.env);
|
|
16
|
+
const app = express();
|
|
17
|
+
const sessions = new Map();
|
|
18
|
+
// Enable CORS for all routes
|
|
19
|
+
app.use(cors());
|
|
20
|
+
app.use(express.json({ limit: '10mb' }));
|
|
21
|
+
const logger = {
|
|
22
|
+
log: (...message) => {
|
|
23
|
+
console.log('[MCP]', ...message);
|
|
24
|
+
},
|
|
25
|
+
error: (...message) => {
|
|
26
|
+
console.error('[MCP ERROR]', ...message);
|
|
27
|
+
},
|
|
28
|
+
debug: (...message) => {
|
|
29
|
+
console.log('[MCP DEBUG]', ...message);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
// OAuth 2.0 Protected Resource Metadata endpoint
|
|
33
|
+
app.get('/.well-known/oauth-protected-resource', (req, res) => {
|
|
34
|
+
const baseUrl = `http://${env.HOST}:${env.PORT}`;
|
|
35
|
+
res.json({
|
|
36
|
+
resource: baseUrl,
|
|
37
|
+
authorization_servers: [baseUrl], // This MCP server provides authorization server metadata
|
|
38
|
+
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
|
|
39
|
+
bearer_methods_supported: ['header'],
|
|
40
|
+
// Note: HeadHunter does not support dynamic client registration
|
|
41
|
+
// resource_registration_endpoint is omitted to indicate this
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
// OAuth 2.0 Authorization Server Metadata endpoint - HeadHunter doesn't provide this, so we create it
|
|
45
|
+
app.get('/.well-known/oauth-authorization-server', (req, res) => {
|
|
46
|
+
res.json({
|
|
47
|
+
issuer: 'https://api.hh.ru',
|
|
48
|
+
authorization_endpoint: 'https://hh.ru/oauth/authorize',
|
|
49
|
+
token_endpoint: 'https://api.hh.ru/token',
|
|
50
|
+
registration_endpoint: `${req.protocol}://${req.get('host')}/oauth/register`,
|
|
51
|
+
jwks_uri: `${req.protocol}://${req.get('host')}/.well-known/jwks.json`,
|
|
52
|
+
response_types_supported: ['code'],
|
|
53
|
+
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
54
|
+
token_endpoint_auth_methods_supported: ['client_secret_post', 'client_secret_basic'],
|
|
55
|
+
code_challenge_methods_supported: ['S256'],
|
|
56
|
+
scopes_supported: [],
|
|
57
|
+
// Note: This endpoint simulates DCR for Inspector compatibility
|
|
58
|
+
// Real HeadHunter OAuth happens via pre-registered clients at dev.hh.ru
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
// JWKS endpoint - HeadHunter doesn't provide this, return empty keys for now
|
|
62
|
+
app.get('/.well-known/jwks.json', (req, res) => {
|
|
63
|
+
res.json({
|
|
64
|
+
keys: [],
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
// OAuth 2.0 Dynamic Client Registration endpoint (HeadHunter compatibility)
|
|
68
|
+
app.post('/oauth/register', (req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
const { client_name, grant_types = ['authorization_code'], response_types = ['code'], redirect_uris, scope = '', } = req.body;
|
|
71
|
+
// Validate required fields
|
|
72
|
+
if (!client_name) {
|
|
73
|
+
return res.status(400).json({
|
|
74
|
+
error: 'invalid_client_metadata',
|
|
75
|
+
error_description: 'client_name is required',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (!redirect_uris || !Array.isArray(redirect_uris) || redirect_uris.length === 0) {
|
|
79
|
+
return res.status(400).json({
|
|
80
|
+
error: 'invalid_redirect_uri',
|
|
81
|
+
error_description: 'redirect_uris is required and must be a non-empty array',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Check if OAuth credentials are configured
|
|
85
|
+
if (!env.HH_CLIENT_ID) {
|
|
86
|
+
return res.status(503).json({
|
|
87
|
+
error: 'oauth_not_configured',
|
|
88
|
+
error_description: 'OAuth client credentials not configured. Set HH_CLIENT_ID and HH_CLIENT_SECRET environment variables.',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// Return pre-configured OAuth client credentials
|
|
92
|
+
// This simulates DCR but actually uses static credentials
|
|
93
|
+
const client_id = env.HH_CLIENT_ID;
|
|
94
|
+
logger.log(`DCR simulation: Returning HeadHunter client credentials for "${client_name}"`);
|
|
95
|
+
// Return successful registration response with HeadHunter client_id
|
|
96
|
+
res.json({
|
|
97
|
+
client_id,
|
|
98
|
+
client_name,
|
|
99
|
+
grant_types,
|
|
100
|
+
response_types,
|
|
101
|
+
redirect_uris, // Use the redirect_uris from the request
|
|
102
|
+
scope,
|
|
103
|
+
token_endpoint_auth_method: 'client_secret_basic', // HeadHunter uses client_secret
|
|
104
|
+
client_id_issued_at: Math.floor(Date.now() / 1000),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
logger.error('DCR simulation error:', error instanceof Error ? error.message : String(error));
|
|
109
|
+
res.status(400).json({
|
|
110
|
+
error: 'invalid_request',
|
|
111
|
+
error_description: 'Invalid client registration request',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
// OAuth Client Configuration endpoint for MCP Inspector
|
|
116
|
+
app.get('/.well-known/oauth-client-config', (req, res) => {
|
|
117
|
+
if (!env.HH_CLIENT_ID) {
|
|
118
|
+
return res.status(503).json({
|
|
119
|
+
error: 'oauth_not_configured',
|
|
120
|
+
error_description: 'OAuth client ID not configured. Set HH_CLIENT_ID environment variable.',
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
res.json({
|
|
124
|
+
client_id: env.HH_CLIENT_ID,
|
|
125
|
+
authorization_endpoint: 'https://hh.ru/oauth/authorize',
|
|
126
|
+
token_endpoint: 'https://api.hh.ru/token',
|
|
127
|
+
scope: '', // OAuth server doesn't use explicit scopes
|
|
128
|
+
response_type: 'code',
|
|
129
|
+
redirect_uri: env.HH_REDIRECT_URI || 'http://localhost:6274/oauth/callback/debug', // Configurable redirect URI
|
|
130
|
+
code_challenge_method: 'S256', // PKCE for security
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
// Session management middleware
|
|
134
|
+
function getOrCreateSession(req) {
|
|
135
|
+
let sessionId = req.get('Mcp-Session-Id');
|
|
136
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
137
|
+
sessionId = crypto.randomUUID();
|
|
138
|
+
sessions.set(sessionId, {
|
|
139
|
+
id: sessionId,
|
|
140
|
+
createdAt: new Date(),
|
|
141
|
+
lastAccessAt: new Date(),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const session = sessions.get(sessionId);
|
|
146
|
+
if (session) {
|
|
147
|
+
session.lastAccessAt = new Date();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return sessionId;
|
|
151
|
+
}
|
|
152
|
+
// Authorization middleware
|
|
153
|
+
async function requireAuth(req, res, next) {
|
|
154
|
+
const authHeader = req.get('Authorization');
|
|
155
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
156
|
+
return res
|
|
157
|
+
.status(401)
|
|
158
|
+
.set('WWW-Authenticate', `Bearer realm="MCP Server", resource="http://${env.HOST}:${env.PORT}/.well-known/oauth-protected-resource"`)
|
|
159
|
+
.json({ error: 'Authorization required' });
|
|
160
|
+
}
|
|
161
|
+
const token = authHeader.substring(7);
|
|
162
|
+
if (!token) {
|
|
163
|
+
return res.status(401).json({ error: 'Invalid token' });
|
|
164
|
+
}
|
|
165
|
+
// Validate HeadHunter access token
|
|
166
|
+
try {
|
|
167
|
+
const response = await fetch('https://api.hh.ru/me', {
|
|
168
|
+
headers: {
|
|
169
|
+
Authorization: `Bearer ${token}`,
|
|
170
|
+
'User-Agent': 'MCP-Server/1.0',
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
logger.debug(`Token validation failed: ${response.status} ${response.statusText}`);
|
|
175
|
+
return res
|
|
176
|
+
.status(401)
|
|
177
|
+
.set('WWW-Authenticate', `Bearer realm="MCP Server", resource="http://${env.HOST}:${env.PORT}/.well-known/oauth-protected-resource", error="invalid_token"`)
|
|
178
|
+
.json({ error: 'Invalid or expired token' });
|
|
179
|
+
}
|
|
180
|
+
const userInfo = (await response.json());
|
|
181
|
+
const userEmail = userInfo.email;
|
|
182
|
+
const userId = userInfo.id;
|
|
183
|
+
logger.debug(`Authenticated user: ${userEmail || userId}`);
|
|
184
|
+
// Store user info in request for later use
|
|
185
|
+
req.user = userInfo;
|
|
186
|
+
next();
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
logger.error('Token validation error:', error instanceof Error ? error.message : String(error));
|
|
190
|
+
return res.status(500).json({ error: 'Token validation failed' });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// MCP endpoint - POST for JSON-RPC requests
|
|
194
|
+
app.post('/mcp', requireAuth, async (req, res) => {
|
|
195
|
+
try {
|
|
196
|
+
const sessionId = getOrCreateSession(req);
|
|
197
|
+
res.set('Mcp-Session-Id', sessionId);
|
|
198
|
+
res.set('Content-Type', 'application/json');
|
|
199
|
+
// Handle JSON-RPC request through MCP server
|
|
200
|
+
const result = await mcpServer.server.request(req.body, z.any());
|
|
201
|
+
res.json(result);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
console.error('MCP request error:', error);
|
|
205
|
+
res.status(500).json({
|
|
206
|
+
jsonrpc: '2.0',
|
|
207
|
+
error: {
|
|
208
|
+
code: -32603,
|
|
209
|
+
message: 'Internal error',
|
|
210
|
+
data: error instanceof Error ? error.message : String(error),
|
|
211
|
+
},
|
|
212
|
+
id: req.body?.id || null,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
// MCP endpoint - GET for SSE streams
|
|
217
|
+
app.get('/mcp', requireAuth, (req, res) => {
|
|
218
|
+
const sessionId = getOrCreateSession(req);
|
|
219
|
+
res.writeHead(200, {
|
|
220
|
+
'Content-Type': 'text/event-stream',
|
|
221
|
+
'Cache-Control': 'no-cache',
|
|
222
|
+
Connection: 'keep-alive',
|
|
223
|
+
'Access-Control-Allow-Origin': req.get('Origin') || '*',
|
|
224
|
+
'Access-Control-Allow-Headers': 'Authorization, Mcp-Session-Id',
|
|
225
|
+
'Mcp-Session-Id': sessionId,
|
|
226
|
+
});
|
|
227
|
+
// Send initial connection event
|
|
228
|
+
res.write(`id: ${Date.now()}\n`);
|
|
229
|
+
res.write(`event: connected\n`);
|
|
230
|
+
res.write(`data: {"sessionId": "${sessionId}"}\n\n`);
|
|
231
|
+
// Handle client disconnect
|
|
232
|
+
req.on('close', () => {
|
|
233
|
+
res.end();
|
|
234
|
+
});
|
|
235
|
+
// Keep connection alive
|
|
236
|
+
const keepAlive = setInterval(() => {
|
|
237
|
+
res.write(`id: ${Date.now()}\n`);
|
|
238
|
+
res.write(`event: heartbeat\n`);
|
|
239
|
+
res.write(`data: {"timestamp": "${new Date().toISOString()}"}\n\n`);
|
|
240
|
+
}, 30000);
|
|
241
|
+
req.on('close', () => {
|
|
242
|
+
clearInterval(keepAlive);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
// Session termination endpoint
|
|
246
|
+
app.delete('/mcp', requireAuth, (req, res) => {
|
|
247
|
+
const sessionId = req.get('Mcp-Session-Id');
|
|
248
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
249
|
+
sessions.delete(sessionId);
|
|
250
|
+
res.json({ message: 'Session terminated' });
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
res.status(404).json({ error: 'Session not found' });
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
// OAuth endpoints are handled by the authorization server directly:
|
|
257
|
+
// - Authorization: https://api.hh.ru/oauth/authorize (adjusted for auth domain)
|
|
258
|
+
// - Token: https://api.hh.ru/token
|
|
259
|
+
// - Inspector will communicate with the authorization server directly for OAuth flow
|
|
260
|
+
// Health check endpoint
|
|
261
|
+
app.get('/health', (req, res) => {
|
|
262
|
+
res.json({
|
|
263
|
+
status: 'healthy',
|
|
264
|
+
timestamp: new Date().toISOString(),
|
|
265
|
+
version: '',
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
async function main() {
|
|
269
|
+
try {
|
|
270
|
+
const port = parseInt(env.PORT);
|
|
271
|
+
const host = env.HOST;
|
|
272
|
+
app.listen(port, host, () => {
|
|
273
|
+
logger.log(` MCP Server (OAuth HTTP) started on http://${host}:${port}`);
|
|
274
|
+
logger.log(`MCP endpoint: http://${host}:${port}/mcp`);
|
|
275
|
+
logger.log(`OAuth metadata: http://${host}:${port}/.well-known/oauth-protected-resource`);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
console.error('Server startup error:', error);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Graceful shutdown
|
|
284
|
+
process.on('SIGINT', () => {
|
|
285
|
+
logger.log('Shutting down MCP Server...');
|
|
286
|
+
process.exit(0);
|
|
287
|
+
});
|
|
288
|
+
process.on('SIGTERM', () => {
|
|
289
|
+
logger.log('Shutting down MCP Server...');
|
|
290
|
+
process.exit(0);
|
|
291
|
+
});
|
|
292
|
+
main().catch((error) => {
|
|
293
|
+
console.error('Server error:', error);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { AxiosInstance } from 'axios';
|
|
4
|
+
export declare const envSchema: z.ZodObject<{
|
|
5
|
+
HH_CLIENT_ID: z.ZodString;
|
|
6
|
+
HH_CLIENT_SECRET: z.ZodString;
|
|
7
|
+
HH_USER_AGENT: z.ZodString;
|
|
8
|
+
HH_REDIRECT_URI: z.ZodString;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
HH_CLIENT_ID: string;
|
|
11
|
+
HH_CLIENT_SECRET: string;
|
|
12
|
+
HH_USER_AGENT: string;
|
|
13
|
+
HH_REDIRECT_URI: string;
|
|
14
|
+
}, {
|
|
15
|
+
HH_CLIENT_ID: string;
|
|
16
|
+
HH_CLIENT_SECRET: string;
|
|
17
|
+
HH_USER_AGENT: string;
|
|
18
|
+
HH_REDIRECT_URI: string;
|
|
19
|
+
}>;
|
|
20
|
+
export declare const mcpServer: McpServer;
|
|
21
|
+
export declare const env: {
|
|
22
|
+
HH_CLIENT_ID: string;
|
|
23
|
+
HH_CLIENT_SECRET: string;
|
|
24
|
+
HH_USER_AGENT: string;
|
|
25
|
+
HH_REDIRECT_URI: string;
|
|
26
|
+
};
|
|
27
|
+
export declare const apiClient: AxiosInstance;
|