suitecrm-mcp-server
Version:
Model Context Protocol server for SuiteCRM integration with natural language SQL reporting
385 lines • 16.1 kB
JavaScript
#!/usr/bin/env node
"use strict";
/**
* Main MCP server for SuiteCRM integration
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
const zod_1 = require("zod");
const dotenv_1 = __importDefault(require("dotenv"));
const auth_1 = require("./services/auth");
const modules_1 = require("./services/modules");
const query_1 = require("./services/query");
const logger_1 = require("./utils/logger");
// Load environment variables
dotenv_1.default.config();
// Environment configuration
const config = {
SUITECRM_URL: process.env['SUITECRM_URL'] || 'http://localhost/suitecrm',
SUITECRM_CLIENT_ID: process.env['SUITECRM_CLIENT_ID'] || '',
SUITECRM_CLIENT_SECRET: process.env['SUITECRM_CLIENT_SECRET'] || '',
LOG_LEVEL: process.env['LOG_LEVEL'] || 'info',
MAX_QUERY_ROWS: parseInt(process.env['MAX_QUERY_ROWS'] || '100', 10),
REQUEST_TIMEOUT: parseInt(process.env['REQUEST_TIMEOUT'] || '30000', 10),
CACHE_TTL: parseInt(process.env['CACHE_TTL'] || '1800000', 10),
RATE_LIMIT_WINDOW: parseInt(process.env['RATE_LIMIT_WINDOW'] || '60000', 10),
RATE_LIMIT_MAX_REQUESTS: parseInt(process.env['RATE_LIMIT_MAX_REQUESTS'] || '100', 10)
};
// Initialize services
const authService = new auth_1.AuthService(config.SUITECRM_URL, config.REQUEST_TIMEOUT);
const moduleService = new modules_1.ModuleService(config.SUITECRM_URL, config.REQUEST_TIMEOUT);
const queryService = new query_1.QueryService(config.SUITECRM_URL, config.MAX_QUERY_ROWS, config.REQUEST_TIMEOUT);
// Set log level
logger_1.logger.setLevel(config.LOG_LEVEL);
// MCP Server implementation
class SuiteCRMCPServer {
server;
constructor() {
this.server = new index_js_1.Server({
name: 'suitecrm-mcp-server',
version: '1.0.0',
});
this.setupTools();
this.setupErrorHandling();
}
setupTools() {
// Tool 1: authenticate_crm
this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'authenticate_crm',
description: 'Authenticate with SuiteCRM using client credentials',
inputSchema: {
type: 'object',
properties: {
crm_url: {
type: 'string',
description: 'SuiteCRM instance URL'
},
client_id: {
type: 'string',
description: 'OAuth client ID'
},
client_secret: {
type: 'string',
description: 'OAuth client secret'
}
},
required: ['crm_url', 'client_id', 'client_secret']
}
},
{
name: 'get_modules',
description: 'Fetch available CRM modules',
inputSchema: {
type: 'object',
properties: {
crm_url: {
type: 'string',
description: 'SuiteCRM instance URL'
},
access_token: {
type: 'string',
description: 'Valid access token'
}
},
required: ['crm_url', 'access_token']
}
},
{
name: 'get_module_schema',
description: 'Get detailed schema for a specific module',
inputSchema: {
type: 'object',
properties: {
crm_url: {
type: 'string',
description: 'SuiteCRM instance URL'
},
access_token: {
type: 'string',
description: 'Valid access token'
},
module_name: {
type: 'string',
description: 'Name of the module to retrieve schema for'
}
},
required: ['crm_url', 'access_token', 'module_name']
}
},
{
name: 'execute_query',
description: 'Execute SQL query against CRM database',
inputSchema: {
type: 'object',
properties: {
crm_url: {
type: 'string',
description: 'SuiteCRM instance URL'
},
access_token: {
type: 'string',
description: 'Valid access token'
},
sql_query: {
type: 'string',
description: 'SQL query to execute (SELECT only)'
}
},
required: ['crm_url', 'access_token', 'sql_query']
}
}
]
};
});
// Tool 2: authenticate_crm implementation
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'authenticate_crm': {
const params = AuthenticateCrmParamsSchema.parse(args);
const result = await this.handleAuthenticateCrm(params);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
case 'get_modules': {
const params = GetModulesParamsSchema.parse(args);
const result = await this.handleGetModules(params);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
case 'get_module_schema': {
const params = GetModuleSchemaParamsSchema.parse(args);
const result = await this.handleGetModuleSchema(params);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
case 'execute_query': {
const params = ExecuteQueryParamsSchema.parse(args);
const result = await this.handleExecuteQuery(params);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
catch (error) {
logger_1.logger.error(`Tool execution failed: ${name}`, { args }, error instanceof Error ? error : new Error(String(error)));
throw error;
}
});
}
async handleAuthenticateCrm(params) {
try {
logger_1.logger.info('Authenticating with SuiteCRM', { crmUrl: params.crm_url });
const authResponse = await authService.authenticate({
client_id: params.client_id,
client_secret: params.client_secret
});
return {
success: true,
token: authResponse.access_token,
expires_in: authResponse.expires_in
};
}
catch (error) {
logger_1.logger.error('Authentication failed', { crmUrl: params.crm_url }, error instanceof Error ? error : new Error(String(error)));
return {
success: false,
token: '',
expires_in: 0,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
async handleGetModules(params) {
try {
logger_1.logger.info('Fetching modules', { crmUrl: params.crm_url });
const modules = await moduleService.getModules(params.access_token);
return {
success: true,
modules
};
}
catch (error) {
logger_1.logger.error('Failed to fetch modules', { crmUrl: params.crm_url }, error instanceof Error ? error : new Error(String(error)));
return {
success: false,
modules: [],
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
async handleGetModuleSchema(params) {
try {
logger_1.logger.info('Fetching module schema', {
crmUrl: params.crm_url,
module: params.module_name
});
const schema = await moduleService.getModuleSchema(params.access_token, params.module_name);
return {
success: true,
schema
};
}
catch (error) {
logger_1.logger.error('Failed to fetch module schema', {
crmUrl: params.crm_url,
module: params.module_name
}, error instanceof Error ? error : new Error(String(error)));
return {
success: false,
schema: {
name: params.module_name,
label: '',
fields: [],
relationships: [],
indexes: []
},
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
async handleExecuteQuery(params) {
try {
logger_1.logger.info('Executing query', {
crmUrl: params.crm_url,
queryLength: params.sql_query.length
});
const response = await queryService.executeQuery(params.access_token, {
sql: params.sql_query
});
return {
success: true,
data: response.data,
metadata: response.metadata
};
}
catch (error) {
logger_1.logger.error('Query execution failed', {
crmUrl: params.crm_url
}, error instanceof Error ? error : new Error(String(error)));
return {
success: false,
data: [],
metadata: {
query_time: 0,
rows_returned: 0,
columns: []
},
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
setupErrorHandling() {
process.on('uncaughtException', (error) => {
logger_1.logger.error('Uncaught exception', {}, error);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
logger_1.logger.error('Unhandled rejection', { promise }, reason instanceof Error ? reason : new Error(String(reason)));
process.exit(1);
});
process.on('SIGINT', () => {
logger_1.logger.info('Received SIGINT, shutting down gracefully');
this.shutdown();
});
process.on('SIGTERM', () => {
logger_1.logger.info('Received SIGTERM, shutting down gracefully');
this.shutdown();
});
}
async shutdown() {
try {
authService.destroy();
moduleService.clearCache();
await logger_1.logger.close();
process.exit(0);
}
catch (error) {
logger_1.logger.error('Error during shutdown', {}, error instanceof Error ? error : new Error(String(error)));
process.exit(1);
}
}
async run() {
try {
logger_1.logger.info('Starting SuiteCRM MCP Server', {
version: '1.0.0',
config: {
maxQueryRows: config.MAX_QUERY_ROWS,
requestTimeout: config.REQUEST_TIMEOUT,
logLevel: config.LOG_LEVEL
}
});
const transport = new stdio_js_1.StdioServerTransport();
await this.server.connect(transport);
logger_1.logger.info('SuiteCRM MCP Server started successfully');
}
catch (error) {
logger_1.logger.error('Failed to start MCP server', {}, error instanceof Error ? error : new Error(String(error)));
process.exit(1);
}
}
}
// Validation schemas for tool parameters
const AuthenticateCrmParamsSchema = zod_1.z.object({
crm_url: zod_1.z.string().url('Invalid CRM URL'),
client_id: zod_1.z.string().min(1, 'Client ID is required'),
client_secret: zod_1.z.string().min(1, 'Client secret is required')
});
const GetModulesParamsSchema = zod_1.z.object({
crm_url: zod_1.z.string().url('Invalid CRM URL'),
access_token: zod_1.z.string().min(1, 'Access token is required')
});
const GetModuleSchemaParamsSchema = zod_1.z.object({
crm_url: zod_1.z.string().url('Invalid CRM URL'),
access_token: zod_1.z.string().min(1, 'Access token is required'),
module_name: zod_1.z.string().min(1, 'Module name is required')
});
const ExecuteQueryParamsSchema = zod_1.z.object({
crm_url: zod_1.z.string().url('Invalid CRM URL'),
access_token: zod_1.z.string().min(1, 'Access token is required'),
sql_query: zod_1.z.string().min(1, 'SQL query is required')
});
// Start the server
if (require.main === module) {
const server = new SuiteCRMCPServer();
server.run().catch((error) => {
logger_1.logger.error('Server startup failed', {}, error instanceof Error ? error : new Error(String(error)));
process.exit(1);
});
}
//# sourceMappingURL=index.js.map