UNPKG

@the_cfdude/productboard-mcp

Version:

Model Context Protocol server for Productboard REST API with dynamic tool loading

147 lines (146 loc) 4.62 kB
/** * Configuration management for Productboard MCP server * Supports multi-workspace setup following Jira MCP patterns */ import { readFileSync, existsSync } from 'fs'; import { join } from 'path'; import { RATE_LIMITS } from './constants.js'; const CONFIG_FILE = '.productboard-config.json'; /** * Load configuration from file or environment variables */ export function loadConfig() { // Check if we're in test mode if (process.env.NODE_ENV === 'test' && !process.env.PRODUCTBOARD_API_TOKEN) { // Return a mock configuration for tests return { instances: { test: { apiToken: 'mock-test-token', baseUrl: 'https://api.productboard.test', rateLimitPerMinute: RATE_LIMITS.DEFAULT_RATE_LIMIT_PER_MINUTE, workspaces: ['test-workspace'], }, }, workspaces: { 'test-workspace': { instance: 'test', workspaceId: 'test-workspace-id', }, }, defaultInstance: 'test', toolCategories: { enabled: ['*'], }, }; } // Try to load from config file first const configPath = findConfigFile(); if (configPath && existsSync(configPath)) { try { const configContent = readFileSync(configPath, 'utf-8'); const config = JSON.parse(configContent); return validateConfig(config); } catch (error) { console.error(`Failed to load config from ${configPath}:`, error); } } // Fallback to environment variables return createConfigFromEnvironment(); } /** * Find config file in current directory or parent directories */ function findConfigFile() { let currentDir = process.cwd(); while (currentDir !== '/') { const configPath = join(currentDir, CONFIG_FILE); if (existsSync(configPath)) { return configPath; } currentDir = join(currentDir, '..'); } return null; } /** * Create configuration from environment variables */ function createConfigFromEnvironment() { const apiToken = process.env.PRODUCTBOARD_API_TOKEN; const baseUrl = process.env.PRODUCTBOARD_BASE_URL || 'https://api.productboard.com'; const workspaceId = process.env.PRODUCTBOARD_WORKSPACE_ID; if (!apiToken) { throw new Error('PRODUCTBOARD_API_TOKEN environment variable is required'); } return { instances: { default: { apiToken, baseUrl, rateLimitPerMinute: 60, workspaces: workspaceId ? [workspaceId] : [], }, }, workspaces: workspaceId ? { [workspaceId]: { instance: 'default', workspaceId, }, } : {}, defaultInstance: 'default', }; } /** * Validate configuration structure */ function validateConfig(config) { if (!config.instances || Object.keys(config.instances).length === 0) { throw new Error('Configuration must have at least one instance'); } for (const [instanceName, instance] of Object.entries(config.instances)) { if (!instance.apiToken) { throw new Error(`Instance '${instanceName}' is missing apiToken`); } if (!instance.baseUrl) { instance.baseUrl = 'https://api.productboard.com'; } if (!instance.rateLimitPerMinute) { instance.rateLimitPerMinute = RATE_LIMITS.DEFAULT_RATE_LIMIT_PER_MINUTE; } } if (!config.defaultInstance) { config.defaultInstance = Object.keys(config.instances)[0]; } return config; } /** * Get instance configuration by name */ export function getInstance(config, instanceName) { const name = instanceName || config.defaultInstance; if (!name || !config.instances[name]) { throw new Error(`Instance '${name}' not found in configuration`); } return config.instances[name]; } /** * Get workspace configuration */ export function getWorkspace(config, workspaceId) { const workspace = config.workspaces?.[workspaceId]; if (!workspace) { // Return default workspace config return { instance: config.defaultInstance, workspaceId, }; } // Ensure workspaceId is always set return { ...workspace, workspaceId: workspace.workspaceId || workspaceId, }; }