haloapi-mcp-tools
Version:
Model Context Protocol (MCP) server for interacting with the HaloPSA API
271 lines (236 loc) • 8.91 kB
JavaScript
/**
* 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 };