Skip to main content
Glama

X MCP Server

by tomaitagaki
session-manager.tsโ€ข5.81 kB
import { createHash } from 'crypto'; import { v4 as uuidv4 } from 'uuid'; import { User, Session, AuthError } from './types.js'; import { XDatabase } from './database.js'; import { encryption } from './encryption.js'; export class SessionManager { private db: XDatabase; private defaultUserId?: number; // For local single-user mode constructor(db: XDatabase) { this.db = db; this.initializeDefaultUser(); } private initializeDefaultUser(): void { // In local mode, create or get a default "local_user" const DEFAULT_X_USER_ID = 'local_user'; let user = this.db.getUserByXUserId(DEFAULT_X_USER_ID); if (!user) { user = this.db.createUser(DEFAULT_X_USER_ID, 'local_user', 'Local User'); console.log(`Created default local user with ID: ${user.id}`); } this.defaultUserId = user.id; } createSession(userId: number, expiresAt?: number): { sessionId: string; sessionSecret: string } { const sessionId = uuidv4(); const sessionSecret = encryption.generateSecureToken(); const sessionSecretHash = this.hashSessionSecret(sessionSecret); this.db.createSession(userId, sessionId, sessionSecretHash, expiresAt); return { sessionId, sessionSecret }; } validateSession(sessionId: string, sessionSecret?: string): Session | null { const session = this.db.getSession(sessionId); if (!session) return null; // For stdio/local mode, we don't require session secret validation if (!sessionSecret) { return session; } // For hosted mode, validate the session secret if (!this.verifySessionSecret(sessionSecret, session.session_secret_hash)) { return null; } return session; } getUserFromSession(sessionId?: string, sessionSecret?: string): User | null { // If no session provided, use default local user if (!sessionId) { if (this.defaultUserId) { return this.db.getUserById(this.defaultUserId); } return null; } const session = this.validateSession(sessionId, sessionSecret); if (!session) return null; return this.db.getUserById(session.user_id); } createAuthError(code: AuthError['code'], message: string, options?: { loginUrl?: string; missingScopes?: string[]; }): AuthError { const error = new Error(message) as AuthError; error.code = code; error.login_url = options?.loginUrl; error.missing_scopes = options?.missingScopes; return error; } requireUser(sessionId?: string, sessionSecret?: string): User { const user = this.getUserFromSession(sessionId, sessionSecret); if (!user) { throw this.createAuthError('auth_invalid_session', 'Invalid or expired session'); } return user; } deleteSession(sessionId: string): void { this.db.deleteSession(sessionId); } cleanupExpiredSessions(): void { this.db.cleanupExpiredSessions(); } private hashSessionSecret(secret: string): string { const { hash } = encryption.hashPassword(secret); return hash; } private verifySessionSecret(secret: string, hash: string): boolean { try { // Extract salt from hash (assuming we stored hash:salt format) const [hashPart, saltPart] = hash.split(':'); if (!saltPart) { // Legacy format without explicit salt return encryption.hashPassword(secret).hash === hash; } return encryption.verifyPassword(secret, hashPart, saltPart); } catch (error) { return false; } } // Context extraction from different transport types extractSessionContext(request: any): { sessionId?: string; sessionSecret?: string } { // For stdio transport, we typically don't have session context // Return undefined to use default local user if (!request.meta) { return {}; } // For HTTP-based transports, check headers/cookies const headers = request.meta.headers || {}; // Check Authorization header for Bearer token const authHeader = headers.authorization || headers.Authorization; if (authHeader && authHeader.startsWith('Bearer ')) { const token = authHeader.substring(7); const [sessionId, sessionSecret] = token.split(':'); return { sessionId, sessionSecret }; } // Check cookie for session const cookie = headers.cookie; if (cookie) { const sessionMatch = cookie.match(/session=([^;]+)/); if (sessionMatch) { const [sessionId, sessionSecret] = sessionMatch[1].split(':'); return { sessionId, sessionSecret }; } } // Check custom headers const sessionId = headers['x-session-id']; const sessionSecret = headers['x-session-secret']; return { sessionId, sessionSecret }; } getDefaultUserId(): number | undefined { return this.defaultUserId; } // Helper method for creating local sessions (for CLI tools, etc.) createLocalSession(userId?: number): { sessionId: string; sessionSecret: string } { const targetUserId = userId || this.defaultUserId; if (!targetUserId) { throw new Error('No user ID available for session creation'); } return this.createSession(targetUserId, Date.now() + (365 * 24 * 60 * 60 * 1000)); // 1 year } // List all users (for admin/debugging purposes) getAllUsers(): User[] { return this.db.getAllUsers(); } // Get user statistics getUserStats(userId: number): { sessionCount: number; lastActivity?: number; hasTokens: boolean; } { const sessionCount = this.db.getUserSessionCount(userId); const lastActivity = this.db.getUserLastActivity(userId); const tokenCount = this.db.getUserTokenCount(userId); return { sessionCount, lastActivity: lastActivity || undefined, hasTokens: tokenCount > 0 }; } }

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/tomaitagaki/x-mcp'

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