@sargonpiraev/hh-mcp-server 1.14.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.
- package/README.md +280 -33
- package/build/main.d.ts +7 -0
- package/build/main.js +421 -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/build/main.js
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
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('localhost'),
|
|
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
|
+
const baseUrl = `http://${env.HOST}:${env.PORT}`;
|
|
47
|
+
res.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`,
|
|
53
|
+
response_types_supported: ['code'],
|
|
54
|
+
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
55
|
+
token_endpoint_auth_methods_supported: ['client_secret_post', 'client_secret_basic'],
|
|
56
|
+
code_challenge_methods_supported: ['S256'],
|
|
57
|
+
scopes_supported: [],
|
|
58
|
+
// This is now a real OAuth Authorization Server, not just metadata
|
|
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 facade: Accepting any redirect_uri for "${client_name}"`);
|
|
95
|
+
// Return successful registration response - now we accept ANY redirect_uri
|
|
96
|
+
res.json({
|
|
97
|
+
client_id,
|
|
98
|
+
client_name,
|
|
99
|
+
grant_types,
|
|
100
|
+
response_types,
|
|
101
|
+
redirect_uris, // Accept client's redirect_uris as-is (facade supports any URI)
|
|
102
|
+
scope,
|
|
103
|
+
token_endpoint_auth_method: 'client_secret_basic',
|
|
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
|
+
const baseUrl = `http://${env.HOST}:${env.PORT}`;
|
|
124
|
+
res.json({
|
|
125
|
+
client_id: env.HH_CLIENT_ID,
|
|
126
|
+
authorization_endpoint: `${baseUrl}/oauth/authorize`, // Our facade endpoint
|
|
127
|
+
token_endpoint: `${baseUrl}/oauth/token`, // Our facade endpoint
|
|
128
|
+
scope: '', // OAuth server doesn't use explicit scopes
|
|
129
|
+
response_type: 'code',
|
|
130
|
+
redirect_uri: env.HH_REDIRECT_URI || 'http://localhost:6274/oauth/callback/debug', // Configurable redirect URI
|
|
131
|
+
code_challenge_method: 'S256', // PKCE for security
|
|
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
|
+
});
|
|
259
|
+
// Session management middleware
|
|
260
|
+
function getOrCreateSession(req) {
|
|
261
|
+
let sessionId = req.get('Mcp-Session-Id');
|
|
262
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
263
|
+
sessionId = crypto.randomUUID();
|
|
264
|
+
sessions.set(sessionId, {
|
|
265
|
+
id: sessionId,
|
|
266
|
+
createdAt: new Date(),
|
|
267
|
+
lastAccessAt: new Date(),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
const session = sessions.get(sessionId);
|
|
272
|
+
if (session) {
|
|
273
|
+
session.lastAccessAt = new Date();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return sessionId;
|
|
277
|
+
}
|
|
278
|
+
// Authorization middleware
|
|
279
|
+
async function requireAuth(req, res, next) {
|
|
280
|
+
const authHeader = req.get('Authorization');
|
|
281
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
282
|
+
return res
|
|
283
|
+
.status(401)
|
|
284
|
+
.set('WWW-Authenticate', `Bearer realm="MCP Server", resource="http://${env.HOST}:${env.PORT}/.well-known/oauth-protected-resource"`)
|
|
285
|
+
.json({ error: 'Authorization required' });
|
|
286
|
+
}
|
|
287
|
+
const token = authHeader.substring(7);
|
|
288
|
+
if (!token) {
|
|
289
|
+
return res.status(401).json({ error: 'Invalid token' });
|
|
290
|
+
}
|
|
291
|
+
// Validate HeadHunter access token
|
|
292
|
+
try {
|
|
293
|
+
const response = await fetch('https://api.hh.ru/me', {
|
|
294
|
+
headers: {
|
|
295
|
+
Authorization: `Bearer ${token}`,
|
|
296
|
+
'User-Agent': 'MCP-Server/1.0',
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
if (!response.ok) {
|
|
300
|
+
logger.debug(`Token validation failed: ${response.status} ${response.statusText}`);
|
|
301
|
+
return res
|
|
302
|
+
.status(401)
|
|
303
|
+
.set('WWW-Authenticate', `Bearer realm="MCP Server", resource="http://${env.HOST}:${env.PORT}/.well-known/oauth-protected-resource", error="invalid_token"`)
|
|
304
|
+
.json({ error: 'Invalid or expired token' });
|
|
305
|
+
}
|
|
306
|
+
const userInfo = (await response.json());
|
|
307
|
+
const userEmail = userInfo.email;
|
|
308
|
+
const userId = userInfo.id;
|
|
309
|
+
logger.debug(`Authenticated user: ${userEmail || userId}`);
|
|
310
|
+
// Store user info in request for later use
|
|
311
|
+
req.user = userInfo;
|
|
312
|
+
next();
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
logger.error('Token validation error:', error instanceof Error ? error.message : String(error));
|
|
316
|
+
return res.status(500).json({ error: 'Token validation failed' });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// MCP endpoint - POST for JSON-RPC requests
|
|
320
|
+
app.post('/mcp', requireAuth, async (req, res) => {
|
|
321
|
+
try {
|
|
322
|
+
const sessionId = getOrCreateSession(req);
|
|
323
|
+
res.set('Mcp-Session-Id', sessionId);
|
|
324
|
+
res.set('Content-Type', 'application/json');
|
|
325
|
+
// Handle JSON-RPC request through MCP server
|
|
326
|
+
const result = await mcpServer.server.request(req.body, z.any());
|
|
327
|
+
res.json(result);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
console.error('MCP request error:', error);
|
|
331
|
+
res.status(500).json({
|
|
332
|
+
jsonrpc: '2.0',
|
|
333
|
+
error: {
|
|
334
|
+
code: -32603,
|
|
335
|
+
message: 'Internal error',
|
|
336
|
+
data: error instanceof Error ? error.message : String(error),
|
|
337
|
+
},
|
|
338
|
+
id: req.body?.id || null,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
// MCP endpoint - GET for SSE streams
|
|
343
|
+
app.get('/mcp', requireAuth, (req, res) => {
|
|
344
|
+
const sessionId = getOrCreateSession(req);
|
|
345
|
+
res.writeHead(200, {
|
|
346
|
+
'Content-Type': 'text/event-stream',
|
|
347
|
+
'Cache-Control': 'no-cache',
|
|
348
|
+
Connection: 'keep-alive',
|
|
349
|
+
'Access-Control-Allow-Origin': req.get('Origin') || '*',
|
|
350
|
+
'Access-Control-Allow-Headers': 'Authorization, Mcp-Session-Id',
|
|
351
|
+
'Mcp-Session-Id': sessionId,
|
|
352
|
+
});
|
|
353
|
+
// Send initial connection event
|
|
354
|
+
res.write(`id: ${Date.now()}\n`);
|
|
355
|
+
res.write(`event: connected\n`);
|
|
356
|
+
res.write(`data: {"sessionId": "${sessionId}"}\n\n`);
|
|
357
|
+
// Handle client disconnect
|
|
358
|
+
req.on('close', () => {
|
|
359
|
+
res.end();
|
|
360
|
+
});
|
|
361
|
+
// Keep connection alive
|
|
362
|
+
const keepAlive = setInterval(() => {
|
|
363
|
+
res.write(`id: ${Date.now()}\n`);
|
|
364
|
+
res.write(`event: heartbeat\n`);
|
|
365
|
+
res.write(`data: {"timestamp": "${new Date().toISOString()}"}\n\n`);
|
|
366
|
+
}, 30000);
|
|
367
|
+
req.on('close', () => {
|
|
368
|
+
clearInterval(keepAlive);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
// Session termination endpoint
|
|
372
|
+
app.delete('/mcp', requireAuth, (req, res) => {
|
|
373
|
+
const sessionId = req.get('Mcp-Session-Id');
|
|
374
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
375
|
+
sessions.delete(sessionId);
|
|
376
|
+
res.json({ message: 'Session terminated' });
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
res.status(404).json({ error: 'Session not found' });
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
// OAuth endpoints are handled by the authorization server directly:
|
|
383
|
+
// - Authorization: https://api.hh.ru/oauth/authorize (adjusted for auth domain)
|
|
384
|
+
// - Token: https://api.hh.ru/token
|
|
385
|
+
// - Inspector will communicate with the authorization server directly for OAuth flow
|
|
386
|
+
// Health check endpoint
|
|
387
|
+
app.get('/health', (req, res) => {
|
|
388
|
+
res.json({
|
|
389
|
+
status: 'healthy',
|
|
390
|
+
timestamp: new Date().toISOString(),
|
|
391
|
+
version: '',
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
async function main() {
|
|
395
|
+
try {
|
|
396
|
+
const port = parseInt(env.PORT);
|
|
397
|
+
const host = env.HOST;
|
|
398
|
+
app.listen(port, host, () => {
|
|
399
|
+
logger.log(` MCP Server (OAuth HTTP) started on http://${host}:${port}`);
|
|
400
|
+
logger.log(`MCP endpoint: http://${host}:${port}/mcp`);
|
|
401
|
+
logger.log(`OAuth metadata: http://${host}:${port}/.well-known/oauth-protected-resource`);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
console.error('Server startup error:', error);
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Graceful shutdown
|
|
410
|
+
process.on('SIGINT', () => {
|
|
411
|
+
logger.log('Shutting down MCP Server...');
|
|
412
|
+
process.exit(0);
|
|
413
|
+
});
|
|
414
|
+
process.on('SIGTERM', () => {
|
|
415
|
+
logger.log('Shutting down MCP Server...');
|
|
416
|
+
process.exit(0);
|
|
417
|
+
});
|
|
418
|
+
main().catch((error) => {
|
|
419
|
+
console.error('Server error:', error);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
});
|
|
@@ -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;
|