Skip to main content
Glama
config.ts7.07 kB
/** * Navidrome MCP Server - Configuration Management * Copyright (C) 2025 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ import { z } from 'zod'; import { config as loadDotenv } from 'dotenv'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { ErrorFormatter } from './utils/error-formatter.js'; import { logger } from './utils/logger.js'; // Safely load dotenv - it's optional since environment variables // can be provided directly (e.g., by Claude MCP configuration) try { // Try to load from the project root directory const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const projectRoot = join(__dirname, '..'); loadDotenv({ path: join(projectRoot, '.env'), // Don't override existing environment variables override: false }); } catch (error) { // Silently ignore dotenv errors - environment variables may be // provided by the MCP host (Claude) directly if (process.env['DEBUG'] === 'true') { logger.warn('Could not load .env file (this is normal when running as MCP server):', error); } } const ConfigSchema = z.object({ navidromeUrl: z.string().url('NAVIDROME_URL must be a valid URL'), navidromeUsername: z.string().min(1, 'NAVIDROME_USERNAME is required'), navidromePassword: z.string().min(1, 'NAVIDROME_PASSWORD is required'), debug: z.boolean().default(false), cacheTtl: z.number().positive().default(300), tokenExpiry: z.number().positive().default(86400), // Default 24 hours in seconds // Library Configuration defaultLibraryIds: z.array(z.number()).optional(), // Feature Configuration features: z.object({ lastfm: z.boolean().default(false), radioBrowser: z.boolean().default(false), lyrics: z.boolean().default(false), }), // API Keys and External Service Configuration lastFmApiKey: z.string().optional(), radioBrowserUserAgent: z.string().optional(), radioBrowserBase: z.string().url().default('https://de1.api.radio-browser.info'), // Lyrics Configuration lyricsProvider: z.string().optional(), lrclibUserAgent: z.string().optional(), lrclibBase: z.string().url().default('https://lrclib.net'), }); export type Config = z.infer<typeof ConfigSchema>; export async function loadConfig(): Promise<Config> { // Try to safely load .env file only in development mode // MCP servers get their environment from the host application if (process.env['NAVIDROME_URL'] === null || process.env['NAVIDROME_URL'] === undefined || process.env['NAVIDROME_URL'] === '') { // Only attempt to load .env if we're missing required environment variables // This suggests we're in development mode try { // Use import.meta.url to get absolute path, avoiding process.cwd() entirely const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const projectRoot = join(__dirname, '..'); const envPath = join(projectRoot, '.env'); // Check if we can actually access the file system // This will fail gracefully if we can't loadDotenv({ path: envPath, // Don't override existing environment variables override: false }); } catch { // Silently ignore dotenv errors - environment variables should be // provided by the MCP host (Claude Desktop) directly if (process.env['DEBUG'] === 'true') { logger.warn('Could not load .env file (this is expected when running as MCP server)'); } } } // Centralized environment variable access const lastFmApiKey = process.env['LASTFM_API_KEY'] ?? undefined; const radioBrowserUserAgent = process.env['RADIO_BROWSER_USER_AGENT'] ?? undefined; const lyricsProvider = process.env['LYRICS_PROVIDER'] ?? undefined; const lrclibUserAgent = process.env['LRCLIB_USER_AGENT'] ?? undefined; // Parse default library IDs from environment let defaultLibraryIds: number[] | undefined; const defaultLibrariesEnv = process.env['NAVIDROME_DEFAULT_LIBRARIES']; if (defaultLibrariesEnv !== null && defaultLibrariesEnv !== undefined && defaultLibrariesEnv.trim() !== '') { try { defaultLibraryIds = defaultLibrariesEnv .split(',') .map(id => parseInt(id.trim(), 10)) .filter(id => !isNaN(id)); if (defaultLibraryIds.length === 0) { logger.warn('NAVIDROME_DEFAULT_LIBRARIES contains no valid library IDs'); defaultLibraryIds = undefined; } } catch (error) { logger.warn('Failed to parse NAVIDROME_DEFAULT_LIBRARIES:', error); defaultLibraryIds = undefined; } } const rawConfig = { navidromeUrl: process.env['NAVIDROME_URL'], navidromeUsername: process.env['NAVIDROME_USERNAME'], navidromePassword: process.env['NAVIDROME_PASSWORD'], debug: process.env['DEBUG'] === 'true', cacheTtl: (process.env['CACHE_TTL'] !== null && process.env['CACHE_TTL'] !== undefined && process.env['CACHE_TTL'] !== '') ? parseInt(process.env['CACHE_TTL'], 10) : 300, tokenExpiry: (process.env['TOKEN_EXPIRY'] !== null && process.env['TOKEN_EXPIRY'] !== undefined && process.env['TOKEN_EXPIRY'] !== '') ? parseInt(process.env['TOKEN_EXPIRY'], 10) : 86400, // Library Configuration defaultLibraryIds, // Feature detection based on available configuration features: { lastfm: (lastFmApiKey !== null && lastFmApiKey !== undefined && lastFmApiKey.trim() !== ''), radioBrowser: (radioBrowserUserAgent !== null && radioBrowserUserAgent !== undefined && radioBrowserUserAgent.trim() !== ''), lyrics: (lyricsProvider !== null && lyricsProvider !== undefined && lyricsProvider.trim() !== '') && (lrclibUserAgent !== null && lrclibUserAgent !== undefined && lrclibUserAgent.trim() !== ''), }, // API Keys and External Service Configuration lastFmApiKey, radioBrowserUserAgent, radioBrowserBase: process.env['RADIO_BROWSER_BASE'] ?? 'https://de1.api.radio-browser.info', // Lyrics Configuration lyricsProvider, lrclibUserAgent, lrclibBase: process.env['LRCLIB_BASE'] ?? 'https://lrclib.net', }; try { return ConfigSchema.parse(rawConfig); } catch (error) { if (error instanceof z.ZodError) { const messages = error.issues.map((e) => `${e.path.join('.')}: ${e.message}`); throw new Error(ErrorFormatter.configValidation(messages)); } throw error; } }

Latest Blog Posts

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/Blakeem/Navidrome-MCP'

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