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}¤t=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);
});