Skip to main content
Glama
index.ts7.96 kB
import { config } from 'dotenv'; import { APIConfiguration } from '../types/todoist.js'; import { ValidationError } from '../types/errors.js'; // Load environment variables from .env file config(); /** * Configuration constants */ export const CONFIG_DEFAULTS = { TODOIST_API_BASE_URL: 'https://api.todoist.com/api/v1', REQUEST_TIMEOUT: 10000, // 10 seconds RETRY_ATTEMPTS: 3, RATE_LIMIT_BUFFER: 0.8, // Use 80% of rate limit capacity CACHE_TTL_PROJECTS: 30 * 60 * 1000, // 30 minutes CACHE_TTL_LABELS: 30 * 60 * 1000, // 30 minutes CACHE_TTL_SECTIONS: 15 * 60 * 1000, // 15 minutes LOG_LEVEL: 'info', MAX_BATCH_SIZE: 100, } as const; /** * Environment variable mapping */ interface EnvironmentConfig { TODOIST_API_TOKEN?: string; TODOIST_API_BASE_URL?: string; REQUEST_TIMEOUT?: string; RETRY_ATTEMPTS?: string; LOG_LEVEL?: string; MAX_BATCH_SIZE?: string; NODE_ENV?: string; } /** * Parse and validate environment variables * * NOTE: Token validation is now deferred until first API tool invocation. * This allows MCP platform inspection (e.g., Smithery) without requiring token. */ function normalizeApiToken(token: string | undefined | null): string | null { if (typeof token !== 'string') { return null; } const trimmed = token.trim(); return trimmed.length > 0 ? trimmed : null; } function parseEnvironmentConfig(): { apiToken: string | null; baseUrl: string; timeout: number; retryAttempts: number; logLevel: string; maxBatchSize: number; isProduction: boolean; isDevelopment: boolean; } { const env = process.env as EnvironmentConfig; // Token is now optional at startup (deferred validation) const apiToken = normalizeApiToken(env.TODOIST_API_TOKEN ?? null); // Skip token format validation (deferred to first tool call) // Parse and validate numeric values const timeout = env.REQUEST_TIMEOUT ? parseInt(env.REQUEST_TIMEOUT, 10) : CONFIG_DEFAULTS.REQUEST_TIMEOUT; if (isNaN(timeout) || timeout < 1000 || timeout > 60000) { throw new ValidationError( 'REQUEST_TIMEOUT must be between 1000 and 60000 milliseconds' ); } const retryAttempts = env.RETRY_ATTEMPTS ? parseInt(env.RETRY_ATTEMPTS, 10) : CONFIG_DEFAULTS.RETRY_ATTEMPTS; if (isNaN(retryAttempts) || retryAttempts < 0 || retryAttempts > 10) { throw new ValidationError('RETRY_ATTEMPTS must be between 0 and 10'); } const maxBatchSize = env.MAX_BATCH_SIZE ? parseInt(env.MAX_BATCH_SIZE, 10) : CONFIG_DEFAULTS.MAX_BATCH_SIZE; if (isNaN(maxBatchSize) || maxBatchSize < 1 || maxBatchSize > 100) { throw new ValidationError( 'MAX_BATCH_SIZE must be between 1 and 100 (Todoist API limit)' ); } // Validate URL const baseUrl = env.TODOIST_API_BASE_URL || CONFIG_DEFAULTS.TODOIST_API_BASE_URL; try { new URL(baseUrl); } catch { throw new ValidationError('TODOIST_API_BASE_URL must be a valid URL'); } // Validate log level const validLogLevels = ['error', 'warn', 'info', 'debug']; const logLevel = env.LOG_LEVEL || CONFIG_DEFAULTS.LOG_LEVEL; if (!validLogLevels.includes(logLevel)) { throw new ValidationError( `LOG_LEVEL must be one of: ${validLogLevels.join(', ')}` ); } // Environment detection const nodeEnv = env.NODE_ENV || 'development'; const isProduction = nodeEnv === 'production'; const isDevelopment = nodeEnv === 'development'; return { apiToken, baseUrl, timeout, retryAttempts, logLevel, maxBatchSize, isProduction, isDevelopment, }; } /** * Application configuration */ export interface AppConfig { api: APIConfiguration; cache: { ttlProjects: number; ttlLabels: number; ttlSections: number; }; logging: { level: string; enableCorrelationIds: boolean; sanitizePersonalData: boolean; }; performance: { maxBatchSize: number; rateLimitBuffer: number; }; environment: { isProduction: boolean; isDevelopment: boolean; nodeEnv: string; }; } let cachedConfig: AppConfig | null = null; /** * Get application configuration * Uses caching to avoid re-parsing environment variables on every call */ export function getConfig(): APIConfiguration { if (!cachedConfig) { try { const env = parseEnvironmentConfig(); cachedConfig = { api: { token: env.apiToken, // Now nullable base_url: env.baseUrl, timeout: env.timeout, retry_attempts: env.retryAttempts, }, cache: { ttlProjects: CONFIG_DEFAULTS.CACHE_TTL_PROJECTS, ttlLabels: CONFIG_DEFAULTS.CACHE_TTL_LABELS, ttlSections: CONFIG_DEFAULTS.CACHE_TTL_SECTIONS, }, logging: { level: env.logLevel, enableCorrelationIds: true, sanitizePersonalData: env.isProduction, // Only sanitize in production }, performance: { maxBatchSize: env.maxBatchSize, rateLimitBuffer: CONFIG_DEFAULTS.RATE_LIMIT_BUFFER, }, environment: { isProduction: env.isProduction, isDevelopment: env.isDevelopment, nodeEnv: process.env.NODE_ENV || 'development', }, }; } catch (error) { // Re-throw configuration errors with helpful context throw new ValidationError( `Configuration error: ${error instanceof Error ? error.message : 'Unknown error'}. ` + 'Please check your environment variables and .env file.' ); } } // Always reflect the latest token from environment to support deferred validation flows cachedConfig.api.token = normalizeApiToken( process.env.TODOIST_API_TOKEN ?? null ); return cachedConfig.api; } /** * Get full application configuration (including non-API settings) */ export function getFullConfig(): AppConfig { // Trigger config loading if not already cached getConfig(); if (!cachedConfig) { throw new ValidationError('Failed to load configuration'); } return cachedConfig; } /** * Validate configuration without throwing errors * Useful for health checks and diagnostics */ export function validateConfig(): { valid: boolean; errors: string[]; warnings: string[]; } { const errors: string[] = []; const warnings: string[] = []; try { parseEnvironmentConfig(); } catch (error) { errors.push( error instanceof Error ? error.message : 'Unknown configuration error' ); } // Additional validation warnings const env = process.env; if (!env.NODE_ENV) { warnings.push('NODE_ENV not set, defaulting to development'); } if ( env.TODOIST_API_BASE_URL && env.TODOIST_API_BASE_URL !== CONFIG_DEFAULTS.TODOIST_API_BASE_URL ) { warnings.push('Using custom Todoist API base URL'); } return { valid: errors.length === 0, errors, warnings, }; } /** * Reset cached configuration (useful for testing) */ export function resetConfig(): void { cachedConfig = null; } /** * Get configuration as environment-safe object (no sensitive data) * Useful for logging and diagnostics */ export function getConfigSummary(): { api: { baseUrl: string; timeout: number; retryAttempts: number; hasToken: boolean; }; cache: { ttlProjects: number; ttlLabels: number; ttlSections: number; }; performance: { maxBatchSize: number; rateLimitBuffer: number; }; environment: { nodeEnv: string; isProduction: boolean; }; } { const fullConfig = getFullConfig(); return { api: { baseUrl: fullConfig.api.base_url, timeout: fullConfig.api.timeout, retryAttempts: fullConfig.api.retry_attempts, hasToken: !!fullConfig.api.token, }, cache: fullConfig.cache, performance: fullConfig.performance, environment: { nodeEnv: fullConfig.environment.nodeEnv, isProduction: fullConfig.environment.isProduction, }, }; }

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/shayonpal/mcp-todoist'

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