UNPKG

halopsa-workflows-mcp

Version:
369 lines (324 loc) 11.6 kB
/** * Direct HaloPSA API Implementation * Based on successful curl commands */ import axios from 'axios'; import dotenv from 'dotenv'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; // Get the directory name of the current module const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Load environment variables from parent directory dotenv.config({ path: path.join(__dirname, '..', '.env') }); /** * HaloPSA API Configuration */ const config = { baseUrl: process.env.HALOPSA_BASE_URL, tenant: process.env.HALOPSA_TENANT, clientId: process.env.HALOPSA_CLIENT_ID, clientSecret: process.env.HALOPSA_CLIENT_SECRET, scope: process.env.HALOPSA_SCOPE || 'all' }; // Validate configuration if (!config.baseUrl || !config.tenant || !config.clientId || !config.clientSecret) { console.error('[ERROR] Missing required configuration. Please check your .env file.'); console.error('Required variables: HALOPSA_BASE_URL, HALOPSA_TENANT, HALOPSA_CLIENT_ID, HALOPSA_CLIENT_SECRET'); process.exit(1); } console.error('[INFO] HaloPSA Direct API Implementation'); console.error('---------------------------------------'); console.error(`Base URL: ${config.baseUrl}`); console.error(`Tenant: ${config.tenant}`); console.error(`Client ID: ${config.clientId}`); console.error(`Client Secret: ${config.clientSecret.substring(0, 10)}...`); console.error(`Scope: ${config.scope}`); console.error('---------------------------------------'); let tokenCache = { accessToken: null, expiresAt: 0 }; /** * Get authentication token from HaloPSA * @returns {Promise<string>} Access token */ async function getAuthToken() { // Check if we have a valid cached token if (tokenCache.accessToken && tokenCache.expiresAt > Date.now()) { console.error('[INFO] Using cached token'); return tokenCache.accessToken; } console.error('[INFO] Getting new auth token...'); try { // Prepare the token URL with tenant parameter // Make sure to handle the case where baseUrl already includes /api const baseUrlForAuth = config.baseUrl.endsWith('/api') ? config.baseUrl.substring(0, config.baseUrl.length - 4) : config.baseUrl; const tokenUrl = `${baseUrlForAuth}/auth/token?tenant=${config.tenant}`; console.error(`[DEBUG] Token URL: ${tokenUrl}`); // Prepare form data exactly like successful curl command const formData = new URLSearchParams(); formData.append('grant_type', 'client_credentials'); formData.append('client_id', config.clientId); formData.append('client_secret', config.clientSecret); formData.append('scope', config.scope); // Make token request const response = await axios.post(tokenUrl, formData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); // Extract token data const { access_token, expires_in } = response.data; // Cache the token tokenCache = { accessToken: access_token, expiresAt: Date.now() + (expires_in * 1000) - 60000 // Buffer 1 minute }; console.error(`[INFO] Successfully obtained auth token, expires in ${expires_in} seconds`); return access_token; } catch (error) { console.error('[ERROR] Failed to get auth token:'); if (error.response) { console.error(`Status: ${error.response.status}`); console.error('Response:', error.response.data); } else { console.error(error.message); } throw new Error(`Authentication failed: ${error.message}`); } } /** * Get workflows from HaloPSA * @param {boolean} includeInactive - Whether to include inactive workflows * @returns {Promise<Array>} Workflows */ async function getWorkflows(includeInactive = false) { try { // Get auth token const token = await getAuthToken(); // Prepare API endpoint // Handle the case where baseUrl already ends with /api const apiBase = config.baseUrl.endsWith('/api') ? config.baseUrl : `${config.baseUrl}/api`; const apiEndpoint = `${apiBase}/Workflow`; console.error(`[DEBUG] API Endpoint: ${apiEndpoint}`); // Query parameters const params = {}; if (includeInactive !== undefined) { params.includeinactive = includeInactive; } // Make API request const response = await axios.get(apiEndpoint, { headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }, params }); console.error(`[INFO] Retrieved ${response.data.length} workflows`); return response.data; } catch (error) { console.error('[ERROR] Failed to get workflows:'); if (error.response) { console.error(`Status: ${error.response.status}`); console.error('Response:', error.response.data); } else { console.error(error.message); } throw new Error(`Failed to get workflows: ${error.message}`); } } /** * Get workflow steps from HaloPSA * @param {boolean} includeCriteriaInfo - Whether to include criteria information * @returns {Promise<Array>} Workflow steps */ async function getWorkflowSteps(includeCriteriaInfo = false) { try { // Get auth token const token = await getAuthToken(); // Prepare API endpoint // Handle the case where baseUrl already ends with /api const apiBase = config.baseUrl.endsWith('/api') ? config.baseUrl : `${config.baseUrl}/api`; const apiEndpoint = `${apiBase}/WorkflowStep`; console.error(`[DEBUG] API Endpoint: ${apiEndpoint}`); // Query parameters const params = {}; if (includeCriteriaInfo !== undefined) { params.includecriteriainfo = includeCriteriaInfo; } // Make API request const response = await axios.get(apiEndpoint, { headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }, params }); console.error(`[INFO] Retrieved ${response.data.length} workflow steps`); return response.data; } catch (error) { console.error('[ERROR] Failed to get workflow steps:'); if (error.response) { console.error(`Status: ${error.response.status}`); console.error('Response:', error.response.data); } else { console.error(error.message); } throw new Error(`Failed to get workflow steps: ${error.message}`); } } /** * Get a specific workflow by ID * @param {number} id - Workflow ID * @param {boolean} includeDetails - Whether to include workflow details * @returns {Promise<Object>} Workflow */ async function getWorkflow(id, includeDetails = false) { try { // Get auth token const token = await getAuthToken(); // Prepare API endpoint // Handle the case where baseUrl already ends with /api const apiBase = config.baseUrl.endsWith('/api') ? config.baseUrl : `${config.baseUrl}/api`; const apiEndpoint = `${apiBase}/Workflow/${id}`; console.error(`[DEBUG] API Endpoint: ${apiEndpoint}`); // Query parameters const params = {}; if (includeDetails !== undefined) { params.includedetails = includeDetails; } // Make API request const response = await axios.get(apiEndpoint, { headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }, params }); console.error(`[INFO] Retrieved workflow ${id}`); return response.data; } catch (error) { console.error(`[ERROR] Failed to get workflow ${id}:`); if (error.response) { console.error(`Status: ${error.response.status}`); console.error('Response:', error.response.data); } else { console.error(error.message); } throw new Error(`Failed to get workflow ${id}: ${error.message}`); } } /** * Delete a workflow * @param {number} id - Workflow ID to delete * @returns {Promise<void>} */ async function deleteWorkflow(id) { try { // Get auth token const token = await getAuthToken(); // Prepare API endpoint // Handle the case where baseUrl already ends with /api const apiBase = config.baseUrl.endsWith('/api') ? config.baseUrl : `${config.baseUrl}/api`; const apiEndpoint = `${apiBase}/Workflow/${id}`; console.error(`[DEBUG] API Endpoint: ${apiEndpoint}`); // Make API request await axios.delete(apiEndpoint, { headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' } }); console.error(`[INFO] Deleted workflow ${id}`); } catch (error) { console.error(`[ERROR] Failed to delete workflow ${id}:`); if (error.response) { console.error(`Status: ${error.response.status}`); console.error('Response:', error.response.data); } else { console.error(error.message); } throw new Error(`Failed to delete workflow ${id}: ${error.message}`); } } /** * Create a new workflow * @param {Array} workflows - Array of workflow objects to create * @returns {Promise<Array>} Created workflows */ async function createWorkflows(workflows) { try { // Get auth token const token = await getAuthToken(); // Prepare API endpoint // Handle the case where baseUrl already ends with /api const apiBase = config.baseUrl.endsWith('/api') ? config.baseUrl : `${config.baseUrl}/api`; const apiEndpoint = `${apiBase}/Workflow`; console.error(`[DEBUG] API Endpoint: ${apiEndpoint}`); // Make API request const response = await axios.post(apiEndpoint, workflows, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'Accept': 'application/json' } }); console.error(`[INFO] Created ${response.data.length} workflows`); return response.data; } catch (error) { console.error('[ERROR] Failed to create workflows:'); if (error.response) { console.error(`Status: ${error.response.status}`); console.error('Response:', error.response.data); } else { console.error(error.message); } throw new Error(`Failed to create workflows: ${error.message}`); } } // Export the functions export { getAuthToken, getWorkflows, getWorkflowSteps, getWorkflow, deleteWorkflow, createWorkflows }; // If this file is run directly, test the functions if (process.argv[1] === fileURLToPath(import.meta.url)) { console.error('[INFO] Running direct test...'); (async () => { try { // Test getting workflows console.error('\n[TEST] Getting workflows...'); const workflows = await getWorkflows(); console.error(`Retrieved ${workflows.length} workflows`); if (workflows.length > 0) { console.error('First workflow:', workflows[0]); // Test getting workflow details console.error(`\n[TEST] Getting workflow ${workflows[0].id} details...`); const workflow = await getWorkflow(workflows[0].id, true); console.error('Workflow details:', workflow); } // Test getting workflow steps console.error('\n[TEST] Getting workflow steps...'); try { const steps = await getWorkflowSteps(); console.error(`Retrieved ${steps.length} workflow steps`); if (steps.length > 0) { console.error('First workflow step:', steps[0]); } } catch (error) { console.error('Workflow steps not available or accessible'); } console.error('\n[INFO] Test completed successfully!'); } catch (error) { console.error('[ERROR] Test failed:', error.message); } })(); }