UNPKG

breathe-api

Version:

Model Context Protocol server for Breathe HR APIs with Swagger/OpenAPI support - also works with custom APIs

494 lines 18.4 kB
import { swaggerCache, apiResponseCache } from './cache.js'; import axios from 'axios'; import { ApiError } from './errors.js'; import { createEnvironmentSecret as createEnvSecret } from './security-utils.js'; import { loadCustomConfig, customConfigToEnvironments } from '../config/custom-config.js'; export function parseResourceUri(uri) { const match = uri.match(/^(\w+):\/\/([^\/]+)\/([^?]+)(\?.*)?$/); if (!match) { throw new Error(`Invalid resource URI format: ${uri}`); } const [, scheme, environment, pathStr, queryStr] = match; const path = pathStr.split('/').filter(Boolean); const resourceType = path[0]; const query = {}; if (queryStr) { const params = new URLSearchParams(queryStr); params.forEach((value, key) => { query[key] = value; }); } return { scheme: scheme, environment: environment, resourceType, path: path.slice(1), query }; } export function buildResourceUri(components) { let uri = `${components.scheme}://${components.environment}/${components.resourceType}`; if (components.path && components.path.length > 0) { uri += '/' + components.path.join('/'); } if (components.query && Object.keys(components.query).length > 0) { const params = new URLSearchParams(components.query); uri += '?' + params.toString(); } return uri; } export class ResourceManager { config; discoveryCache = new Map(); constructor(config) { this.config = { environments: {}, defaultEnvironment: 'production', cacheEnabled: true, cacheTTL: 3600, autoDiscovery: true }; if (config) { this.config = { ...this.config, ...config, environments: config.environments || {} }; } const customConfig = loadCustomConfig(); if (customConfig) { try { const customEnvs = customConfigToEnvironments(customConfig); this.config.environments = customEnvs; this.config.defaultEnvironment = customConfig.defaultEnvironment || 'production'; console.error('Loaded custom API configuration:', customConfig.name); } catch (error) { console.error('Failed to load custom configuration:', error); if (Object.keys(this.config.environments).length === 0) { this.initializeDefaultEnvironments(); } } } else { if (Object.keys(this.config.environments).length === 0) { this.initializeDefaultEnvironments(); } } } initializeDefaultEnvironments() { const baseConfigs = { production: { displayName: 'Production', description: 'Production Breathe HR API', baseUrl: process.env.BREATHE_PRODUCTION_URL || process.env.BREATHE_API_URL || '', isActive: true }, staging: { displayName: 'Staging', description: 'Staging Breathe HR API', baseUrl: process.env.BREATHE_STAGING_URL || '', isActive: false }, development: { displayName: 'Development', description: 'Development Breathe HR API', baseUrl: process.env.BREATHE_DEV_URL || 'http://localhost:3000/v1', isActive: false }, sandbox: { displayName: 'Sandbox', description: 'Sandbox environment for testing', baseUrl: process.env.BREATHE_SANDBOX_URL || '', isActive: false } }; Object.entries(baseConfigs).forEach(([env, config]) => { const envName = env; const envPrefix = env.toUpperCase(); this.config.environments[envName] = { name: envName, ...config, auth: this.getAuthFromEnv(envPrefix), endpoints: this.getDefaultEndpoints() }; }); } getAuthFromEnv(prefix) { const username = process.env[`BREATHE_${prefix}_USERNAME`] || process.env.BREATHE_API_USERNAME; const password = process.env[`BREATHE_${prefix}_PASSWORD`] || process.env.BREATHE_API_PASSWORD; const token = process.env[`BREATHE_${prefix}_TOKEN`]; const apiKey = process.env[`BREATHE_${prefix}_API_KEY`]; if (token) { return { type: 'bearer', token: createEnvSecret(token) }; } else if (apiKey) { return { type: 'api-key', apiKey: createEnvSecret(apiKey), apiKeyHeader: 'X-API-Key' }; } else { return { type: 'basic', username: username || '', password: createEnvSecret(password || '') }; } } getDefaultEndpoints() { return { employees: { path: '/employees', method: 'GET', description: 'List all employees' }, employee: { path: '/employees/{id}', method: 'GET', description: 'Get employee by ID' }, leave: { path: '/absences', method: 'GET', description: 'List leave/absence records' }, timesheets: { path: '/timesheets', method: 'GET', description: 'List timesheet entries' }, kudos: { path: '/kudos', method: 'GET', description: 'List kudos/recognition' } }; } getEnvironment(name) { return this.config.environments[name]; } getActiveEnvironment() { const active = Object.values(this.config.environments).find(env => env.isActive); return active || this.config.environments[this.config.defaultEnvironment]; } setActiveEnvironment(name) { Object.values(this.config.environments).forEach(env => { env.isActive = false; }); const env = this.config.environments[name]; if (env) { env.isActive = true; } else { throw new Error(`Environment '${name}' not found`); } } async listResources() { const resources = []; Object.values(this.config.environments).forEach(env => { resources.push({ uri: `config://${env.name}/environment`, name: `${env.displayName} Configuration`, description: env.description, mimeType: 'application/json', environment: env.name, tags: ['configuration', 'environment'] }); resources.push({ uri: `swagger://${env.name}/spec`, name: `${env.displayName} API Specification`, description: `OpenAPI/Swagger specification for ${env.displayName}`, mimeType: 'application/json', environment: env.name, tags: ['swagger', 'api-spec'] }); if (env.endpoints) { Object.entries(env.endpoints).forEach(([key, endpoint]) => { resources.push({ uri: `breathe://${env.name}/api/${key}`, name: `${env.displayName} - ${endpoint.description || key}`, description: `${endpoint.method || 'GET'} ${endpoint.path}`, mimeType: 'application/json', environment: env.name, tags: ['api', key] }); }); } }); return resources; } getResourceTemplates() { return [ { uriTemplate: 'breathe://{environment}/employees/{id}', name: 'Employee Details', description: 'Get details for a specific employee', mimeType: 'application/json', parameters: [ { name: 'environment', description: 'Target environment', required: true, values: Object.keys(this.config.environments) }, { name: 'id', description: 'Employee ID', required: true } ] }, { uriTemplate: 'breathe://{environment}/api/{endpoint}', name: 'API Endpoint', description: 'Access any API endpoint in any environment', mimeType: 'application/json', parameters: [ { name: 'environment', description: 'Target environment', required: true, values: Object.keys(this.config.environments) }, { name: 'endpoint', description: 'API endpoint path', required: true } ] }, { uriTemplate: 'breathe://{environment}/leave/{employeeId}', name: 'Employee Leave Records', description: 'Get leave records for a specific employee', mimeType: 'application/json', parameters: [ { name: 'environment', description: 'Target environment', required: true, values: Object.keys(this.config.environments) }, { name: 'employeeId', description: 'Employee ID', required: true } ] } ]; } async readResource(uri) { const components = parseResourceUri(uri); const env = this.getEnvironment(components.environment); if (!env) { throw new Error(`Environment '${components.environment}' not found`); } switch (components.scheme) { case 'config': return this.readConfigResource(components, env); case 'swagger': return this.readSwaggerResource(components, env); case 'breathe': return this.readApiResource(components, env); default: throw new Error(`Unsupported resource scheme: ${components.scheme}`); } } async readConfigResource(components, env) { const config = JSON.parse(JSON.stringify(env)); if (config.auth.password) { config.auth.password = '***'; } if (config.auth.token) { config.auth.token = '***'; } if (config.auth.apiKey) { config.auth.apiKey = '***'; } return { uri: buildResourceUri(components), mimeType: 'application/json', text: JSON.stringify(config, null, 2) }; } async readSwaggerResource(components, env) { const cacheKey = `swagger:${env.name}`; if (this.config.cacheEnabled) { const cached = swaggerCache.get(cacheKey); if (cached) { return { uri: buildResourceUri(components), mimeType: 'application/json', text: JSON.stringify(cached, null, 2) }; } } const swaggerUrl = `${env.baseUrl}/swagger.json`; try { const response = await axios.get(swaggerUrl, { auth: this.getAxiosAuth(env.auth), headers: this.getAuthHeaders(env.auth) }); if (this.config.cacheEnabled && this.config.cacheTTL) { swaggerCache.set(cacheKey, response.data, this.config.cacheTTL); } return { uri: buildResourceUri(components), mimeType: 'application/json', text: JSON.stringify(response.data, null, 2) }; } catch (error) { throw ApiError.fromAxiosError(error); } } async readApiResource(components, env) { const endpoint = env.endpoints?.[components.resourceType]; if (!endpoint && !components.path?.length) { throw new Error(`Unknown resource type: ${components.resourceType}`); } let apiPath = endpoint?.path || `/${components.resourceType}`; if (components.path && components.path.length > 0) { apiPath += '/' + components.path.join('/'); } if (components.query) { Object.entries(components.query).forEach(([key, value]) => { apiPath = apiPath.replace(`{${key}}`, value); }); } const cacheKey = `api:${env.name}:${apiPath}`; if (this.config.cacheEnabled) { const cached = apiResponseCache.get(cacheKey); if (cached) { return { uri: buildResourceUri(components), mimeType: 'application/json', text: JSON.stringify(cached, null, 2) }; } } const url = `${env.baseUrl}${apiPath}`; try { const response = await axios.get(url, { auth: this.getAxiosAuth(env.auth), headers: this.getAuthHeaders(env.auth), params: components.query }); if (this.config.cacheEnabled) { apiResponseCache.set(cacheKey, response.data); } return { uri: buildResourceUri(components), mimeType: 'application/json', text: JSON.stringify(response.data, null, 2) }; } catch (error) { throw ApiError.fromAxiosError(error); } } getAxiosAuth(auth) { if (auth.type === 'basic' && auth.username && auth.password) { return { username: auth.username, password: auth.password }; } return undefined; } getAuthHeaders(auth) { const headers = {}; if (auth.type === 'bearer' && auth.token) { headers['Authorization'] = `Bearer ${auth.token}`; } else if (auth.type === 'api-key' && auth.apiKey) { const headerName = auth.apiKeyHeader || 'X-API-Key'; headers[headerName] = auth.apiKey; } return headers; } async discoverEndpoints(environmentName) { const cached = this.discoveryCache.get(environmentName); if (cached && Date.now() - new Date(cached.timestamp).getTime() < this.config.cacheTTL * 1000) { return cached; } const env = this.getEnvironment(environmentName); if (!env) { throw new Error(`Environment '${environmentName}' not found`); } try { const swaggerContent = await this.readSwaggerResource({ scheme: 'swagger', environment: environmentName, resourceType: 'swagger', path: ['spec'] }, env); const swagger = JSON.parse(swaggerContent.text || '{}'); const endpoints = []; if (swagger.paths) { Object.entries(swagger.paths).forEach(([path, methods]) => { Object.entries(methods).forEach(([method, operation]) => { if (['get', 'post', 'put', 'delete', 'patch'].includes(method.toLowerCase())) { endpoints.push({ path, method: method.toUpperCase(), description: operation.summary || operation.description, parameters: operation.parameters?.map((param) => ({ name: param.name, in: param.in, required: param.required, type: param.type || param.schema?.type })) }); } }); }); } const result = { environment: environmentName, endpoints, timestamp: new Date().toISOString() }; this.discoveryCache.set(environmentName, result); return result; } catch (error) { throw new Error(`Failed to discover endpoints for ${environmentName}: ${error.message}`); } } updateEnvironment(name, updates) { const env = this.config.environments[name]; if (!env) { throw new Error(`Environment '${name}' not found`); } this.config.environments[name] = { ...env, ...updates, name }; swaggerCache.del(`swagger:${name}`); this.discoveryCache.delete(name); } addEnvironment(config) { if (this.config.environments[config.name]) { throw new Error(`Environment '${config.name}' already exists`); } this.config.environments[config.name] = config; } removeEnvironment(name) { if (name === this.config.defaultEnvironment) { throw new Error(`Cannot remove default environment '${name}'`); } delete this.config.environments[name]; swaggerCache.del(`swagger:${name}`); this.discoveryCache.delete(name); } } export const resourceManager = new ResourceManager(); //# sourceMappingURL=resource-manager.js.map