UNPKG

haloapi-mcp-tools

Version:

Model Context Protocol (MCP) server for interacting with the HaloPSA API

271 lines (236 loc) 8.91 kB
/** * desktop-mcp.js * * Claude Desktop MCP integration for HaloPSA * This script serves as the main entry point for the Claude Desktop integration. */ 'use strict'; // Load environment variables from .env file require('dotenv').config(); const path = require('path'); const fs = require('fs'); const os = require('os'); // Fix import paths to match the new SDK structure const { Server } = require('@modelcontextprotocol/sdk/dist/cjs/server'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/dist/cjs/server/stdio'); const { registerTools } = require('./src/tools'); const { logger, jsonValidator } = require('./src/utils'); const { server: serverConfig } = require('./src/config'); // Default values const DEFAULT_MCP_SERVER_NAME = process.env.MCP_SERVER_NAME || 'halopsa'; /** * Read Claude Desktop configuration * @returns {Object} Configuration object */ function readClaudeDesktopConfig() { // Determine config locations based on platform const configLocations = []; // Add environment variable path if specified if (process.env.CLAUDE_CONFIG_PATH) { configLocations.push(process.env.CLAUDE_CONFIG_PATH); } // Add default locations based on platform switch (process.platform) { case 'darwin': // macOS configLocations.push(path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')); break; case 'win32': // Windows configLocations.push(path.join(os.homedir(), 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json')); break; case 'linux': // Linux configLocations.push(path.join(os.homedir(), '.config', 'claude', 'claude_desktop_config.json')); break; default: break; } // Try each location for (const configPath of configLocations) { if (fs.existsSync(configPath)) { try { // Read and parse config const configData = fs.readFileSync(configPath, 'utf8'); const config = jsonValidator.safeJsonParse(configData); if (config) { logger.info(`Successfully loaded Claude Desktop configuration from: ${configPath}`); return config; } else { logger.error(`Invalid JSON in Claude Desktop config at ${configPath}`); } } catch (error) { logger.error(`Error reading Claude Desktop config at ${configPath}: ${error.message}`); } } } logger.warn('Claude Desktop config not found in any of the default locations'); logger.info('Using environment variables for configuration'); return null; } /** * Extract HaloPSA configuration from Claude Desktop config * @param {Object} claudeConfig - Claude Desktop configuration * @returns {Object} HaloPSA configuration */ function extractHaloConfig(claudeConfig) { if (!claudeConfig) { return {}; } // Check for both configuration formats // Format 1: mcpServers.halopsa.env style (new format) if (claudeConfig.mcpServers && claudeConfig.mcpServers[DEFAULT_MCP_SERVER_NAME]) { const serverConfig = claudeConfig.mcpServers[DEFAULT_MCP_SERVER_NAME]; const env = serverConfig.env || {}; return { baseUrl: env.HALOPSA_API_URL, authUrl: env.HALOPSA_AUTH_URL || env.HALOPSA_API_URL?.replace(/\/api$/, '/auth/token'), clientId: env.HALOPSA_CLIENT_ID, clientSecret: env.HALOPSA_CLIENT_SECRET, apiKey: env.HALOPSA_API_KEY, // Some setups might use API key instead of OAuth scope: env.HALOPSA_SCOPE, tenant: env.HALOPSA_TENANT }; } // Format 2: haloapi object style (older format) if (claudeConfig.haloapi) { const haloConfig = claudeConfig.haloapi; return { baseUrl: haloConfig.url, authUrl: haloConfig.tokenUrl, clientId: haloConfig.clientId, clientSecret: haloConfig.clientSecret, scope: haloConfig.scope, tenant: haloConfig.tenant }; } // No recognized configuration found logger.warn('No HaloPSA configuration found in Claude Desktop config'); return {}; } /** * Set environment variables from config * @param {Object} haloConfig - HaloPSA configuration */ function setConfigInEnv(haloConfig) { if (haloConfig.baseUrl) process.env.HALO_API_URL = haloConfig.baseUrl; if (haloConfig.authUrl) process.env.HALO_TOKEN_URL = haloConfig.authUrl; if (haloConfig.clientId) process.env.HALO_CLIENT_ID = haloConfig.clientId; if (haloConfig.clientSecret) process.env.HALO_CLIENT_SECRET = haloConfig.clientSecret; if (haloConfig.apiKey) process.env.HALO_API_KEY = haloConfig.apiKey; if (haloConfig.scope) process.env.HALO_SCOPE = haloConfig.scope; if (haloConfig.tenant) process.env.HALO_TENANT = haloConfig.tenant; } /** * Main function to start the desktop MCP server */ async function main() { try { logger.info('Starting HaloPSA MCP Server for Claude Desktop...'); // Read Claude Desktop configuration const claudeConfig = readClaudeDesktopConfig(); const haloConfig = extractHaloConfig(claudeConfig); // Log configuration source if (Object.keys(haloConfig).length > 0) { logger.info('Using HaloPSA configuration from Claude Desktop config'); setConfigInEnv(haloConfig); } else { logger.info('Using HaloPSA configuration from environment variables'); } // Create MCP server const server = new Server({ name: 'halopsa-mcp-server', version: serverConfig.mcpVersion || '1.0.0' }, { capabilities: { tools: {}, resources: { list: true }, prompts: { list: true }, tools: { list: true } } }); // Register all tools with the server registerTools(server); // Set up standard handlers for resources and prompts server.resources = { async list() { logger.debug('Handling resources/list request'); return { content: [{ type: 'text', text: JSON.stringify({ tickets: { description: 'Ticket management resources', operations: ['list', 'get', 'create', 'update', 'delete'] }, users: { description: 'User management resources', operations: ['list', 'get'] }, assets: { description: 'Asset management resources', operations: ['list', 'get'] } }) }] }; } }; server.prompts = { async list() { logger.debug('Handling prompts/list request'); return { content: [{ type: 'text', text: JSON.stringify([ { id: 'create-ticket', name: 'Create a new ticket', description: 'Create a new support ticket in HaloPSA', prompt: 'Create a new ticket in HaloPSA with the following details: [title], [description], [priority], [category]' }, { id: 'find-tickets', name: 'Find tickets', description: 'Search for tickets by various criteria', prompt: 'Find tickets in HaloPSA that match the following criteria: [status], [assignee], [created after date]' }, { id: 'user-lookup', name: 'Look up user', description: 'Find a user in HaloPSA by name or email', prompt: 'Find the user profile in HaloPSA for [name/email]' } ]) }] }; } }; // Create transport const transport = new StdioServerTransport(); // Connect server to transport logger.info('Connecting server to transport...'); await server.connect(transport); logger.info('HaloPSA MCP Server running on stdio'); // Handle process events for graceful shutdown process.on('SIGINT', async () => { logger.info('Received SIGINT signal, shutting down...'); await server.close(); process.exit(0); }); process.on('SIGTERM', async () => { logger.info('Received SIGTERM signal, shutting down...'); await server.close(); process.exit(0); }); // Handle unhandled rejections process.on('unhandledRejection', (reason, promise) => { logger.error('Unhandled Rejection at:', promise, 'reason:', reason); }); // Handle uncaught exceptions process.on('uncaughtException', (error) => { logger.error('Uncaught Exception:', error); process.exit(1); }); return server; } catch (error) { logger.error('Error starting HaloPSA MCP Server for Claude Desktop:', error); process.exit(1); } } // Run the main function if this is the main module if (require.main === module) { main().catch(error => { logger.error('Unhandled error in main function:', error); process.exit(1); }); } module.exports = { main, readClaudeDesktopConfig, extractHaloConfig };