Skip to main content
Glama

@arizeai/phoenix-mcp

Official
by Arize-ai
client.ts7.89 kB
import { Client } from "pg"; import type { User } from "../types/index.js"; export class DatabaseClient { private static readonly DEFAULT_USER: User = { id: "default-test-user", email: "testuser@arize.com", name: "Test User", role: "admin", groups: ["phoenix-admins", "full-access"], }; private client: Client; private connected = false; private users: User[] = []; constructor() { this.client = new Client({ host: process.env.DB_HOST || "db", port: parseInt(process.env.DB_PORT || "5432"), database: process.env.DB_NAME || "postgres", user: process.env.DB_USER || "postgres", password: process.env.DB_PASSWORD || "postgres", }); this.users = [DatabaseClient.DEFAULT_USER]; } async connect(): Promise<void> { try { await this.client.connect(); this.connected = true; const dbConnected = { timestamp: new Date().toISOString(), event: "database_connected", host: process.env.DB_HOST || "db", port: parseInt(process.env.DB_PORT || "5432"), database: process.env.DB_NAME || "postgres", }; console.log(JSON.stringify(dbConnected)); await this.fetchUsers(); this.startPolling(); } catch (error) { const dbConnectionFailed = { timestamp: new Date().toISOString(), event: "database_connection_failed", error: error instanceof Error ? error.message : String(error), host: process.env.DB_HOST || "db", port: parseInt(process.env.DB_PORT || "5432"), }; console.log(JSON.stringify(dbConnectionFailed)); this.fallbackToNoUsers(); } } private async fetchUsers(): Promise<void> { if (!this.connected) { const dbNotConnected = { timestamp: new Date().toISOString(), event: "database_fetch_skipped", reason: "not_connected", }; console.log(JSON.stringify(dbNotConnected)); return; } try { const startTime = Date.now(); const result = await this.client.query( `SELECT u.id, u.email, u.username, u.created_at, u.updated_at, ur.name as role_name FROM users u LEFT JOIN user_roles ur ON u.user_role_id = ur.id WHERE u.auth_method = 'OAUTH2' ORDER BY u.created_at` ); const queryTime = Date.now() - startTime; const previousCount = this.users.length; const previousEmails = this.users.map((u) => u.email).sort(); if (result.rows.length === 0) { this.users = [DatabaseClient.DEFAULT_USER]; } else { this.users = result.rows.map((row) => ({ id: row.id.toString(), email: row.email, name: row.username || row.email.split("@")[0], role: this.getRoleForUser(row), groups: this.getGroupsForUser(row), })); } const currentEmails = this.users.map((u) => u.email).sort(); const hasChanges = previousCount !== this.users.length || JSON.stringify(previousEmails) !== JSON.stringify(currentEmails); if (hasChanges) { const added = currentEmails.filter( (email) => !previousEmails.includes(email) ); const removed = previousEmails.filter( (email) => !currentEmails.includes(email) ); const userChangeDetected = { timestamp: new Date().toISOString(), event: "user_change_detected", query_time_ms: queryTime, user_count: { previous: previousCount, current: this.users.length, }, changes: { added: added, removed: removed, }, current_users: this.users.length > 0 ? this.users.map((user, i) => { const dbUser = result.rows.find( (row) => row.id.toString() === user.id ); return { index: i + 1, id: user.id, name: user.name, email: user.email, role_name: dbUser?.role_name, oauth_role: user.role, groups: user.groups, groups_count: user.groups.length, created_date: dbUser?.created_at ?.toISOString() .split("T")[0], }; }) : [], }; console.log(JSON.stringify(userChangeDetected)); } else { if (Date.now() % (120 * 5000) < 5000) { const userPollHealthCheck = { timestamp: new Date().toISOString(), event: "user_poll_health_check", user_count: this.users.length, query_time_ms: queryTime, poll_interval_note: "logged_every_10_minutes", }; console.log(JSON.stringify(userPollHealthCheck)); } } } catch (error) { const dbQueryFailed = { timestamp: new Date().toISOString(), event: "database_query_failed", error: error instanceof Error ? error.message : String(error), connection_status: this.connected, current_user_count: this.users.length, action: "keeping_existing_users", }; console.log(JSON.stringify(dbQueryFailed)); } } private startPolling(): void { setInterval(async () => { await this.fetchUsers(); }, 5000); const pollingStarted = { timestamp: new Date().toISOString(), event: "user_polling_started", interval_seconds: 5, mode: "debug", next_poll_time: new Date(Date.now() + 5000).toISOString(), }; console.log(JSON.stringify(pollingStarted)); } private fallbackToNoUsers(): void { this.users = [DatabaseClient.DEFAULT_USER]; const fallbackMode = { timestamp: new Date().toISOString(), event: "fallback_to_default_user", reason: "database_unavailable", user_count: 1, default_user: DatabaseClient.DEFAULT_USER.email, action: "starting_reconnection_polling", }; console.log(JSON.stringify(fallbackMode)); setInterval(async () => { if (!this.connected) { try { await this.client.connect(); this.connected = true; const dbReconnected = { timestamp: new Date().toISOString(), event: "database_reconnected", action: "resuming_user_fetch", }; console.log(JSON.stringify(dbReconnected)); await this.fetchUsers(); } catch (error) {} } else { await this.fetchUsers(); } }, 5000); } private getGroupsForUser(row: any): string[] { const roleName = row.role_name?.toUpperCase(); const roleGroups: { [roleName: string]: string[] } = { ADMIN: ["phoenix-admins", "full-access"], MEMBER: ["phoenix-members", "write-access"], VIEWER: ["phoenix-viewers", "read-access"], }; const defaultGroups = ["phoenix-users", "authenticated"]; const roleSpecificGroups = roleGroups[roleName] || ["phoenix-users"]; return [...defaultGroups, ...roleSpecificGroups]; } private getRoleForUser(row: any): string { const roleName = row.role_name?.toUpperCase(); const roleMapping: { [roleName: string]: string } = { ADMIN: "admin", MEMBER: "editor", VIEWER: "viewer", }; return roleMapping[roleName] || "viewer"; } getUsers(): User[] { return this.users; } async close(): Promise<void> { if (this.connected) { await this.client.end(); this.connected = false; const dbClosed = { timestamp: new Date().toISOString(), event: "database_connection_closed", reason: "graceful_shutdown", }; console.log(JSON.stringify(dbClosed)); } } }

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/Arize-ai/phoenix'

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