@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 CHANGED
@@ -1,65 +1,312 @@
1
- # HeadHunter MCP Server
1
+ # HeadHunter API MCP Server 🔧
2
2
 
3
- MCP Server for HeadHunter API - job search and vacancy management
3
+ ![npm version](https://img.shields.io/npm/v/@sargonpiraev/hh-mcp-server)
4
+ ![npm downloads](https://img.shields.io/npm/dw/@sargonpiraev/hh-mcp-server)
5
+ ![license](https://img.shields.io/github/license/sargonpiraev/hh-mcp-server)
6
+ ![pipeline status](https://gitlab.com/sargonpiraev/hh-mcp-server/badges/main/pipeline.svg)
7
+ ![smithery badge](https://smithery.ai/badge/@sargonpiraev/hh-mcp-server)
8
+ ![MCP Compatible](https://img.shields.io/badge/MCP-Compatible-blue)
9
+ [![Join Discord](https://img.shields.io/discord/1331631275464671347?color=7289da&label=Discord&logo=discord)](https://discord.gg/ZsWGxRGj)
4
10
 
5
- ## Quick Start
11
+ ## Features
6
12
 
7
- 1. Install dependencies:
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
- npm install
42
+ npx -y @smithery/cli@latest install @sargonpiraev/hh-mcp-server --client <CLIENT_NAME>
11
43
  ```
12
44
 
13
- 2. Set up environment:
45
+ </details>
14
46
 
15
- ```bash
16
- cp .env.example .env
17
- # Edit .env with your API credentials
47
+ <details>
48
+ <summary><b>Install in Cursor</b></summary>
49
+
50
+ #### Cursor One-Click Installation
51
+
52
+ [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](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
- 3. Start the server:
75
+ </details>
21
76
 
22
- ```bash
23
- npm start
77
+ <details>
78
+ <summary><b>Install in VS Code</b></summary>
79
+
80
+ [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_MCP-0098FF)](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
- ## Configuration
102
+ </details>
27
103
 
28
- Add to your MCP client config:
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": "node",
35
- "args": ["/dist/index.js"],
113
+ "command": "npx",
114
+ "args": ["-y", "@sargonpiraev/hh-mcp-server"],
36
115
  "env": {
37
- "HH_USER_AGENT": "your_hh_user_agent"
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
- - **get-vacancies**: Search for vacancies
47
- - **get-dictionaries**: Directories of fields
48
- - **get-areas**: Tree view of all regions
49
- - **get-languages**: The list of all languages
50
- - **get-industries**: Industries
51
- - **get-metro**: The list of metro stations in all cities
52
- - **get-professional-roles**: Professional role directory
53
- - **get-employers**: Employer search
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&#x27;s addresses
134
+ - **`get-employer-managers`**: Directory of employer&#x27;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&#x27;s work accounts
138
+ - **`get-applicant-phone-info`**: Get information about the applicant&#x27;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&#x27;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&#x27;s department directory
250
+ - **`get-vacancy-branded-templates-list`**: Employer&#x27;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
- ## Development
298
+ **Total: 167 tools available** 🎯
56
299
 
57
- ```bash
58
- npm run build # Build TypeScript
59
- npm run test # Run tests
60
- npm run lint # Check code style
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
+ [![Support on Boosty](https://img.shields.io/badge/Support-Boosty-orange?logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMMTMuMDkgOC4yNkwyMCA5TDEzLjA5IDE1Ljc0TDEyIDIyTDEwLjkxIDE1Ljc0TDQgOUwxMC45MSA4LjI2TDEyIDJaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K)](https://boosty.to/sargonpiraev)
62
307
 
63
- ## License
308
+ ## Connect with Author
64
309
 
65
- MIT
310
+ - 🌐 Visit [sargonpiraev.com](https://sargonpiraev.com)
311
+ - 📧 Email: [sargonpiraev@gmail.com](mailto:sargonpiraev@gmail.com)
312
+ - 💬 Join [Discord](https://discord.gg/ZsWGxRGj)
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ declare module 'express-serve-static-core' {
3
+ interface Request {
4
+ user?: Record<string, unknown>;
5
+ }
6
+ }
7
+ export {};
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;