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
JavaScript
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