Skip to main content
Glama

Weather MCP Server with GitHub OAuth & Location Management

by f
server.js•36.2 kB
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; import express from 'express'; import cors from 'cors'; import { randomUUID } from 'node:crypto'; import { z } from 'zod'; import { authenticateRequest, handleGitHubCallback, getGitHubAuthUrl } from './auth.js'; import { addLocation, getUserLocations, getLocationByLabel, deleteLocation, deleteAllLocations, } from './database.js'; import crypto from 'node:crypto'; import dotenv from 'dotenv'; dotenv.config(); // Helper function to convert WMO weather codes to descriptions function getWeatherDescription(code) { const descriptions = { 0: 'Clear sky', 1: 'Mainly clear', 2: 'Partly cloudy', 3: 'Overcast', 45: 'Foggy', 48: 'Depositing rime fog', 51: 'Light drizzle', 53: 'Moderate drizzle', 55: 'Dense drizzle', 56: 'Light freezing drizzle', 57: 'Dense freezing drizzle', 61: 'Slight rain', 63: 'Moderate rain', 65: 'Heavy rain', 66: 'Light freezing rain', 67: 'Heavy freezing rain', 71: 'Slight snow fall', 73: 'Moderate snow fall', 75: 'Heavy snow fall', 77: 'Snow grains', 80: 'Slight rain showers', 81: 'Moderate rain showers', 82: 'Violent rain showers', 85: 'Slight snow showers', 86: 'Heavy snow showers', 95: 'Thunderstorm', 96: 'Thunderstorm with slight hail', 99: 'Thunderstorm with heavy hail', }; return descriptions[code] || 'Unknown'; } // Open-Meteo API implementation (free, no API key required) async function getCurrentWeather(location, unit = 'fahrenheit', userId = null) { try { // If userId is provided, check if location is a label first let resolvedLocation = location; if (userId) { const savedLocation = getLocationByLabel(userId, location); if (savedLocation) { resolvedLocation = savedLocation.location_name; console.log(`[WEATHER] Resolved label "${location}" to "${resolvedLocation}"`); } } // Step 1: Geocode the location using Open-Meteo Geocoding API const geoUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(resolvedLocation)}&count=1&language=en&format=json`; const geoResponse = await fetch(geoUrl); if (!geoResponse.ok) { return JSON.stringify({ error: `Geocoding failed (${geoResponse.status})` }); } const geoData = await geoResponse.json(); if (!geoData.results || geoData.results.length === 0) { return JSON.stringify({ error: `Location "${resolvedLocation}" not found` }); } const { latitude, longitude, name, country } = geoData.results[0]; // Step 2: Get weather data from Open-Meteo Weather API const temperatureUnit = unit === 'celsius' ? 'celsius' : 'fahrenheit'; const windSpeedUnit = unit === 'celsius' ? 'kmh' : 'mph'; const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m,pressure_msl,cloud_cover&temperature_unit=${temperatureUnit}&wind_speed_unit=${windSpeedUnit}`; const weatherResponse = await fetch(weatherUrl); if (!weatherResponse.ok) { return JSON.stringify({ error: `Weather API failed (${weatherResponse.status})` }); } const weatherData = await weatherResponse.json(); const current = weatherData.current; return JSON.stringify({ location: `${name}, ${country}`, temperature: Math.round(current.temperature_2m), unit: unit, condition: getWeatherDescription(current.weather_code), humidity: current.relative_humidity_2m, wind_speed: Math.round(current.wind_speed_10m * 10) / 10, pressure: current.pressure_msl, cloud_cover: current.cloud_cover, }); } catch (error) { return JSON.stringify({ error: `Failed to fetch weather data: ${error.message}` }); } } // Start the MCP server with Streamable HTTP transport async function main() { const app = express(); const PORT = process.env.PORT || 3000; const MCP_ENDPOINT = '/mcp'; // Enable CORS for localhost and expose session header app.use(cors({ origin: (origin, callback) => { // Allow requests with no origin (like Postman) or localhost origins if (!origin || origin.match(/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/)) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } }, credentials: true, exposedHeaders: ['Mcp-Session-Id'], allowedHeaders: ['Content-Type', 'Mcp-Session-Id', 'Authorization'], })); app.use(express.json()); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', server: 'weather-mcp-server' }); }); // OAuth 2.0 Authorization Server Metadata (RFC 8414) // This describes the OAuth capabilities of this server // We're proxying to GitHub OAuth through our own endpoints app.get('/.well-known/oauth-authorization-server', (req, res) => { const baseUrl = `http://127.0.0.1:${PORT}`; res.json({ // Required fields per RFC 8414 issuer: baseUrl, authorization_endpoint: `${baseUrl}/oauth/authorize`, token_endpoint: `${baseUrl}/oauth/token`, response_types_supported: ['code'], // Optional but recommended fields grant_types_supported: ['authorization_code'], token_endpoint_auth_methods_supported: ['none'], // Public client (no client secret needed) scopes_supported: ['read:user', 'user:email'], service_documentation: `${baseUrl}/docs`, // PKCE support (RFC 7636) - required for public clients code_challenge_methods_supported: ['S256', 'plain'], // Token revocation (RFC 7009) revocation_endpoint: `${baseUrl}/oauth/revoke`, // Dynamic Client Registration (RFC 7591) registration_endpoint: `${baseUrl}/oauth/register`, }); }); // OAuth 2.0 Protected Resource Metadata (RFC 9728) // This is specifically for MCP servers acting as resource servers app.get('/.well-known/oauth-protected-resource', (req, res) => { const baseUrl = `http://127.0.0.1:${PORT}`; res.json({ resource: baseUrl, authorization_servers: [baseUrl], bearer_methods_supported: ['header'], resource_signing_alg_values_supported: [], resource_documentation: `${baseUrl}/docs`, scopes_supported: ['read:user', 'user:email'], }); }); // OAuth state storage (in production, use Redis or similar) const oauthStates = new Map(); // Registered OAuth clients storage (in production, use database) const registeredClients = new Map(); // PKCE challenge storage (maps authorization code to code_challenge) const pkceChallenges = new Map(); // OAuth Dynamic Client Registration endpoint (RFC 7591) app.post('/oauth/register', express.json(), (req, res) => { try { const { redirect_uris, client_name, client_uri, logo_uri, scope } = req.body; // Validate redirect_uris (required per RFC 7591) if (!redirect_uris || !Array.isArray(redirect_uris) || redirect_uris.length === 0) { return res.status(400).json({ error: 'invalid_redirect_uri', error_description: 'redirect_uris is required and must be a non-empty array', }); } // Generate client_id (we don't need client_secret for public clients) const client_id = `mcp_${randomUUID()}`; const client_id_issued_at = Math.floor(Date.now() / 1000); // Store the registered client const clientInfo = { client_id, client_id_issued_at, redirect_uris, client_name: client_name || 'MCP Client', client_uri, logo_uri, scope: scope || 'read:user user:email', grant_types: ['authorization_code'], response_types: ['code'], token_endpoint_auth_method: 'none', // Public client }; registeredClients.set(client_id, clientInfo); console.log('[OAUTH] Client registered:', client_id, client_name || 'MCP Client'); // Return client credentials per RFC 7591 res.status(201).json(clientInfo); } catch (error) { console.error('[OAUTH] Registration error:', error); res.status(500).json({ error: 'server_error', error_description: error.message, }); } }); // OAuth authorization endpoint - proxies to GitHub OAuth // This is the standard OAuth 2.0 authorization endpoint that MCP clients will use app.get('/oauth/authorize', (req, res) => { try { const { response_type, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method } = req.query; // Validate parameters if (response_type !== 'code') { return res.status(400).json({ error: 'unsupported_response_type', error_description: 'Only "code" response type is supported', }); } // Validate client_id and redirect_uri if client is registered // Since we're proxying to GitHub, we accept unregistered clients too if (client_id && registeredClients.has(client_id)) { const client = registeredClients.get(client_id); // Validate redirect_uri matches registered client if (redirect_uri && !client.redirect_uris.includes(redirect_uri)) { return res.status(400).json({ error: 'invalid_redirect_uri', error_description: 'redirect_uri does not match registered URIs', }); } console.log('[OAUTH] Using registered client:', client_id); } else if (client_id) { console.log('[OAUTH] Unregistered client_id, allowing for GitHub proxy:', client_id); } // Store the client's state and redirect_uri for callback const internalState = crypto.randomBytes(32).toString('hex'); oauthStates.set(internalState, { created: Date.now(), expires: Date.now() + 10 * 60 * 1000, // 10 minutes clientState: state, clientRedirectUri: redirect_uri, clientId: client_id, codeChallenge: code_challenge, codeChallengeMethod: code_challenge_method || 'plain', }); // Clean up expired states for (const [key, value] of oauthStates.entries()) { if (value.expires < Date.now()) { oauthStates.delete(key); } } // Build GitHub authorization URL with our server's client ID const githubAuthUrl = getGitHubAuthUrl(internalState); console.log('[OAUTH] Authorization request from client, redirecting to GitHub'); console.log(`[OAUTH] Client redirect_uri: ${redirect_uri}`); res.redirect(githubAuthUrl); } catch (error) { console.error('[OAUTH] Authorization error:', error); res.status(500).json({ error: 'server_error', error_description: error.message, }); } }); // OAuth token revocation endpoint (RFC 7009) app.post('/oauth/revoke', express.urlencoded({ extended: true }), express.json(), async (req, res) => { try { const { token, token_type_hint } = req.body; if (!token) { return res.status(400).json({ error: 'invalid_request', error_description: 'Missing token parameter', }); } console.log('[OAUTH] Token revocation request'); // In our implementation, we don't actually store tokens server-side // (they're GitHub tokens), so we just acknowledge the revocation // In a production system, you'd: // 1. Invalidate the token in your database // 2. Optionally revoke it with GitHub // For now, we'll just return success res.status(200).send(''); // RFC 7009 specifies empty response on success console.log('[OAUTH] Token revocation successful'); } catch (error) { console.error('[OAUTH] Revocation error:', error); res.status(500).json({ error: 'server_error', error_description: error.message, }); } }); // OAuth token endpoint - exchanges authorization code for access token // Note: OAuth 2.0 token requests use application/x-www-form-urlencoded app.post('/oauth/token', express.urlencoded({ extended: true }), express.json(), async (req, res) => { try { const { grant_type, code, redirect_uri, client_id, code_verifier } = req.body; console.log('[OAUTH] Token request received:', { grant_type, client_id, has_code: !!code, has_verifier: !!code_verifier }); if (grant_type !== 'authorization_code') { return res.status(400).json({ error: 'unsupported_grant_type', error_description: 'Only "authorization_code" grant type is supported', }); } if (!code) { return res.status(400).json({ error: 'invalid_request', error_description: 'Missing authorization code', }); } console.log('[OAUTH] Token exchange request'); // Verify PKCE if code_challenge was provided const pkceData = pkceChallenges.get(code); if (pkceData) { // Check expiration if (pkceData.expires < Date.now()) { pkceChallenges.delete(code); return res.status(400).json({ error: 'invalid_grant', error_description: 'Authorization code expired', }); } // Verify code_verifier if (!code_verifier) { return res.status(400).json({ error: 'invalid_request', error_description: 'code_verifier is required for PKCE', }); } // Compute challenge from verifier let computedChallenge; if (pkceData.codeChallengeMethod === 'S256') { // SHA-256 hash of code_verifier, base64url encoded const hash = crypto.createHash('sha256').update(code_verifier).digest(); computedChallenge = hash.toString('base64url'); } else { // plain method computedChallenge = code_verifier; } // Verify challenge matches if (computedChallenge !== pkceData.codeChallenge) { pkceChallenges.delete(code); return res.status(400).json({ error: 'invalid_grant', error_description: 'Code verifier does not match code challenge', }); } // PKCE verification successful, clean up pkceChallenges.delete(code); console.log('[OAUTH] PKCE verification successful'); } // Exchange code with GitHub (using our server's credentials) const result = await handleGitHubCallback(code); // Return access token to client res.json({ access_token: result.access_token, token_type: 'Bearer', scope: 'read:user user:email', }); console.log('[OAUTH] Token issued successfully'); } catch (error) { console.error('[OAUTH] Token exchange error:', error); res.status(500).json({ error: 'server_error', error_description: error.message, }); } }); // OAuth login endpoint - initiates GitHub OAuth flow (for manual/browser flow) app.get('/oauth/login', (req, res) => { try { // Generate random state for CSRF protection const state = crypto.randomBytes(32).toString('hex'); // Store state with expiration (5 minutes) oauthStates.set(state, { created: Date.now(), expires: Date.now() + 5 * 60 * 1000, }); // Clean up expired states for (const [key, value] of oauthStates.entries()) { if (value.expires < Date.now()) { oauthStates.delete(key); } } const authUrl = getGitHubAuthUrl(state); console.log('[OAUTH] Login initiated, redirecting to GitHub'); res.redirect(authUrl); } catch (error) { console.error('[OAUTH] Login error:', error); res.status(500).json({ error: 'OAuth initialization failed', message: error.message, }); } }); // OAuth callback endpoint - handles GitHub OAuth callback app.get('/oauth/callback', async (req, res) => { try { const { code, state } = req.query; if (!code) { return res.status(400).json({ error: 'Missing authorization code', message: 'No authorization code provided by GitHub', }); } // Verify state to prevent CSRF attacks if (!state || !oauthStates.has(state)) { return res.status(400).json({ error: 'Invalid state parameter', message: 'State verification failed. Please try again.', }); } const stateData = oauthStates.get(state); if (stateData.expires < Date.now()) { oauthStates.delete(state); return res.status(400).json({ error: 'Expired state', message: 'OAuth flow expired. Please try again.', }); } // Delete used state oauthStates.delete(state); console.log('[OAUTH] Processing callback with code'); // Check if this is an automatic OAuth flow (from /oauth/authorize) if (stateData.clientRedirectUri) { // This is from Claude Desktop or another OAuth client // Store PKCE challenge with the authorization code for later verification if (stateData.codeChallenge) { pkceChallenges.set(code, { codeChallenge: stateData.codeChallenge, codeChallengeMethod: stateData.codeChallengeMethod, expires: Date.now() + 10 * 60 * 1000, // 10 minutes }); } // Redirect back to the client with the authorization code const redirectUrl = new URL(stateData.clientRedirectUri); redirectUrl.searchParams.set('code', code); if (stateData.clientState) { redirectUrl.searchParams.set('state', stateData.clientState); } console.log('[OAUTH] Redirecting back to client:', redirectUrl.toString()); return res.redirect(redirectUrl.toString()); } // This is a manual browser flow (from /oauth/login) // Exchange code for access token and create/update user const result = await handleGitHubCallback(code); // Return HTML page with access token res.send(` <!DOCTYPE html> <html> <head> <title>OAuth Success</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } .container { background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); max-width: 600px; width: 90%; } h1 { color: #333; margin-top: 0; } .success { color: #10b981; font-size: 3rem; margin: 0; } .user-info { margin: 1.5rem 0; padding: 1rem; background: #f3f4f6; border-radius: 8px; } .token-box { background: #1f2937; color: #10b981; padding: 1rem; border-radius: 8px; font-family: 'Courier New', monospace; word-break: break-all; margin: 1rem 0; position: relative; } .copy-btn { position: absolute; top: 0.5rem; right: 0.5rem; background: #10b981; color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.875rem; } .copy-btn:hover { background: #059669; } .info { color: #6b7280; font-size: 0.875rem; margin-top: 1rem; } </style> </head> <body> <div class="container"> <div class="success">āœ…</div> <h1>Authentication Successful!</h1> <div class="user-info"> <strong>Welcome, ${result.user.name}!</strong><br> Username: @${result.user.username}<br> Email: ${result.user.email || 'Not provided'} </div> <p><strong>Your Access Token:</strong></p> <div class="token-box"> <button class="copy-btn" onclick="copyToken()">Copy</button> <code id="token">${result.access_token}</code> </div> <p class="info"> šŸ’” Use this token in the Authorization header as: <code>Bearer ${result.access_token}</code> </p> <p class="info"> You can now close this window and use the token to make authenticated requests to the MCP server. </p> </div> <script> function copyToken() { const token = document.getElementById('token').textContent; navigator.clipboard.writeText(token).then(() => { const btn = document.querySelector('.copy-btn'); btn.textContent = 'Copied!'; setTimeout(() => { btn.textContent = 'Copy'; }, 2000); }); } </script> </body> </html> `); console.log('[OAUTH] User authenticated successfully:', result.user.username); } catch (error) { console.error('[OAUTH] Callback error:', error); res.status(500).send(` <!DOCTYPE html> <html> <head> <title>OAuth Error</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #fee; } .container { background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); max-width: 500px; } h1 { color: #dc2626; } .error { color: #dc2626; font-size: 3rem; margin: 0; } </style> </head> <body> <div class="container"> <div class="error">āŒ</div> <h1>Authentication Failed</h1> <p>${error.message}</p> <p><a href="/oauth/login">Try again</a></p> </div> </body> </html> `); } }); // Get current user info endpoint (protected) app.get('/oauth/me', authenticateRequest, (req, res) => { res.json({ success: true, user: req.user, }); }); // Map to store transports by session ID const transports = {}; // Map to store user info by session ID const sessionUsers = {}; // Handle POST requests for client-to-server communication app.post(MCP_ENDPOINT, authenticateRequest, async (req, res) => { const sessionId = req.headers['mcp-session-id']; let transport; console.log(`[POST ${MCP_ENDPOINT}] Request received`, { sessionId: sessionId || 'none', method: req.body?.method || 'unknown', hasSession: !!(sessionId && transports[sessionId]), }); if (sessionId && transports[sessionId]) { // Reuse existing transport console.log(`[SESSION] Reusing existing session: ${sessionId}`); transport = transports[sessionId]; } else if (!sessionId && isInitializeRequest(req.body)) { console.log('[INIT] New client initialization request'); // New initialization request - create new transport and server transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (sid) => { console.log(`[SESSION] New session created: ${sid} for user: ${req.user.name}`); console.log(`[SESSION] Active sessions: ${Object.keys(transports).length + 1}`); transports[sid] = transport; sessionUsers[sid] = req.user; }, enableDnsRebindingProtection: true, allowedHosts: ['127.0.0.1', 'localhost', `127.0.0.1:${PORT}`, `localhost:${PORT}`], }); // Clean up transport when closed transport.onclose = () => { if (transport.sessionId) { console.log(`[SESSION] Session closed: ${transport.sessionId}`); console.log(`[SESSION] Active sessions: ${Object.keys(transports).length - 1}`); delete transports[transport.sessionId]; delete sessionUsers[transport.sessionId]; } }; // Create a new server instance for this session const sessionServer = new McpServer({ name: 'weather-server', version: '1.0.0', }); // Register tool using the cleaner .tool() API sessionServer.tool( 'get_current_weather', 'Get the current weather in a location. You can use a saved location label (e.g., "home", "office") or a location name (e.g., "New York", "London").', { location: z.string().describe('The location to get the weather for (can be a saved label like "home" or a location name)'), unit: z.enum(['celsius', 'fahrenheit']).optional().default('celsius').describe('The unit of temperature to use'), }, async ({ location, unit = 'celsius' }) => { console.log(`[TOOL] get_current_weather called:`, { location, unit }); const userId = sessionUsers[transport.sessionId]?.id; const weatherData = await getCurrentWeather(location, unit, userId); const data = JSON.parse(weatherData); if (data.error) { return { content: [ { type: 'text', text: `Error: ${data.error}`, }, ], isError: true, }; } const weatherText = `Current weather in ${data.location}: - Temperature: ${data.temperature}°${data.unit === 'fahrenheit' ? 'F' : 'C'} - Condition: ${data.condition} - Humidity: ${data.humidity}% - Wind Speed: ${data.wind_speed} ${data.unit === 'fahrenheit' ? 'mph' : 'km/h'} - Pressure: ${data.pressure} hPa - Cloud Cover: ${data.cloud_cover}%`; return { content: [ { type: 'text', text: weatherText, }, ], }; } ); // Tool: Add or update a location sessionServer.tool( 'add_location', 'Save a location with a custom label (e.g., "home", "office", "gym"). You can use this label later when checking weather.', { label: z.string().describe('A custom label for the location (e.g., "home", "office", "gym")'), location: z.string().describe('The actual location name (e.g., "New York", "London", "123 Main St")'), }, async ({ label, location }) => { console.log(`[TOOL] add_location called:`, { label, location }); const userId = sessionUsers[transport.sessionId]?.id; if (!userId) { return { content: [ { type: 'text', text: 'Error: User not authenticated', }, ], isError: true, }; } const result = addLocation(userId, label, location); if (result.success) { return { content: [ { type: 'text', text: `āœ… Location saved: "${label}" → "${location}"`, }, ], }; } else { return { content: [ { type: 'text', text: `Error: ${result.error}`, }, ], isError: true, }; } } ); // Tool: List all saved locations sessionServer.tool( 'list_locations', 'List all your saved locations with their labels', {}, async () => { console.log(`[TOOL] list_locations called`); const userId = sessionUsers[transport.sessionId]?.id; if (!userId) { return { content: [ { type: 'text', text: 'Error: User not authenticated', }, ], isError: true, }; } const locations = getUserLocations(userId); if (locations.length === 0) { return { content: [ { type: 'text', text: 'You have no saved locations. Use add_location to save one!', }, ], }; } const locationList = locations .map((loc, idx) => `${idx + 1}. "${loc.label}" → ${loc.location_name}`) .join('\n'); return { content: [ { type: 'text', text: `šŸ“ Your saved locations:\n${locationList}`, }, ], }; } ); // Tool: Delete a saved location sessionServer.tool( 'delete_location', 'Delete a saved location by its label', { label: z.string().describe('The label of the location to delete'), }, async ({ label }) => { console.log(`[TOOL] delete_location called:`, { label }); const userId = sessionUsers[transport.sessionId]?.id; if (!userId) { return { content: [ { type: 'text', text: 'Error: User not authenticated', }, ], isError: true, }; } const success = deleteLocation(userId, label); if (success) { return { content: [ { type: 'text', text: `āœ… Location "${label}" deleted successfully`, }, ], }; } else { return { content: [ { type: 'text', text: `Error: Location "${label}" not found`, }, ], isError: true, }; } } ); // Tool: Get current user info sessionServer.tool( 'get_user_info', 'Get information about the currently logged-in user (GitHub profile)', {}, async () => { console.log(`[TOOL] get_user_info called`); const user = sessionUsers[transport.sessionId]; if (!user) { return { content: [ { type: 'text', text: 'Error: User not authenticated', }, ], isError: true, }; } const userInfo = `šŸ‘¤ User Profile: - Name: ${user.name || 'N/A'} - GitHub Username: @${user.username || 'N/A'} - Email: ${user.email || 'Not provided'} - Avatar: ${user.avatar_url || 'N/A'} - User ID: ${user.id}`; return { content: [ { type: 'text', text: userInfo, }, ], }; } ); await sessionServer.connect(transport); } else { // Invalid request res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided', }, id: null, }); return; } // Handle the request await transport.handleRequest(req, res, req.body); }); // Handle GET requests for server-to-client notifications via SSE app.get(MCP_ENDPOINT, authenticateRequest, async (req, res) => { const sessionId = req.headers['mcp-session-id']; console.log(`[GET ${MCP_ENDPOINT}] SSE connection request`, { sessionId: sessionId || 'none', hasSession: !!(sessionId && transports[sessionId]), }); if (!sessionId || !transports[sessionId]) { console.log(`[GET ${MCP_ENDPOINT}] Rejected: Invalid or missing session ID`); res.status(400).send('Invalid or missing session ID'); return; } console.log(`[SSE] Opening SSE stream for session: ${sessionId}`); const transport = transports[sessionId]; await transport.handleRequest(req, res); }); // Handle DELETE requests for session termination app.delete(MCP_ENDPOINT, authenticateRequest, async (req, res) => { const sessionId = req.headers['mcp-session-id']; console.log(`[DELETE ${MCP_ENDPOINT}] Session termination request`, { sessionId: sessionId || 'none', }); if (!sessionId || !transports[sessionId]) { res.status(400).send('Invalid or missing session ID'); return; } const transport = transports[sessionId]; await transport.handleRequest(req, res); }); app.listen(PORT, '127.0.0.1', () => { console.log('='.repeat(60)); console.log('Weather MCP Server Started (OAuth Enabled)'); console.log('='.repeat(60)); console.log(`Server: http://127.0.0.1:${PORT}`); console.log(`MCP endpoint: http://127.0.0.1:${PORT}${MCP_ENDPOINT}`); console.log(`Health check: http://127.0.0.1:${PORT}/health`); console.log(''); console.log('OAuth Endpoints:'); console.log(` Metadata: http://127.0.0.1:${PORT}/.well-known/oauth-authorization-server`); console.log(` Protected Resource: http://127.0.0.1:${PORT}/.well-known/oauth-protected-resource`); console.log(` Registration: http://127.0.0.1:${PORT}/oauth/register`); console.log(` Authorize: http://127.0.0.1:${PORT}/oauth/authorize`); console.log(` Token: http://127.0.0.1:${PORT}/oauth/token`); console.log(` Revoke: http://127.0.0.1:${PORT}/oauth/revoke`); console.log(` Login (Manual): http://127.0.0.1:${PORT}/oauth/login`); console.log(` Callback: http://127.0.0.1:${PORT}/oauth/callback`); console.log(` User Info: http://127.0.0.1:${PORT}/oauth/me`); console.log('='.repeat(60)); console.log('šŸ’” OAuth Features:'); console.log(' āœ… Dynamic Client Registration (RFC 7591)'); console.log(' āœ… PKCE with S256 (RFC 7636)'); console.log(' āœ… Automatic OAuth Discovery'); console.log(' āœ… GitHub OAuth Proxy'); console.log(''); console.log('šŸ’” For Manual Authentication:'); console.log(` 1. Visit http://127.0.0.1:${PORT}/oauth/login`); console.log(' 2. Authorize with GitHub'); console.log(' 3. Copy your access token'); console.log(' 4. Use it in Authorization header: Bearer <token>'); console.log('='.repeat(60)); console.log('Waiting for connections...'); console.log(''); }); } main().catch((error) => { console.error('Fatal error in main():', error); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/f/komunite-mcp-bootcamp-weather-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server