@sargonpiraev/hh-mcp-server 1.15.0 → 1.16.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 (2) hide show
  1. package/build/main.js +140 -14
  2. package/package.json +1 -1
package/build/main.js CHANGED
@@ -6,7 +6,7 @@ import { z } from 'zod';
6
6
  import { mcpServer, envSchema as baseEnvSchema } from './server.js';
7
7
  const envSchema = baseEnvSchema.extend({
8
8
  PORT: z.string().optional().default('3000'),
9
- HOST: z.string().optional().default('127.0.0.1'),
9
+ HOST: z.string().optional().default('localhost'),
10
10
  HH_CLIENT_ID: z.string(),
11
11
  HH_CLIENT_SECRET: z.string(),
12
12
  HH_USER_AGENT: z.string(),
@@ -43,19 +43,19 @@ app.get('/.well-known/oauth-protected-resource', (req, res) => {
43
43
  });
44
44
  // OAuth 2.0 Authorization Server Metadata endpoint - HeadHunter doesn't provide this, so we create it
45
45
  app.get('/.well-known/oauth-authorization-server', (req, res) => {
46
+ const baseUrl = `http://${env.HOST}:${env.PORT}`;
46
47
  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`,
48
+ issuer: baseUrl, // This MCP server IS the authorization server now
49
+ authorization_endpoint: `${baseUrl}/oauth/authorize`, // Our facade endpoint
50
+ token_endpoint: `${baseUrl}/oauth/token`, // Our facade endpoint
51
+ registration_endpoint: `${baseUrl}/oauth/register`,
52
+ jwks_uri: `${baseUrl}/.well-known/jwks.json`,
52
53
  response_types_supported: ['code'],
53
54
  grant_types_supported: ['authorization_code', 'refresh_token'],
54
55
  token_endpoint_auth_methods_supported: ['client_secret_post', 'client_secret_basic'],
55
56
  code_challenge_methods_supported: ['S256'],
56
57
  scopes_supported: [],
57
- // Note: This endpoint simulates DCR for Inspector compatibility
58
- // Real HeadHunter OAuth happens via pre-registered clients at dev.hh.ru
58
+ // This is now a real OAuth Authorization Server, not just metadata
59
59
  });
60
60
  });
61
61
  // JWKS endpoint - HeadHunter doesn't provide this, return empty keys for now
@@ -91,16 +91,16 @@ app.post('/oauth/register', (req, res) => {
91
91
  // Return pre-configured OAuth client credentials
92
92
  // This simulates DCR but actually uses static credentials
93
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
94
+ logger.log(`DCR facade: Accepting any redirect_uri for "${client_name}"`);
95
+ // Return successful registration response - now we accept ANY redirect_uri
96
96
  res.json({
97
97
  client_id,
98
98
  client_name,
99
99
  grant_types,
100
100
  response_types,
101
- redirect_uris, // Use the redirect_uris from the request
101
+ redirect_uris, // Accept client's redirect_uris as-is (facade supports any URI)
102
102
  scope,
103
- token_endpoint_auth_method: 'client_secret_basic', // HeadHunter uses client_secret
103
+ token_endpoint_auth_method: 'client_secret_basic',
104
104
  client_id_issued_at: Math.floor(Date.now() / 1000),
105
105
  });
106
106
  }
@@ -120,16 +120,142 @@ app.get('/.well-known/oauth-client-config', (req, res) => {
120
120
  error_description: 'OAuth client ID not configured. Set HH_CLIENT_ID environment variable.',
121
121
  });
122
122
  }
123
+ const baseUrl = `http://${env.HOST}:${env.PORT}`;
123
124
  res.json({
124
125
  client_id: env.HH_CLIENT_ID,
125
- authorization_endpoint: 'https://hh.ru/oauth/authorize',
126
- token_endpoint: 'https://api.hh.ru/token',
126
+ authorization_endpoint: `${baseUrl}/oauth/authorize`, // Our facade endpoint
127
+ token_endpoint: `${baseUrl}/oauth/token`, // Our facade endpoint
127
128
  scope: '', // OAuth server doesn't use explicit scopes
128
129
  response_type: 'code',
129
130
  redirect_uri: env.HH_REDIRECT_URI || 'http://localhost:6274/oauth/callback/debug', // Configurable redirect URI
130
131
  code_challenge_method: 'S256', // PKCE for security
131
132
  });
132
133
  });
134
+ // Facade OAuth Authorization endpoint - accepts any redirect_uri and delegates to HeadHunter
135
+ app.get('/oauth/authorize', (req, res) => {
136
+ const { client_id, redirect_uri, state, code_challenge, code_challenge_method = 'S256' } = req.query;
137
+ // Validate required parameters
138
+ if (!client_id || !redirect_uri) {
139
+ return res.status(400).json({
140
+ error: 'invalid_request',
141
+ error_description: 'client_id and redirect_uri are required',
142
+ });
143
+ }
144
+ // Store original request parameters for later use in callback
145
+ const stateKey = `auth_${crypto.randomUUID()}`;
146
+ sessions.set(stateKey, {
147
+ originalRedirectUri: redirect_uri,
148
+ originalState: state,
149
+ clientId: client_id,
150
+ codeChallenge: code_challenge,
151
+ codeChallengeMethod: code_challenge_method,
152
+ createdAt: new Date(),
153
+ });
154
+ // Construct HeadHunter authorization URL with our callback
155
+ const hhAuthUrl = new URL('https://hh.ru/oauth/authorize');
156
+ hhAuthUrl.searchParams.set('client_id', env.HH_CLIENT_ID || '');
157
+ hhAuthUrl.searchParams.set('response_type', 'code');
158
+ hhAuthUrl.searchParams.set('redirect_uri', env.HH_REDIRECT_URI || `http://${env.HOST}:${env.PORT}/oauth/callback/debug`);
159
+ hhAuthUrl.searchParams.set('state', stateKey); // Use our state key
160
+ logger.log(`Facade authorize: Redirecting to HeadHunter with state=${stateKey}`);
161
+ // Redirect user to HeadHunter authorization
162
+ res.redirect(hhAuthUrl.toString());
163
+ });
164
+ // Facade OAuth Token endpoint - exchanges authorization code for access token
165
+ app.post('/oauth/token', express.urlencoded({ extended: true }), async (req, res) => {
166
+ const { grant_type, client_id, code, redirect_uri, code_verifier } = req.body;
167
+ // Validate required parameters
168
+ if (grant_type !== 'authorization_code' || !client_id || !code || !redirect_uri) {
169
+ return res.status(400).json({
170
+ error: 'invalid_request',
171
+ error_description: 'Invalid token request parameters',
172
+ });
173
+ }
174
+ try {
175
+ // Use the configured redirect_uri instead of the client's redirect_uri
176
+ const configuredRedirectUri = env.HH_REDIRECT_URI || `http://${env.HOST}:${env.PORT}/oauth/callback/debug`;
177
+ // Prepare token request for HeadHunter
178
+ const tokenParams = new URLSearchParams({
179
+ grant_type: 'authorization_code',
180
+ client_id: env.HH_CLIENT_ID || '',
181
+ client_secret: env.HH_CLIENT_SECRET || '',
182
+ code: code,
183
+ redirect_uri: configuredRedirectUri, // Use our configured redirect_uri, not client's
184
+ });
185
+ // Add PKCE code_verifier if provided by client
186
+ if (code_verifier) {
187
+ tokenParams.set('code_verifier', code_verifier);
188
+ }
189
+ logger.log(`Facade token: Exchanging code with HeadHunter, redirect_uri=${configuredRedirectUri}`);
190
+ // Exchange code with HeadHunter token endpoint
191
+ const tokenResponse = await fetch('https://api.hh.ru/token', {
192
+ method: 'POST',
193
+ headers: {
194
+ 'Content-Type': 'application/x-www-form-urlencoded',
195
+ 'User-Agent': 'MCP-Facade-Server/1.0',
196
+ },
197
+ body: tokenParams,
198
+ });
199
+ if (!tokenResponse.ok) {
200
+ const errorText = await tokenResponse.text();
201
+ logger.error(`HeadHunter token exchange failed: ${tokenResponse.status} ${errorText}`);
202
+ return res.status(400).json({
203
+ error: 'invalid_grant',
204
+ error_description: 'Authorization code is invalid or expired',
205
+ });
206
+ }
207
+ const tokenData = (await tokenResponse.json());
208
+ logger.log(`Facade token: Successfully exchanged code for HeadHunter access token`);
209
+ // Return the token response from HeadHunter (with our issuer)
210
+ res.json({
211
+ ...tokenData,
212
+ issuer: `http://${env.HOST}:${env.PORT}`, // Override issuer to point to our facade
213
+ });
214
+ }
215
+ catch (error) {
216
+ logger.error('Token exchange error:', error instanceof Error ? error.message : String(error));
217
+ res.status(500).json({
218
+ error: 'server_error',
219
+ error_description: 'Internal server error during token exchange',
220
+ });
221
+ }
222
+ });
223
+ // Enhanced proxy callback endpoint for facade authorization flow
224
+ app.get('/oauth/callback/debug', (req, res) => {
225
+ const { code, state, error, error_description } = req.query;
226
+ if (error) {
227
+ logger.error(`HeadHunter authorization error: ${error} - ${error_description}`);
228
+ return res.status(400).json({
229
+ error: error,
230
+ error_description: error_description,
231
+ });
232
+ }
233
+ if (!code || !state) {
234
+ return res.status(400).json({
235
+ error: 'invalid_request',
236
+ error_description: 'Missing authorization code or state parameter',
237
+ });
238
+ }
239
+ // Retrieve stored authorization request
240
+ const authSession = sessions.get(state);
241
+ if (!authSession) {
242
+ logger.error(`No session found for state: ${state}`);
243
+ return res.status(400).json({
244
+ error: 'invalid_state',
245
+ error_description: 'Invalid or expired authorization request',
246
+ });
247
+ }
248
+ // Clean up session
249
+ sessions.delete(state);
250
+ // Redirect back to original client with authorization code
251
+ const callbackUrl = new URL(authSession.originalRedirectUri);
252
+ callbackUrl.searchParams.set('code', code);
253
+ if (authSession.originalState) {
254
+ callbackUrl.searchParams.set('state', authSession.originalState);
255
+ }
256
+ logger.log(`Facade callback: Redirecting to original client: ${callbackUrl.toString()}`);
257
+ res.redirect(callbackUrl.toString());
258
+ });
133
259
  // Session management middleware
134
260
  function getOrCreateSession(req) {
135
261
  let sessionId = req.get('Mcp-Session-Id');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sargonpiraev/hh-mcp-server",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "HeadHunter API MCP Server provides seamless integration between AI tools and HeadHunter's comprehensive job search platform.",
5
5
  "keywords": [
6
6
  "mcp",