|
92 | 92 | const server = new Server( |
93 | 93 | { |
94 | 94 | name: 'strava-mcp-server', |
95 | | - version: '1.0.0', |
| 95 | + version: '3.0.0', |
96 | 96 | }, |
97 | 97 | { |
98 | 98 | capabilities: { |
@@ -325,50 +325,91 @@ async function main() { |
325 | 325 |
|
326 | 326 | // Authentication middleware for local dev (optional - can be disabled) |
327 | 327 | // Supports both Authorization header and query parameter |
| 328 | + // When ALLOW_AUTHLESS=true, SSE endpoints bypass auth for Claude.ai custom connectors |
328 | 329 | app.use((req: Request, res: Response, next) => { |
329 | | - if (req.path === '/health') { |
| 330 | + if (req.path === '/health' || req.path === '/debug') { |
330 | 331 | return next(); |
331 | 332 | } |
332 | 333 |
|
333 | | - // For SSE endpoint, authentication happens via query param or header |
334 | | - if (req.path === '/sse' || req.path === '/sse/') { |
335 | | - // SSE authentication happens here before establishing connection |
336 | | - // Continue to token validation below |
| 334 | + // Check if authless mode is enabled for SSE endpoints |
| 335 | + // This allows Claude.ai custom connectors to connect without Bearer tokens |
| 336 | + const isSSEEndpoint = req.path === '/sse' || req.path === '/sse/'; |
| 337 | + const isMessageEndpoint = req.path === '/message'; |
| 338 | + |
| 339 | + if (config.ALLOW_AUTHLESS && (isSSEEndpoint || isMessageEndpoint)) { |
| 340 | + if (isSSEEndpoint) { |
| 341 | + console.error('[StravaServer] Authless SSE connection allowed (ALLOW_AUTHLESS=true)'); |
| 342 | + } |
| 343 | + return next(); |
337 | 344 | } |
338 | 345 |
|
339 | | - // For SSE message endpoint, trust valid session IDs |
| 346 | + // For SSE message endpoint with valid session, trust the session |
340 | 347 | const sessionId = req.query.sessionId as string; |
341 | | - if (req.path === '/message' && sessionId && transports[sessionId]) { |
| 348 | + if (isMessageEndpoint && sessionId && transports[sessionId]) { |
342 | 349 | return next(); |
343 | 350 | } |
344 | 351 |
|
345 | 352 | // For local development, authentication is optional |
346 | 353 | // If AUTH_TOKEN is set in .env, validate it |
347 | 354 | if (config.AUTH_TOKEN && config.AUTH_TOKEN.length > 0) { |
348 | 355 | let token: string | undefined; |
349 | | - |
| 356 | + |
350 | 357 | const authHeader = req.headers['authorization']; |
351 | 358 | if (authHeader && authHeader.startsWith('Bearer ')) { |
352 | 359 | token = authHeader.substring(7); |
353 | 360 | } else if (req.query.token) { |
354 | 361 | token = req.query.token as string; |
355 | 362 | } |
356 | | - |
| 363 | + |
357 | 364 | if (!token || token !== config.AUTH_TOKEN) { |
358 | 365 | console.error('[StravaServer] Invalid or missing token'); |
359 | | - return res.status(401).json({ |
| 366 | + return res.status(401).json({ |
360 | 367 | error: 'Unauthorized', |
361 | 368 | message: 'Invalid or missing token' |
362 | 369 | }); |
363 | 370 | } |
364 | 371 | } |
365 | | - |
| 372 | + |
366 | 373 | next(); |
367 | 374 | }); |
368 | 375 |
|
369 | | - // Health check endpoint |
| 376 | + // Health check endpoint - enhanced with diagnostic info |
370 | 377 | app.get('/health', (_req: Request, res: Response) => { |
371 | | - res.json({ status: 'healthy', version: '2.0.0' }); |
| 378 | + res.json({ |
| 379 | + status: 'healthy', |
| 380 | + version: '3.0.0', |
| 381 | + runtime: 'local', |
| 382 | + authless: config?.ALLOW_AUTHLESS ?? false, |
| 383 | + timestamp: new Date().toISOString(), |
| 384 | + }); |
| 385 | + }); |
| 386 | + |
| 387 | + // Debug endpoint - helps troubleshoot Claude.ai connection issues |
| 388 | + app.get('/debug', (_req: Request, res: Response) => { |
| 389 | + res.json({ |
| 390 | + status: 'ok', |
| 391 | + version: '3.0.0', |
| 392 | + authless_enabled: config?.ALLOW_AUTHLESS ?? false, |
| 393 | + environment: process.env.NODE_ENV || 'development', |
| 394 | + endpoints: { |
| 395 | + health: '/health', |
| 396 | + debug: '/debug', |
| 397 | + sse: '/sse (GET, establishes SSE connection)', |
| 398 | + message: '/message (POST, requires sessionId query param)', |
| 399 | + mcp: '/mcp (POST, requires Bearer token)', |
| 400 | + }, |
| 401 | + claude_ai_setup: { |
| 402 | + connector_url: `Use base URL only: http://localhost:${config.PORT}`, |
| 403 | + auth_mode: config?.ALLOW_AUTHLESS ? 'Authless (SSE endpoints bypass auth)' : 'Bearer token required', |
| 404 | + transport: 'SSE', |
| 405 | + note: 'For local testing with Claude.ai, use ngrok or similar to expose localhost', |
| 406 | + }, |
| 407 | + sse_headers: { |
| 408 | + 'Content-Type': 'text/event-stream', |
| 409 | + 'Cache-Control': 'no-cache', |
| 410 | + Connection: 'keep-alive', |
| 411 | + }, |
| 412 | + }); |
372 | 413 | }); |
373 | 414 |
|
374 | 415 | // SSE endpoint - establishes the server-to-client event stream |
@@ -453,7 +494,7 @@ async function main() { |
453 | 494 | }, |
454 | 495 | serverInfo: { |
455 | 496 | name: 'strava-mcp-server', |
456 | | - version: '2.0.0', |
| 497 | + version: '3.0.0', |
457 | 498 | }, |
458 | 499 | }; |
459 | 500 | break; |
@@ -674,7 +715,10 @@ async function main() { |
674 | 715 | app.listen(port, () => { |
675 | 716 | console.error(`[StravaServer] Remote MCP server running on http://localhost:${port}`); |
676 | 717 | console.error(`[StravaServer] MCP endpoint: http://localhost:${port}/mcp`); |
| 718 | + console.error(`[StravaServer] SSE endpoint: http://localhost:${port}/sse`); |
677 | 719 | console.error(`[StravaServer] Health check: http://localhost:${port}/health`); |
| 720 | + console.error(`[StravaServer] Debug info: http://localhost:${port}/debug`); |
| 721 | + console.error(`[StravaServer] Authless mode: ${config.ALLOW_AUTHLESS ? 'ENABLED' : 'DISABLED'}`); |
678 | 722 | }); |
679 | 723 | } |
680 | 724 |
|
|
0 commit comments