Skip to main content
Glama
vijaykrishnazpro

Remote MCP Server on Cloudflare

index-see.js189 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import fetch from 'node-fetch'; import winston from 'winston'; import express from 'express'; import cors from 'cors'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; // Get current directory for ES modules const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const logger = winston.createLogger({ level: 'debug', format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'maximo-mcp-sse.log', format: winston.format.json() }), new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), winston.format.simple() ) }) ], }); logger.info('Starting Maximo MCP Server (SSE Version)'); // Configuration from environment variables const MAXIMO_HOST = process.env.MAXIMO_HOST; const MAXAUTH = process.env.MAXAUTH; const SERVER_PORT = process.env.MCP_PORT || 3000; const SERVER_HOST = process.env.MCP_HOST || '0.0.0.0'; if (!MAXIMO_HOST || !MAXAUTH) { logger.error('Missing required environment variables: MAXIMO_HOST and/or MAXAUTH'); process.exit(1); } logger.info(`Maximo Host: ${MAXIMO_HOST}`); logger.info(`MCP Server will run on: http://${SERVER_HOST}:${SERVER_PORT}`); // Site mappings and constants (same as original) const SITE_MAPPINGS = { 'MES': 'MES-01', 'SQI': 'SQI-01', 'ZPRO': 'ZPROINC', 'Internal': 'ZPROINC', 'Transdev': 'SFRTA', 'Miller Knoll': 'MKS', 'MK': 'MKS', 'Kindle': 'KP', 'STQ': 'STQ' }; const SR_TYPES = { 'A': 'Application', 'D': 'Database', 'I': 'Infrastructure', 'O': 'Other' }; // Maximo request function (same as original) async function makeMaximoRequest(endpoint, options = {}) { const url = `${MAXIMO_HOST}${endpoint}`; logger.info(`Making request to: ${url}`); logger.debug(`Request options:`, { method: options.method || 'GET', headers: options.headers ? Object.keys(options.headers) : [], bodyPresent: !!options.body }); const defaultHeaders = { 'MAXAUTH': MAXAUTH, 'Content-Type': 'application/json', 'Accept': 'application/json' }; const requestOptions = { method: options.method || 'GET', headers: { ...defaultHeaders, ...options.headers }, ...options }; try { logger.debug('Sending request with headers:', Object.keys(requestOptions.headers)); const response = await fetch(url, requestOptions); logger.info(`Response status: ${response.status} ${response.statusText}`); logger.debug(`Response headers:`, Object.fromEntries(response.headers.entries())); const responseText = await response.text(); logger.debug(`Raw response body: ${responseText.substring(0, 500)}${responseText.length > 500 ? '...' : ''}`); if (!response.ok) { logger.error(`HTTP error ${response.status}: ${responseText}`); throw new Error(`HTTP ${response.status}: ${responseText}`); } let responseData; try { responseData = JSON.parse(responseText); logger.debug('Successfully parsed JSON response'); } catch (parseError) { logger.warn('Failed to parse JSON response, returning raw text'); responseData = responseText; } return responseData; } catch (error) { logger.error('Request failed:', error); throw error; } } // Create MCP Server instance const server = new Server( { name: 'maximo-mcp-server-sse', version: '2.0.0', }, { capabilities: { tools: {}, }, } ); // Set up tool handlers (same as original but keeping all tools) server.setRequestHandler(ListToolsRequestSchema, async () => { logger.info('Listing available tools via SSE'); return { tools: [ { name: 'query_tickets', description: 'Query Maximo service requests/tickets assigned to the specified user', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'Maximo username to query tickets for (e.g., VKRISHNA)', required: true }, status_filter: { type: 'string', description: 'Filter by status (default: exclude RESOLVED, CLOSED, CANCELLED)', enum: ['all', 'active', 'resolved', 'closed'] }, limit: { type: 'number', description: 'Maximum number of tickets to return (default: 50)', minimum: 1, maximum: 200 } }, required: ['username'] } }, { name: 'query_tickets_by_site', description: 'Query Maximo tickets filtered by specific site ID and/or custom status values for specified user', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'Maximo username to query tickets for (e.g., VKRISHNA)', required: true }, site_id: { type: 'string', description: 'Site ID to filter by (e.g., SQI-01, MES-01, KP, etc.)' }, status_values: { type: 'array', items: { type: 'string' }, description: 'Array of specific status values to filter by (e.g., ["INPROG", "L3INPROG"])' }, limit: { type: 'number', description: 'Maximum number of tickets to return (default: 50)', minimum: 1, maximum: 200 } }, required: ['username'] } }, { name: 'create_ticket', description: 'Create a new service request ticket in Maximo', inputSchema: { type: 'object', properties: { reporter_username: { type: 'string', description: 'Username of the person reporting the ticket (e.g., VKRISHNA)', required: true }, reporter_email: { type: 'string', description: 'Email address of the person reporting the ticket', required: true }, siteid: { type: 'string', description: 'Site ID (will be mapped automatically)', required: true }, title: { type: 'string', description: 'Short title/summary of the issue', required: true }, description: { type: 'string', description: 'Detailed description of the issue', required: true }, priority: { type: 'number', description: 'Priority level (1-5, where 1 is highest)', minimum: 1, maximum: 5, required: true }, is_production: { type: 'boolean', description: 'Whether this affects production environment', required: true }, sr_type: { type: 'string', description: 'Service request type', enum: ['A', 'D', 'I', 'O', 'U', 'R', 'VM', 'NW', 'C', 'AM'], required: true } }, required: ['reporter_username', 'reporter_email', 'siteid', 'title', 'description', 'priority', 'is_production', 'sr_type'] } }, { name: 'get_ticket_details', description: 'Get detailed information about a specific ticket', inputSchema: { type: 'object', properties: { ticket_id: { type: 'string', description: 'The ticket ID to query', required: true } }, required: ['ticket_id'] } }, { name: 'get_lastweek_tickets', description: 'Get all tickets reported in the last 7 days for specified user', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'Maximo username to query tickets for (e.g., VKRISHNA)', required: true }, limit: { type: 'number', description: 'Maximum number of tickets to return (default: 100)', minimum: 1, maximum: 200 } }, required: ['username'] } }, { name: 'get_work_done_lastweek', description: 'Get work hours done on tickets in the last 7 days, grouped by ticket for specified user', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'Maximo username to query work activities for (e.g., VKRISHNA)', required: true } }, required: ['username'] } }, { name: 'get_work_done_lastweek_bycustomer', description: 'Get work hours done on tickets in the last 7 days, grouped by customer/site for specified user', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'Maximo username to query work activities for (e.g., VKRISHNA)', required: true } }, required: ['username'] } }, { name: 'get_tickets_daterange', description: 'Get tickets within a custom date range based on report date for specified user', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'Maximo username to query tickets for (e.g., VKRISHNA)', required: true }, start_date: { type: 'string', description: 'Start date in YYYY-MM-DD format (e.g., 2025-06-01)', pattern: '^\\d{4}-\\d{2}-\\d{2} ] }; }); // Tool request handler (keeping all the original tool logic) server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; logger.info(`Tool called via SSE: ${name}`, args); try { switch (name) { case 'query_tickets': { const username = args.username; logger.info(`Querying tickets for user ${username}`); const statusFilter = args.status_filter || 'active'; const limit = args.limit || 50; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE&lean=1`; if (statusFilter === 'active') { endpoint += `&oslc.where=woactivity.owner="${username}" and status!="RESOLVED" and status!="CLOSED" and status!="CANCELLED"`; } else if (statusFilter !== 'all') { endpoint += `&oslc.where=woactivity.owner="${username}" and status="${statusFilter.toUpperCase()}"`; } else { endpoint += `&oslc.where=woactivity.owner="${username}"`; } endpoint += `&oslc.pageSize=${limit}`; logger.debug(`Query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); logger.info(`Retrieved ${response.member ? response.member.length : 0} tickets for user ${username}`); return { content: [ { type: 'text', text: JSON.stringify(response, null, 2) } ] }; } // ... (include all other case statements from original - query_tickets_by_site, create_ticket, etc.) // I'm including the create_ticket case as an example: case 'create_ticket': { const reporterUsername = args.reporter_username; const reporterEmail = args.reporter_email; logger.info('Creating new ticket via SSE', { reporter_username: reporterUsername, reporter_email: reporterEmail, siteid: args.siteid, title: args.title, priority: args.priority, is_production: args.is_production, sr_type: args.sr_type }); const mappedSiteId = SITE_MAPPINGS[args.siteid] || args.siteid; logger.info(`Mapped site ID: ${args.siteid} -> ${mappedSiteId}`); const ticketData = { "_action": "AddChange", "siteid": mappedSiteId, "description": args.title, "description_longdescription": `<p>${args.description}</p>`, "reportedby": reporterUsername, "affectedperson": reporterUsername, "reportedemail": reporterEmail, "reportedpriority": args.priority, "zp_production": args.is_production ? "YES" : "NO", "zp_srtype": args.sr_type }; logger.debug('Ticket payload:', ticketData); const endpoint = `/maximo/oslc/os/MXSR?lean=1&ignorecollectionref=1&ignorekeyref=1&ignorers=1&mxlaction=addchange`; const response = await makeMaximoRequest(endpoint, { method: 'POST', headers: { 'MAXAUTH': MAXAUTH, 'x-method-override': 'BULK' }, body: JSON.stringify([ticketData]) }); logger.info('Ticket creation response received'); logger.debug('Creation response:', response); let ticketNumber = 'Unknown'; if (response && Array.isArray(response) && response.length > 0) { const responseItem = response[0]; if (responseItem.TICKETID || responseItem.ticketid) { ticketNumber = responseItem.TICKETID || responseItem.ticketid; } else if (responseItem._responsemeta && responseItem._responsemeta.Location) { try { const locationUrl = responseItem._responsemeta.Location; logger.debug(`Extracting ticket ID from Location: ${locationUrl}`); const encodedPart = locationUrl.split('/').pop(); let cleanEncoded = encodedPart.replace(/^_+/, '').replace(/-+$/, ''); const standardBase64 = cleanEncoded.replace(/_/g, '/').replace(/-/g, '+'); let paddedBase64 = standardBase64; while (paddedBase64.length % 4) { paddedBase64 += '='; } const decoded = Buffer.from(paddedBase64, 'base64').toString('utf-8'); const ticketMatch = decoded.match(/SR\/(\d+)/); if (ticketMatch && ticketMatch[1]) { ticketNumber = ticketMatch[1]; logger.info(`Successfully extracted ticket ID: ${ticketNumber}`); } } catch (decodeError) { logger.warn('Failed to decode ticket ID from Location URL:', decodeError); } } } const successMessage = ticketNumber !== 'Unknown' ? `Ticket created successfully!\n\nTicket ID: ${ticketNumber}\nReporter: ${reporterUsername}\nSite: ${mappedSiteId}\nPriority: ${args.priority}\nType: ${args.sr_type}\nProduction: ${args.is_production ? 'YES' : 'NO'}\n\nTitle: ${args.title}` : `Ticket created but ID could not be extracted.\n\nPlease check the logs and query recent tickets to find the new ticket.\n\nFull Response:\n${JSON.stringify(response, null, 2)}`; return { content: [ { type: 'text', text: successMessage } ] }; } case 'query_tickets_by_site': { const username = args.username; logger.info('Querying tickets by site and/or status', { username: username, site_id: args.site_id, status_values: args.status_values, limit: args.limit }); const limit = args.limit || 50; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE,SITEID&lean=1`; let whereConditions = [`woactivity.owner="${username}"`]; if (args.site_id) { whereConditions.push(`siteid="${args.site_id}"`); } if (args.status_values && Array.isArray(args.status_values) && args.status_values.length > 0) { const statusConditions = args.status_values.map(status => `status="${status.toUpperCase()}"`); if (statusConditions.length === 1) { whereConditions.push(statusConditions[0]); } else { whereConditions.push(`(${statusConditions.join(' or ')})`); } } const whereClause = whereConditions.join(' and '); endpoint += `&oslc.where=${encodeURIComponent(whereClause)}`; endpoint += `&oslc.pageSize=${limit}`; logger.debug(`Site/Status query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); logger.info(`Retrieved ${response.member ? response.member.length : 0} tickets matching criteria for user ${username}`); if (!response.member || response.member.length === 0) { const filterDescription = []; if (args.site_id) filterDescription.push(`Site: ${args.site_id}`); if (args.status_values) filterDescription.push(`Status: ${args.status_values.join(', ')}`); return { content: [ { type: 'text', text: `No tickets found matching criteria: ${filterDescription.join(', ')} for user ${username}` } ] }; } const ticketSummary = response.member.map(ticket => ({ ticketId: ticket.ticketid, description: ticket.description, status: ticket.status, priority: ticket.reportedpriority, site: ticket.siteid, production: ticket.zp_production, srType: ticket.zp_srtype })); const filterDescription = []; if (args.site_id) filterDescription.push(`Site: ${args.site_id}`); if (args.status_values) filterDescription.push(`Status: ${args.status_values.join(', ')}`); return { content: [ { type: 'text', text: `Tickets matching criteria (${filterDescription.join(', ')}) for user ${username}: ${response.member.length} found\n\n${JSON.stringify(ticketSummary, null, 2)}` } ] }; } case 'get_ticket_details': { logger.info(`Getting details for ticket: ${args.ticket_id}`); const endpoint = `/maximo/oslc/os/MXSR?oslc.select=*&oslc.where=TICKETID="${args.ticket_id}"&lean=1`; const response = await makeMaximoRequest(endpoint); if (!response.member || response.member.length === 0) { logger.warn(`Ticket ${args.ticket_id} not found`); return { content: [ { type: 'text', text: `Ticket ${args.ticket_id} not found. Please verify the ticket ID and ensure you have access to this ticket.` } ] }; } logger.info(`Retrieved details for ticket ${args.ticket_id}`); const ticketData = response.member[0]; const formattedResponse = { ticketId: ticketData.ticketid || 'Unknown', status: ticketData.status || 'Unknown', description: ticketData.description || 'No description', longDescription: ticketData.description_longdescription || 'No detailed description', priority: ticketData.reportedpriority || 'Unknown', siteId: ticketData.siteid || 'Unknown', production: ticketData.zp_production || 'Unknown', srType: ticketData.zp_srtype || 'Unknown', creationDate: ticketData.creationdate || 'Unknown', reportedBy: ticketData.reportedby || 'Unknown', affectedPerson: ticketData.affectedperson || 'Unknown', fullDetails: response }; return { content: [ { type: 'text', text: `Ticket Details:\n\n${JSON.stringify(formattedResponse, null, 2)}` } ] }; } case 'get_lastweek_tickets': { const username = args.username; logger.info(`Getting tickets from last 7 days for user ${username}`); const limit = args.limit || 100; const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); const dateFilter = sevenDaysAgo.toISOString().split('T')[0]; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE,REPORTDATE,SITEID&lean=1`; endpoint += `&oslc.where=woactivity.owner="${username}" and REPORTDATE>="${dateFilter}"`; endpoint += `&oslc.pageSize=${limit}`; logger.debug(`Last week query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); logger.info(`Retrieved ${response.member ? response.member.length : 0} tickets from last 7 days for user ${username}`); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No tickets found in the last 7 days (since ${dateFilter}) for user ${username}` } ] }; } const ticketSummary = response.member.map(ticket => ({ ticketId: ticket.ticketid, description: ticket.description, status: ticket.status, priority: ticket.reportedpriority, site: ticket.siteid, reportDate: ticket.reportdate })); return { content: [ { type: 'text', text: `Last Week's Tickets for ${username} (${response.member.length} found since ${dateFilter}):\n\n${JSON.stringify(ticketSummary, null, 2)}` } ] }; } case 'get_work_done_lastweek': { const username = args.username; logger.info(`Getting work hours by ticket for last 7 days for user ${username}`); const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); const dateFilter = sevenDaysAgo.toISOString().split('T')[0]; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,SITEID,woactivity.ESTDUR,woactivity.OWNER&lean=1`; endpoint += `&oslc.where=REPORTDATE>="${dateFilter}" and woactivity.owner="${username}"`; endpoint += `&oslc.pageSize=500`; logger.debug(`Work hours query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No work activities found for the last 7 days (since ${dateFilter}) for user ${username}` } ] }; } const workByTicket = {}; response.member.forEach(ticket => { const ticketId = ticket.ticketid; const description = ticket.description; if (!workByTicket[ticketId]) { workByTicket[ticketId] = { description: description, totalHours: 0 }; } const activities = ticket.woactivity || []; const activityArray = Array.isArray(activities) ? activities : [activities]; activityArray.forEach(activity => { if (activity && activity.owner === username && activity.estdur) { workByTicket[ticketId].totalHours += parseFloat(activity.estdur) || 0; } }); }); const workSummary = Object.entries(workByTicket) .filter(([_, data]) => data.totalHours > 0) .map(([ticketId, data]) => `${ticketId} (${data.description.substring(0, 50)}...) → ${data.totalHours} hours`) .join('\n'); const totalHours = Object.values(workByTicket).reduce((sum, data) => sum + data.totalHours, 0); return { content: [ { type: 'text', text: `Work Done Last Week for ${username} (since ${dateFilter}):\n\n${workSummary}\n\nTotal Hours: ${totalHours}` } ] }; } case 'get_work_done_lastweek_bycustomer': { const username = args.username; logger.info(`Getting work hours by customer/site for last 7 days for user ${username}`); const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); const dateFilter = sevenDaysAgo.toISOString().split('T')[0]; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,SITEID,woactivity.ESTDUR,woactivity.OWNER&lean=1`; endpoint += `&oslc.where=REPORTDATE>="${dateFilter}" and woactivity.owner="${username}"`; endpoint += `&oslc.pageSize=500`; logger.debug(`Work hours by customer query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No work activities found for the last 7 days (since ${dateFilter}) for user ${username}` } ] }; } const workBySite = {}; response.member.forEach(ticket => { const siteId = ticket.siteid; if (!workBySite[siteId]) { workBySite[siteId] = 0; } const activities = ticket.woactivity || []; const activityArray = Array.isArray(activities) ? activities : [activities]; activityArray.forEach(activity => { if (activity && activity.owner === username && activity.estdur) { workBySite[siteId] += parseFloat(activity.estdur) || 0; } }); }); const workSummary = Object.entries(workBySite) .filter(([_, hours]) => hours > 0) .sort(([,a], [,b]) => b - a) .map(([siteId, hours]) => `${siteId} → ${hours} hours`) .join('\n'); const totalHours = Object.values(workBySite).reduce((sum, hours) => sum + hours, 0); return { content: [ { type: 'text', text: `Work Done Last Week by Customer for ${username} (since ${dateFilter}):\n\n${workSummary}\n\nTotal Hours: ${totalHours}` } ] }; } case 'get_tickets_daterange': { const username = args.username; logger.info(`Getting tickets for date range: ${args.start_date} to ${args.end_date} for user ${username}`); const datePattern = /^\d{4}-\d{2}-\d{2}$/; if (!datePattern.test(args.start_date) || !datePattern.test(args.end_date)) { return { content: [ { type: 'text', text: 'Error: Dates must be in YYYY-MM-DD format (e.g., 2025-06-01)' } ] }; } const startDate = new Date(args.start_date); const endDate = new Date(args.end_date); if (startDate > endDate) { return { content: [ { type: 'text', text: 'Error: Start date must be before or equal to end date' } ] }; } const limit = args.limit || 100; const includeWorkHours = args.include_work_hours || false; const statusFilter = args.status_filter || 'all'; const selectFields = includeWorkHours ? 'TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE,REPORTDATE,SITEID,woactivity.ESTDUR,woactivity.OWNER' : 'TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE,REPORTDATE,SITEID'; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=${selectFields}&lean=1`; let whereClause = `woactivity.owner="${username}" and REPORTDATE>="${args.start_date}" and REPORTDATE<="${args.end_date}"`; if (statusFilter === 'active') { whereClause += ` and status!="RESOLVED" and status!="CLOSED" and status!="CANCELLED"`; } else if (statusFilter !== 'all') { whereClause += ` and status="${statusFilter.toUpperCase()}"`; } endpoint += `&oslc.where=${encodeURIComponent(whereClause)}`; endpoint += `&oslc.pageSize=${limit}`; logger.debug(`Date range query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No tickets found between ${args.start_date} and ${args.end_date} for user ${username}` } ] }; } logger.info(`Retrieved ${response.member.length} tickets for date range for user ${username}`); if (includeWorkHours) { const ticketsWithHours = {}; let totalHours = 0; response.member.forEach(ticket => { const ticketId = ticket.ticketid; if (!ticketsWithHours[ticketId]) { ticketsWithHours[ticketId] = { description: ticket.description, status: ticket.status, priority: ticket.reportedpriority, site: ticket.siteid, reportDate: ticket.reportdate, hours: 0 }; } const activities = ticket.woactivity || []; const activityArray = Array.isArray(activities) ? activities : [activities]; activityArray.forEach(activity => { if (activity && activity.owner === username && activity.estdur) { const hours = parseFloat(activity.estdur) || 0; ticketsWithHours[ticketId].hours += hours; totalHours += hours; } }); }); const ticketSummary = Object.entries(ticketsWithHours) .map(([ticketId, data]) => `${ticketId} (${data.description.substring(0, 40)}...) → ${data.hours} hours\n` + ` Status: ${data.status} | Site: ${data.site} | Date: ${data.reportDate}` ) .join('\n\n'); return { content: [ { type: 'text', text: `Tickets from ${args.start_date} to ${args.end_date} with Work Hours for ${username}:\n\n${ticketSummary}\n\nTotal Hours: ${totalHours}\nTotal Tickets: ${Object.keys(ticketsWithHours).length}` } ] }; } else { const ticketSummary = response.member.map(ticket => ({ ticketId: ticket.ticketid, description: ticket.description, status: ticket.status, priority: ticket.reportedpriority, site: ticket.siteid, reportDate: ticket.reportdate })); return { content: [ { type: 'text', text: `Tickets from ${args.start_date} to ${args.end_date} for ${username} (${response.member.length} found):\n\n${JSON.stringify(ticketSummary, null, 2)}` } ] }; } } case 'get_tickets_by_work_activity_date': { logger.info(`Getting tickets by work activity date range: ${args.start_date} to ${args.end_date}`); const startDate = args.start_date; const endDate = args.end_date; const limit = args.limit || 100; const dateRegex = /^\d{4}-\d{2}-\d{2}$/; if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) { return { content: [ { type: 'text', text: 'Invalid date format. Please use YYYY-MM-DD format (e.g., 2025-06-15)' } ] }; } let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,SITEID,REPORTDATE,woactivity{OWNER,ACTSTART,DESCRIPTION,ESTDUR}&lean=1`; endpoint += `&oslc.where=woactivity.ACTSTART>="${startDate}T00:00:00" and woactivity.ACTSTART<="${endDate}T23:59:59" and woactivity.OWNER="VKRISHNA"`; if (limit) { endpoint += `&oslc.paging=true&oslc.pageSize=${limit}`; } logger.debug(`Query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); logger.debug('Raw API response:', JSON.stringify(response, null, 2)); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No tickets found with work activities between ${startDate} and ${endDate}` } ] }; } const tickets = response.member; logger.info(`Found ${tickets.length} tickets with work activities in date range`); if (tickets.length > 0) { logger.debug('First ticket structure:', JSON.stringify(tickets[0], null, 2)); } let result = `**Tickets with Work Activities: ${startDate} to ${endDate}**\n\n`; result += `Found ${tickets.length} tickets:\n\n`; tickets.forEach(ticket => { const ticketId = ticket.TICKETID || ticket.ticketid || ticket.TicketID || 'N/A'; const description = ticket.DESCRIPTION || ticket.description || ticket.Description || 'N/A'; const status = ticket.STATUS || ticket.status || ticket.Status || 'N/A'; const priority = ticket.REPORTEDPRIORITY || ticket.reportedpriority || ticket.ReportedPriority || 'N/A'; const siteId = ticket.SITEID || ticket.siteid || ticket.SiteID || 'N/A'; const workActivities = ticket.woactivity || ticket.WOACTIVITY || []; const relevantActivities = workActivities.filter(wa => wa.ACTSTART && wa.ACTSTART >= `${startDate}T00:00:00` && wa.ACTSTART <= `${endDate}T23:59:59` ); result += `**${ticketId}** - ${description}\n`; result += ` Site: ${siteId} | Status: ${status} | Priority: ${priority}\n`; if (relevantActivities.length > 0) { result += ` Work Activities:\n`; relevantActivities.forEach(wa => { const actDate = wa.ACTSTART ? new Date(wa.ACTSTART).toLocaleDateString() : 'N/A'; const hours = wa.ESTDUR || wa.estdur || 0; const waDesc = wa.DESCRIPTION || wa.description || 'No description'; result += ` • ${actDate}: ${waDesc} (${hours}h)\n`; }); } result += `\n`; }); return { content: [ { type: 'text', text: result } ] }; } case 'add_work_activity': { logger.info(`Adding work activity to ticket: ${args.ticket_id}`); const ticketEndpoint = `/maximo/oslc/os/MXSR?oslc.select=*&oslc.where=TICKETID="${args.ticket_id}"&lean=1`; logger.debug(`Getting ticket details: ${ticketEndpoint}`); const ticketResponse = await makeMaximoRequest(ticketEndpoint); if (!ticketResponse.member || ticketResponse.member.length === 0) { logger.warn(`Ticket ${args.ticket_id} not found`); return { content: [ { type: 'text', text: `Ticket ${args.ticket_id} not found. Please verify the ticket ID exists.` } ] }; } const ticket = ticketResponse.member[0]; const ticketHref = ticket.href; logger.info(`Found ticket ${args.ticket_id} in site ${ticket.siteid} with href: ${ticketHref}`); const now = new Date(); const reportDate = now.toISOString(); const actStart = new Date(now.getTime() - 5 * 60 * 1000).toISOString(); const workActivityPayload = { "woactivity": [ { "_action": "Add", "description": args.activity_description, "owner": "VKRISHNA", "estdur": parseFloat(args.hours), "actlabhrs": parseFloat(args.hours), "siteid": ticket.siteid || "BEDFORD", "orgid": ticket.orgid || "EAGLENA", "status": "INPRG", "reportdate": reportDate, "actstart": actStart, "woclass": "ACTIVITY", "worktype": "CM", "assignedownergroup": ticket.ownergroup || "", "parentwonum": args.ticket_id, "historyflag": false, "istask": false, "haschildren": false, "template": false } ] }; logger.debug('Work activity payload:', JSON.stringify(workActivityPayload, null, 2)); const resourcePath = ticketHref.replace(MAXIMO_HOST, ''); const updateEndpoint = `${resourcePath}?lean=1`; logger.debug(`Update endpoint: ${updateEndpoint}`); try { const response = await makeMaximoRequest(updateEndpoint, { method: 'POST', headers: { 'MAXAUTH': MAXAUTH, 'Content-Type': 'application/json', 'x-method-override': 'PATCH', 'patchtype': 'MERGE', 'properties': 'wonum,description,status' }, body: JSON.stringify(workActivityPayload) }); logger.info('Work activity update response received'); logger.debug('Update response:', JSON.stringify(response, null, 2)); const verifyEndpoint = `/maximo/oslc/os/MXSR?oslc.select=ticketid,woactivity{wonum,description,owner,estdur,status}&oslc.where=TICKETID="${args.ticket_id}"&lean=1`; logger.debug(`Verifying work activity creation: ${verifyEndpoint}`); const verifyResponse = await makeMaximoRequest(verifyEndpoint); if (verifyResponse && verifyResponse.member && verifyResponse.member.length > 0) { const updatedTicket = verifyResponse.member[0]; const workActivities = updatedTicket.woactivity || []; const latestActivity = workActivities.find(activity => activity.owner === "VKRISHNA" && activity.description === args.activity_description ); if (latestActivity) { logger.info(`Work activity successfully created with WONUM: ${latestActivity.wonum}`); const successMessage = `Work activity added successfully!\n\nSR Ticket: ${args.ticket_id}\nWork Activity: ${latestActivity.wonum}\nOwner: VKRISHNA\nHours: ${args.hours}\nDescription: ${args.activity_description}\nStatus: ${latestActivity.status}\nDate: ${new Date().toLocaleDateString()}`; return { content: [ { type: 'text', text: successMessage } ] }; } else { logger.warn('Work activity was not found in verification check'); return { content: [ { type: 'text', text: `Work activity may have been added to ticket ${args.ticket_id}, but verification failed. Please check the ticket manually.` } ] }; } } else { logger.error('Failed to verify work activity creation'); return { content: [ { type: 'text', text: `Failed to verify work activity creation for ticket ${args.ticket_id}. Please check the ticket manually.` } ] ]; } } catch (error) { logger.error('Error adding work activity:', error); let errorMessage = `Failed to add work activity to ticket ${args.ticket_id}.`; if (error.message) { errorMessage += `\n\nError details: ${error.message}`; } if (error.response) { errorMessage += `\nHTTP Status: ${error.response.status}`; } errorMessage += `\n\nPlease check:\n• Ticket ${args.ticket_id} exists and is accessible\n• You have permission to add work activities\n• The ticket is in a status that allows work activities`; return { content: [ { type: 'text', text: errorMessage } ] }; } } default: logger.error(`Unknown tool: ${name}`); throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { logger.error(`Error in tool ${name}:`, error); return { content: [ { type: 'text', text: `Error: ${error.message}\n\nFull error details logged to maximo-mcp-sse.log` } ], isError: true }; } }); // Create Express app for HTTP/SSE const app = express(); // Enable CORS for cross-origin requests app.use(cors({ origin: '*', // In production, specify your Claude Desktop origin credentials: true })); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'healthy', server: 'maximo-mcp-sse', version: '2.0.0', timestamp: new Date().toISOString() }); }); // SSE endpoint for MCP communication app.get('/sse', async (req, res) => { logger.info('New SSE connection established'); // Set up SSE headers res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type', }); try { // Create SSE transport const transport = new SSEServerTransport('/sse', res); // Connect MCP server to SSE transport await server.connect(transport); logger.info('MCP Server connected via SSE transport'); } catch (error) { logger.error('Failed to establish SSE connection:', error); res.write(`event: error\ndata: ${JSON.stringify({ error: error.message })}\n\n`); res.end(); } }); // Start the HTTP server async function startServer() { try { const server = app.listen(SERVER_PORT, SERVER_HOST, () => { logger.info(`Maximo MCP SSE Server running on http://${SERVER_HOST}:${SERVER_PORT}`); logger.info(`SSE endpoint available at: http://${SERVER_HOST}:${SERVER_PORT}/sse`); logger.info(`Health check available at: http://${SERVER_HOST}:${SERVER_PORT}/health`); }); // Graceful shutdown handling process.on('SIGTERM', () => { logger.info('SIGTERM received, shutting down gracefully'); server.close(() => { logger.info('Server closed'); process.exit(0); }); }); process.on('SIGINT', () => { logger.info('SIGINT received, shutting down gracefully'); server.close(() => { logger.info('Server closed'); process.exit(0); }); }); } catch (error) { logger.error('Failed to start server:', error); process.exit(1); } } startServer();, required: true }, end_date: { type: 'string', description: 'End date in YYYY-MM-DD format (e.g., 2025-06-12)', pattern: '^\\d{4}-\\d{2}-\\d{2} ] }; }); // Tool request handler (keeping all the original tool logic) server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; logger.info(`Tool called via SSE: ${name}`, args); try { switch (name) { case 'query_tickets': { logger.info('Querying tickets for user VKRISHNA'); const statusFilter = args.status_filter || 'active'; const limit = args.limit || 50; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE&lean=1`; if (statusFilter === 'active') { endpoint += `&oslc.where=woactivity.owner="VKRISHNA" and status!="RESOLVED" and status!="CLOSED" and status!="CANCELLED"`; } else if (statusFilter !== 'all') { endpoint += `&oslc.where=woactivity.owner="VKRISHNA" and status="${statusFilter.toUpperCase()}"`; } else { endpoint += `&oslc.where=woactivity.owner="VKRISHNA"`; } endpoint += `&oslc.pageSize=${limit}`; logger.debug(`Query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); logger.info(`Retrieved ${response.member ? response.member.length : 0} tickets`); return { content: [ { type: 'text', text: JSON.stringify(response, null, 2) } ] }; } // ... (include all other case statements from original - query_tickets_by_site, create_ticket, etc.) // I'm including the create_ticket case as an example: case 'create_ticket': { logger.info('Creating new ticket via SSE', { siteid: args.siteid, title: args.title, priority: args.priority, is_production: args.is_production, sr_type: args.sr_type }); const mappedSiteId = SITE_MAPPINGS[args.siteid] || args.siteid; logger.info(`Mapped site ID: ${args.siteid} -> ${mappedSiteId}`); const ticketData = { "_action": "AddChange", "siteid": mappedSiteId, "description": args.title, "description_longdescription": args.description, "reportedby": "VKRISHNA", "affectedperson": "VKRISHNA", "reportedemail": "vijay.krishna@zprosolutions.com", "reportedpriority": args.priority, "zp_production": args.is_production ? "YES" : "NO", "zp_srtype": args.sr_type }; logger.debug('Ticket payload:', ticketData); const endpoint = `/maximo/oslc/os/MXSR?lean=1&ignorecollectionref=1&ignorekeyref=1&ignorers=1&mxlaction=addchange`; const response = await makeMaximoRequest(endpoint, { method: 'POST', headers: { 'MAXAUTH': MAXAUTH, 'x-method-override': 'BULK' }, body: JSON.stringify([ticketData]) }); // ... (rest of create_ticket logic same as original) let ticketNumber = 'Unknown'; if (response && Array.isArray(response) && response.length > 0) { const responseItem = response[0]; if (responseItem.TICKETID || responseItem.ticketid) { ticketNumber = responseItem.TICKETID || responseItem.ticketid; } else if (responseItem._responsemeta && responseItem._responsemeta.Location) { try { const locationUrl = responseItem._responsemeta.Location; logger.debug(`Extracting ticket ID from Location: ${locationUrl}`); const encodedPart = locationUrl.split('/').pop(); let cleanEncoded = encodedPart.replace(/^_+/, '').replace(/-+$/, ''); const standardBase64 = cleanEncoded.replace(/_/g, '/').replace(/-/g, '+'); let paddedBase64 = standardBase64; while (paddedBase64.length % 4) { paddedBase64 += '='; } const decoded = Buffer.from(paddedBase64, 'base64').toString('utf-8'); const ticketMatch = decoded.match(/SR\/(\d+)/); if (ticketMatch && ticketMatch[1]) { ticketNumber = ticketMatch[1]; logger.info(`Successfully extracted ticket ID: ${ticketNumber}`); } } catch (decodeError) { logger.warn('Failed to decode ticket ID from Location URL:', decodeError); } } } const successMessage = ticketNumber !== 'Unknown' ? `Ticket created successfully!\n\nTicket ID: ${ticketNumber}\nSite: ${mappedSiteId}\nPriority: ${args.priority}\nType: ${args.sr_type}\nProduction: ${args.is_production ? 'YES' : 'NO'}\n\nTitle: ${args.title}` : `Ticket created but ID could not be extracted.\n\nPlease check the logs and query recent tickets to find the new ticket.\n\nFull Response:\n${JSON.stringify(response, null, 2)}`; return { content: [ { type: 'text', text: successMessage } ] }; } case 'query_tickets_by_site': { logger.info('Querying tickets by site and/or status', { site_id: args.site_id, status_values: args.status_values, limit: args.limit }); const limit = args.limit || 50; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE,SITEID&lean=1`; let whereConditions = ['woactivity.owner="VKRISHNA"']; if (args.site_id) { whereConditions.push(`siteid="${args.site_id}"`); } if (args.status_values && Array.isArray(args.status_values) && args.status_values.length > 0) { const statusConditions = args.status_values.map(status => `status="${status.toUpperCase()}"`); if (statusConditions.length === 1) { whereConditions.push(statusConditions[0]); } else { whereConditions.push(`(${statusConditions.join(' or ')})`); } } const whereClause = whereConditions.join(' and '); endpoint += `&oslc.where=${encodeURIComponent(whereClause)}`; endpoint += `&oslc.pageSize=${limit}`; logger.debug(`Site/Status query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); logger.info(`Retrieved ${response.member ? response.member.length : 0} tickets matching criteria`); if (!response.member || response.member.length === 0) { const filterDescription = []; if (args.site_id) filterDescription.push(`Site: ${args.site_id}`); if (args.status_values) filterDescription.push(`Status: ${args.status_values.join(', ')}`); return { content: [ { type: 'text', text: `No tickets found matching criteria: ${filterDescription.join(', ')}` } ] }; } const ticketSummary = response.member.map(ticket => ({ ticketId: ticket.ticketid, description: ticket.description, status: ticket.status, priority: ticket.reportedpriority, site: ticket.siteid, production: ticket.zp_production, srType: ticket.zp_srtype })); const filterDescription = []; if (args.site_id) filterDescription.push(`Site: ${args.site_id}`); if (args.status_values) filterDescription.push(`Status: ${args.status_values.join(', ')}`); return { content: [ { type: 'text', text: `Tickets matching criteria (${filterDescription.join(', ')}): ${response.member.length} found\n\n${JSON.stringify(ticketSummary, null, 2)}` } ] }; } case 'get_ticket_details': { logger.info(`Getting details for ticket: ${args.ticket_id}`); const endpoint = `/maximo/oslc/os/MXSR?oslc.select=*&oslc.where=TICKETID="${args.ticket_id}"&lean=1`; const response = await makeMaximoRequest(endpoint); if (!response.member || response.member.length === 0) { logger.warn(`Ticket ${args.ticket_id} not found`); return { content: [ { type: 'text', text: `Ticket ${args.ticket_id} not found. Please verify the ticket ID and ensure you have access to this ticket.` } ] }; } logger.info(`Retrieved details for ticket ${args.ticket_id}`); const ticketData = response.member[0]; const formattedResponse = { ticketId: ticketData.ticketid || 'Unknown', status: ticketData.status || 'Unknown', description: ticketData.description || 'No description', longDescription: ticketData.description_longdescription || 'No detailed description', priority: ticketData.reportedpriority || 'Unknown', siteId: ticketData.siteid || 'Unknown', production: ticketData.zp_production || 'Unknown', srType: ticketData.zp_srtype || 'Unknown', creationDate: ticketData.creationdate || 'Unknown', reportedBy: ticketData.reportedby || 'Unknown', affectedPerson: ticketData.affectedperson || 'Unknown', fullDetails: response }; return { content: [ { type: 'text', text: `Ticket Details:\n\n${JSON.stringify(formattedResponse, null, 2)}` } ] }; } case 'get_lastweek_tickets': { logger.info('Getting tickets from last 7 days'); const limit = args.limit || 100; const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); const dateFilter = sevenDaysAgo.toISOString().split('T')[0]; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE,REPORTDATE,SITEID&lean=1`; endpoint += `&oslc.where=woactivity.owner="VKRISHNA" and REPORTDATE>="${dateFilter}"`; endpoint += `&oslc.pageSize=${limit}`; logger.debug(`Last week query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); logger.info(`Retrieved ${response.member ? response.member.length : 0} tickets from last 7 days`); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No tickets found in the last 7 days (since ${dateFilter})` } ] }; } const ticketSummary = response.member.map(ticket => ({ ticketId: ticket.ticketid, description: ticket.description, status: ticket.status, priority: ticket.reportedpriority, site: ticket.siteid, reportDate: ticket.reportdate })); return { content: [ { type: 'text', text: `Last Week's Tickets (${response.member.length} found since ${dateFilter}):\n\n${JSON.stringify(ticketSummary, null, 2)}` } ] }; } case 'get_work_done_lastweek': { logger.info('Getting work hours by ticket for last 7 days'); const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); const dateFilter = sevenDaysAgo.toISOString().split('T')[0]; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,SITEID,woactivity.ESTDUR,woactivity.OWNER&lean=1`; endpoint += `&oslc.where=REPORTDATE>="${dateFilter}" and woactivity.owner="VKRISHNA"`; endpoint += `&oslc.pageSize=500`; logger.debug(`Work hours query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No work activities found for the last 7 days (since ${dateFilter})` } ] }; } const workByTicket = {}; response.member.forEach(ticket => { const ticketId = ticket.ticketid; const description = ticket.description; if (!workByTicket[ticketId]) { workByTicket[ticketId] = { description: description, totalHours: 0 }; } const activities = ticket.woactivity || []; const activityArray = Array.isArray(activities) ? activities : [activities]; activityArray.forEach(activity => { if (activity && activity.owner === "VKRISHNA" && activity.estdur) { workByTicket[ticketId].totalHours += parseFloat(activity.estdur) || 0; } }); }); const workSummary = Object.entries(workByTicket) .filter(([_, data]) => data.totalHours > 0) .map(([ticketId, data]) => `${ticketId} (${data.description.substring(0, 50)}...) → ${data.totalHours} hours`) .join('\n'); const totalHours = Object.values(workByTicket).reduce((sum, data) => sum + data.totalHours, 0); return { content: [ { type: 'text', text: `Work Done Last Week (since ${dateFilter}):\n\n${workSummary}\n\nTotal Hours: ${totalHours}` } ] }; } case 'get_work_done_lastweek_bycustomer': { logger.info('Getting work hours by customer/site for last 7 days'); const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); const dateFilter = sevenDaysAgo.toISOString().split('T')[0]; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,SITEID,woactivity.ESTDUR,woactivity.OWNER&lean=1`; endpoint += `&oslc.where=REPORTDATE>="${dateFilter}" and woactivity.owner="VKRISHNA"`; endpoint += `&oslc.pageSize=500`; logger.debug(`Work hours by customer query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No work activities found for the last 7 days (since ${dateFilter})` } ] }; } const workBySite = {}; response.member.forEach(ticket => { const siteId = ticket.siteid; if (!workBySite[siteId]) { workBySite[siteId] = 0; } const activities = ticket.woactivity || []; const activityArray = Array.isArray(activities) ? activities : [activities]; activityArray.forEach(activity => { if (activity && activity.owner === "VKRISHNA" && activity.estdur) { workBySite[siteId] += parseFloat(activity.estdur) || 0; } }); }); const workSummary = Object.entries(workBySite) .filter(([_, hours]) => hours > 0) .sort(([,a], [,b]) => b - a) .map(([siteId, hours]) => `${siteId} → ${hours} hours`) .join('\n'); const totalHours = Object.values(workBySite).reduce((sum, hours) => sum + hours, 0); return { content: [ { type: 'text', text: `Work Done Last Week by Customer (since ${dateFilter}):\n\n${workSummary}\n\nTotal Hours: ${totalHours}` } ] }; } case 'get_tickets_daterange': { logger.info(`Getting tickets for date range: ${args.start_date} to ${args.end_date}`); const datePattern = /^\d{4}-\d{2}-\d{2}$/; if (!datePattern.test(args.start_date) || !datePattern.test(args.end_date)) { return { content: [ { type: 'text', text: 'Error: Dates must be in YYYY-MM-DD format (e.g., 2025-06-01)' } ] }; } const startDate = new Date(args.start_date); const endDate = new Date(args.end_date); if (startDate > endDate) { return { content: [ { type: 'text', text: 'Error: Start date must be before or equal to end date' } ] }; } const limit = args.limit || 100; const includeWorkHours = args.include_work_hours || false; const statusFilter = args.status_filter || 'all'; const selectFields = includeWorkHours ? 'TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE,REPORTDATE,SITEID,woactivity.ESTDUR,woactivity.OWNER' : 'TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE,REPORTDATE,SITEID'; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=${selectFields}&lean=1`; let whereClause = `woactivity.owner="VKRISHNA" and REPORTDATE>="${args.start_date}" and REPORTDATE<="${args.end_date}"`; if (statusFilter === 'active') { whereClause += ` and status!="RESOLVED" and status!="CLOSED" and status!="CANCELLED"`; } else if (statusFilter !== 'all') { whereClause += ` and status="${statusFilter.toUpperCase()}"`; } endpoint += `&oslc.where=${encodeURIComponent(whereClause)}`; endpoint += `&oslc.pageSize=${limit}`; logger.debug(`Date range query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No tickets found between ${args.start_date} and ${args.end_date}` } ] }; } logger.info(`Retrieved ${response.member.length} tickets for date range`); if (includeWorkHours) { const ticketsWithHours = {}; let totalHours = 0; response.member.forEach(ticket => { const ticketId = ticket.ticketid; if (!ticketsWithHours[ticketId]) { ticketsWithHours[ticketId] = { description: ticket.description, status: ticket.status, priority: ticket.reportedpriority, site: ticket.siteid, reportDate: ticket.reportdate, hours: 0 }; } const activities = ticket.woactivity || []; const activityArray = Array.isArray(activities) ? activities : [activities]; activityArray.forEach(activity => { if (activity && activity.owner === "VKRISHNA" && activity.estdur) { const hours = parseFloat(activity.estdur) || 0; ticketsWithHours[ticketId].hours += hours; totalHours += hours; } }); }); const ticketSummary = Object.entries(ticketsWithHours) .map(([ticketId, data]) => `${ticketId} (${data.description.substring(0, 40)}...) → ${data.hours} hours\n` + ` Status: ${data.status} | Site: ${data.site} | Date: ${data.reportDate}` ) .join('\n\n'); return { content: [ { type: 'text', text: `Tickets from ${args.start_date} to ${args.end_date} with Work Hours:\n\n${ticketSummary}\n\nTotal Hours: ${totalHours}\nTotal Tickets: ${Object.keys(ticketsWithHours).length}` } ] }; } else { const ticketSummary = response.member.map(ticket => ({ ticketId: ticket.ticketid, description: ticket.description, status: ticket.status, priority: ticket.reportedpriority, site: ticket.siteid, reportDate: ticket.reportdate })); return { content: [ { type: 'text', text: `Tickets from ${args.start_date} to ${args.end_date} (${response.member.length} found):\n\n${JSON.stringify(ticketSummary, null, 2)}` } ] }; } } case 'get_tickets_by_work_activity_date': { logger.info(`Getting tickets by work activity date range: ${args.start_date} to ${args.end_date}`); const startDate = args.start_date; const endDate = args.end_date; const limit = args.limit || 100; const dateRegex = /^\d{4}-\d{2}-\d{2}$/; if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) { return { content: [ { type: 'text', text: 'Invalid date format. Please use YYYY-MM-DD format (e.g., 2025-06-15)' } ] }; } let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,SITEID,REPORTDATE,woactivity{OWNER,ACTSTART,DESCRIPTION,ESTDUR}&lean=1`; endpoint += `&oslc.where=woactivity.ACTSTART>="${startDate}T00:00:00" and woactivity.ACTSTART<="${endDate}T23:59:59" and woactivity.OWNER="VKRISHNA"`; if (limit) { endpoint += `&oslc.paging=true&oslc.pageSize=${limit}`; } logger.debug(`Query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); logger.debug('Raw API response:', JSON.stringify(response, null, 2)); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No tickets found with work activities between ${startDate} and ${endDate}` } ] }; } const tickets = response.member; logger.info(`Found ${tickets.length} tickets with work activities in date range`); if (tickets.length > 0) { logger.debug('First ticket structure:', JSON.stringify(tickets[0], null, 2)); } let result = `**Tickets with Work Activities: ${startDate} to ${endDate}**\n\n`; result += `Found ${tickets.length} tickets:\n\n`; tickets.forEach(ticket => { const ticketId = ticket.TICKETID || ticket.ticketid || ticket.TicketID || 'N/A'; const description = ticket.DESCRIPTION || ticket.description || ticket.Description || 'N/A'; const status = ticket.STATUS || ticket.status || ticket.Status || 'N/A'; const priority = ticket.REPORTEDPRIORITY || ticket.reportedpriority || ticket.ReportedPriority || 'N/A'; const siteId = ticket.SITEID || ticket.siteid || ticket.SiteID || 'N/A'; const workActivities = ticket.woactivity || ticket.WOACTIVITY || []; const relevantActivities = workActivities.filter(wa => wa.ACTSTART && wa.ACTSTART >= `${startDate}T00:00:00` && wa.ACTSTART <= `${endDate}T23:59:59` ); result += `**${ticketId}** - ${description}\n`; result += ` Site: ${siteId} | Status: ${status} | Priority: ${priority}\n`; if (relevantActivities.length > 0) { result += ` Work Activities:\n`; relevantActivities.forEach(wa => { const actDate = wa.ACTSTART ? new Date(wa.ACTSTART).toLocaleDateString() : 'N/A'; const hours = wa.ESTDUR || wa.estdur || 0; const waDesc = wa.DESCRIPTION || wa.description || 'No description'; result += ` • ${actDate}: ${waDesc} (${hours}h)\n`; }); } result += `\n`; }); return { content: [ { type: 'text', text: result } ] }; } case 'add_work_activity': { logger.info(`Adding work activity to ticket: ${args.ticket_id}`); const ticketEndpoint = `/maximo/oslc/os/MXSR?oslc.select=*&oslc.where=TICKETID="${args.ticket_id}"&lean=1`; logger.debug(`Getting ticket details: ${ticketEndpoint}`); const ticketResponse = await makeMaximoRequest(ticketEndpoint); if (!ticketResponse.member || ticketResponse.member.length === 0) { logger.warn(`Ticket ${args.ticket_id} not found`); return { content: [ { type: 'text', text: `Ticket ${args.ticket_id} not found. Please verify the ticket ID exists.` } ] }; } const ticket = ticketResponse.member[0]; const ticketHref = ticket.href; logger.info(`Found ticket ${args.ticket_id} in site ${ticket.siteid} with href: ${ticketHref}`); const now = new Date(); const reportDate = now.toISOString(); const actStart = new Date(now.getTime() - 5 * 60 * 1000).toISOString(); const workActivityPayload = { "woactivity": [ { "_action": "Add", "description": args.activity_description, "owner": "VKRISHNA", "estdur": parseFloat(args.hours), "actlabhrs": parseFloat(args.hours), "siteid": ticket.siteid || "BEDFORD", "orgid": ticket.orgid || "EAGLENA", "status": "INPRG", "reportdate": reportDate, "actstart": actStart, "woclass": "ACTIVITY", "worktype": "CM", "assignedownergroup": ticket.ownergroup || "", "parentwonum": args.ticket_id, "historyflag": false, "istask": false, "haschildren": false, "template": false } ] }; logger.debug('Work activity payload:', JSON.stringify(workActivityPayload, null, 2)); const resourcePath = ticketHref.replace(MAXIMO_HOST, ''); const updateEndpoint = `${resourcePath}?lean=1`; logger.debug(`Update endpoint: ${updateEndpoint}`); try { const response = await makeMaximoRequest(updateEndpoint, { method: 'POST', headers: { 'MAXAUTH': MAXAUTH, 'Content-Type': 'application/json', 'x-method-override': 'PATCH', 'patchtype': 'MERGE', 'properties': 'wonum,description,status' }, body: JSON.stringify(workActivityPayload) }); logger.info('Work activity update response received'); logger.debug('Update response:', JSON.stringify(response, null, 2)); const verifyEndpoint = `/maximo/oslc/os/MXSR?oslc.select=ticketid,woactivity{wonum,description,owner,estdur,status}&oslc.where=TICKETID="${args.ticket_id}"&lean=1`; logger.debug(`Verifying work activity creation: ${verifyEndpoint}`); const verifyResponse = await makeMaximoRequest(verifyEndpoint); if (verifyResponse && verifyResponse.member && verifyResponse.member.length > 0) { const updatedTicket = verifyResponse.member[0]; const workActivities = updatedTicket.woactivity || []; const latestActivity = workActivities.find(activity => activity.owner === "VKRISHNA" && activity.description === args.activity_description ); if (latestActivity) { logger.info(`Work activity successfully created with WONUM: ${latestActivity.wonum}`); const successMessage = `Work activity added successfully!\n\nSR Ticket: ${args.ticket_id}\nWork Activity: ${latestActivity.wonum}\nOwner: VKRISHNA\nHours: ${args.hours}\nDescription: ${args.activity_description}\nStatus: ${latestActivity.status}\nDate: ${new Date().toLocaleDateString()}`; return { content: [ { type: 'text', text: successMessage } ] }; } else { logger.warn('Work activity was not found in verification check'); return { content: [ { type: 'text', text: `Work activity may have been added to ticket ${args.ticket_id}, but verification failed. Please check the ticket manually.` } ] }; } } else { logger.error('Failed to verify work activity creation'); return { content: [ { type: 'text', text: `Failed to verify work activity creation for ticket ${args.ticket_id}. Please check the ticket manually.` } ] ]; } } catch (error) { logger.error('Error adding work activity:', error); let errorMessage = `Failed to add work activity to ticket ${args.ticket_id}.`; if (error.message) { errorMessage += `\n\nError details: ${error.message}`; } if (error.response) { errorMessage += `\nHTTP Status: ${error.response.status}`; } errorMessage += `\n\nPlease check:\n• Ticket ${args.ticket_id} exists and is accessible\n• You have permission to add work activities\n• The ticket is in a status that allows work activities`; return { content: [ { type: 'text', text: errorMessage } ] }; } } default: logger.error(`Unknown tool: ${name}`); throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { logger.error(`Error in tool ${name}:`, error); return { content: [ { type: 'text', text: `Error: ${error.message}\n\nFull error details logged to maximo-mcp-sse.log` } ], isError: true }; } }); // Create Express app for HTTP/SSE const app = express(); // Enable CORS for cross-origin requests app.use(cors({ origin: '*', // In production, specify your Claude Desktop origin credentials: true })); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'healthy', server: 'maximo-mcp-sse', version: '2.0.0', timestamp: new Date().toISOString() }); }); // SSE endpoint for MCP communication app.get('/sse', async (req, res) => { logger.info('New SSE connection established'); // Set up SSE headers res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type', }); try { // Create SSE transport const transport = new SSEServerTransport('/sse', res); // Connect MCP server to SSE transport await server.connect(transport); logger.info('MCP Server connected via SSE transport'); } catch (error) { logger.error('Failed to establish SSE connection:', error); res.write(`event: error\ndata: ${JSON.stringify({ error: error.message })}\n\n`); res.end(); } }); // Start the HTTP server async function startServer() { try { const server = app.listen(SERVER_PORT, SERVER_HOST, () => { logger.info(`Maximo MCP SSE Server running on http://${SERVER_HOST}:${SERVER_PORT}`); logger.info(`SSE endpoint available at: http://${SERVER_HOST}:${SERVER_PORT}/sse`); logger.info(`Health check available at: http://${SERVER_HOST}:${SERVER_PORT}/health`); }); // Graceful shutdown handling process.on('SIGTERM', () => { logger.info('SIGTERM received, shutting down gracefully'); server.close(() => { logger.info('Server closed'); process.exit(0); }); }); process.on('SIGINT', () => { logger.info('SIGINT received, shutting down gracefully'); server.close(() => { logger.info('Server closed'); process.exit(0); }); }); } catch (error) { logger.error('Failed to start server:', error); process.exit(1); } } startServer();, required: true }, include_work_hours: { type: 'boolean', description: 'Include work hours summary for each ticket (default: false)', default: false }, status_filter: { type: 'string', description: 'Filter by status (default: all statuses)', enum: ['all', 'active', 'resolved', 'closed', 'queued'] }, limit: { type: 'number', description: 'Maximum number of tickets to return (default: 100)', minimum: 1, maximum: 500 } }, required: ['username', 'start_date', 'end_date'] } }, { name: 'get_tickets_by_work_activity_date', description: 'Get tickets based on when work activities were performed (WOACTIVITY.ACTSTART) for specified user', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'Maximo username to query work activities for (e.g., VKRISHNA)', required: true }, start_date: { type: 'string', description: 'Start date in YYYY-MM-DD format (e.g., 2025-06-15)', pattern: '^\\d{4}-\\d{2}-\\d{2} ] }; }); // Tool request handler (keeping all the original tool logic) server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; logger.info(`Tool called via SSE: ${name}`, args); try { switch (name) { case 'query_tickets': { logger.info('Querying tickets for user VKRISHNA'); const statusFilter = args.status_filter || 'active'; const limit = args.limit || 50; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE&lean=1`; if (statusFilter === 'active') { endpoint += `&oslc.where=woactivity.owner="VKRISHNA" and status!="RESOLVED" and status!="CLOSED" and status!="CANCELLED"`; } else if (statusFilter !== 'all') { endpoint += `&oslc.where=woactivity.owner="VKRISHNA" and status="${statusFilter.toUpperCase()}"`; } else { endpoint += `&oslc.where=woactivity.owner="VKRISHNA"`; } endpoint += `&oslc.pageSize=${limit}`; logger.debug(`Query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); logger.info(`Retrieved ${response.member ? response.member.length : 0} tickets`); return { content: [ { type: 'text', text: JSON.stringify(response, null, 2) } ] }; } // ... (include all other case statements from original - query_tickets_by_site, create_ticket, etc.) // I'm including the create_ticket case as an example: case 'create_ticket': { logger.info('Creating new ticket via SSE', { siteid: args.siteid, title: args.title, priority: args.priority, is_production: args.is_production, sr_type: args.sr_type }); const mappedSiteId = SITE_MAPPINGS[args.siteid] || args.siteid; logger.info(`Mapped site ID: ${args.siteid} -> ${mappedSiteId}`); const ticketData = { "_action": "AddChange", "siteid": mappedSiteId, "description": args.title, "description_longdescription": args.description, "reportedby": "VKRISHNA", "affectedperson": "VKRISHNA", "reportedemail": "vijay.krishna@zprosolutions.com", "reportedpriority": args.priority, "zp_production": args.is_production ? "YES" : "NO", "zp_srtype": args.sr_type }; logger.debug('Ticket payload:', ticketData); const endpoint = `/maximo/oslc/os/MXSR?lean=1&ignorecollectionref=1&ignorekeyref=1&ignorers=1&mxlaction=addchange`; const response = await makeMaximoRequest(endpoint, { method: 'POST', headers: { 'MAXAUTH': MAXAUTH, 'x-method-override': 'BULK' }, body: JSON.stringify([ticketData]) }); // ... (rest of create_ticket logic same as original) let ticketNumber = 'Unknown'; if (response && Array.isArray(response) && response.length > 0) { const responseItem = response[0]; if (responseItem.TICKETID || responseItem.ticketid) { ticketNumber = responseItem.TICKETID || responseItem.ticketid; } else if (responseItem._responsemeta && responseItem._responsemeta.Location) { try { const locationUrl = responseItem._responsemeta.Location; logger.debug(`Extracting ticket ID from Location: ${locationUrl}`); const encodedPart = locationUrl.split('/').pop(); let cleanEncoded = encodedPart.replace(/^_+/, '').replace(/-+$/, ''); const standardBase64 = cleanEncoded.replace(/_/g, '/').replace(/-/g, '+'); let paddedBase64 = standardBase64; while (paddedBase64.length % 4) { paddedBase64 += '='; } const decoded = Buffer.from(paddedBase64, 'base64').toString('utf-8'); const ticketMatch = decoded.match(/SR\/(\d+)/); if (ticketMatch && ticketMatch[1]) { ticketNumber = ticketMatch[1]; logger.info(`Successfully extracted ticket ID: ${ticketNumber}`); } } catch (decodeError) { logger.warn('Failed to decode ticket ID from Location URL:', decodeError); } } } const successMessage = ticketNumber !== 'Unknown' ? `Ticket created successfully!\n\nTicket ID: ${ticketNumber}\nSite: ${mappedSiteId}\nPriority: ${args.priority}\nType: ${args.sr_type}\nProduction: ${args.is_production ? 'YES' : 'NO'}\n\nTitle: ${args.title}` : `Ticket created but ID could not be extracted.\n\nPlease check the logs and query recent tickets to find the new ticket.\n\nFull Response:\n${JSON.stringify(response, null, 2)}`; return { content: [ { type: 'text', text: successMessage } ] }; } case 'query_tickets_by_site': { logger.info('Querying tickets by site and/or status', { site_id: args.site_id, status_values: args.status_values, limit: args.limit }); const limit = args.limit || 50; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE,SITEID&lean=1`; let whereConditions = ['woactivity.owner="VKRISHNA"']; if (args.site_id) { whereConditions.push(`siteid="${args.site_id}"`); } if (args.status_values && Array.isArray(args.status_values) && args.status_values.length > 0) { const statusConditions = args.status_values.map(status => `status="${status.toUpperCase()}"`); if (statusConditions.length === 1) { whereConditions.push(statusConditions[0]); } else { whereConditions.push(`(${statusConditions.join(' or ')})`); } } const whereClause = whereConditions.join(' and '); endpoint += `&oslc.where=${encodeURIComponent(whereClause)}`; endpoint += `&oslc.pageSize=${limit}`; logger.debug(`Site/Status query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); logger.info(`Retrieved ${response.member ? response.member.length : 0} tickets matching criteria`); if (!response.member || response.member.length === 0) { const filterDescription = []; if (args.site_id) filterDescription.push(`Site: ${args.site_id}`); if (args.status_values) filterDescription.push(`Status: ${args.status_values.join(', ')}`); return { content: [ { type: 'text', text: `No tickets found matching criteria: ${filterDescription.join(', ')}` } ] }; } const ticketSummary = response.member.map(ticket => ({ ticketId: ticket.ticketid, description: ticket.description, status: ticket.status, priority: ticket.reportedpriority, site: ticket.siteid, production: ticket.zp_production, srType: ticket.zp_srtype })); const filterDescription = []; if (args.site_id) filterDescription.push(`Site: ${args.site_id}`); if (args.status_values) filterDescription.push(`Status: ${args.status_values.join(', ')}`); return { content: [ { type: 'text', text: `Tickets matching criteria (${filterDescription.join(', ')}): ${response.member.length} found\n\n${JSON.stringify(ticketSummary, null, 2)}` } ] }; } case 'get_ticket_details': { logger.info(`Getting details for ticket: ${args.ticket_id}`); const endpoint = `/maximo/oslc/os/MXSR?oslc.select=*&oslc.where=TICKETID="${args.ticket_id}"&lean=1`; const response = await makeMaximoRequest(endpoint); if (!response.member || response.member.length === 0) { logger.warn(`Ticket ${args.ticket_id} not found`); return { content: [ { type: 'text', text: `Ticket ${args.ticket_id} not found. Please verify the ticket ID and ensure you have access to this ticket.` } ] }; } logger.info(`Retrieved details for ticket ${args.ticket_id}`); const ticketData = response.member[0]; const formattedResponse = { ticketId: ticketData.ticketid || 'Unknown', status: ticketData.status || 'Unknown', description: ticketData.description || 'No description', longDescription: ticketData.description_longdescription || 'No detailed description', priority: ticketData.reportedpriority || 'Unknown', siteId: ticketData.siteid || 'Unknown', production: ticketData.zp_production || 'Unknown', srType: ticketData.zp_srtype || 'Unknown', creationDate: ticketData.creationdate || 'Unknown', reportedBy: ticketData.reportedby || 'Unknown', affectedPerson: ticketData.affectedperson || 'Unknown', fullDetails: response }; return { content: [ { type: 'text', text: `Ticket Details:\n\n${JSON.stringify(formattedResponse, null, 2)}` } ] }; } case 'get_lastweek_tickets': { logger.info('Getting tickets from last 7 days'); const limit = args.limit || 100; const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); const dateFilter = sevenDaysAgo.toISOString().split('T')[0]; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE,REPORTDATE,SITEID&lean=1`; endpoint += `&oslc.where=woactivity.owner="VKRISHNA" and REPORTDATE>="${dateFilter}"`; endpoint += `&oslc.pageSize=${limit}`; logger.debug(`Last week query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); logger.info(`Retrieved ${response.member ? response.member.length : 0} tickets from last 7 days`); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No tickets found in the last 7 days (since ${dateFilter})` } ] }; } const ticketSummary = response.member.map(ticket => ({ ticketId: ticket.ticketid, description: ticket.description, status: ticket.status, priority: ticket.reportedpriority, site: ticket.siteid, reportDate: ticket.reportdate })); return { content: [ { type: 'text', text: `Last Week's Tickets (${response.member.length} found since ${dateFilter}):\n\n${JSON.stringify(ticketSummary, null, 2)}` } ] }; } case 'get_work_done_lastweek': { logger.info('Getting work hours by ticket for last 7 days'); const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); const dateFilter = sevenDaysAgo.toISOString().split('T')[0]; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,SITEID,woactivity.ESTDUR,woactivity.OWNER&lean=1`; endpoint += `&oslc.where=REPORTDATE>="${dateFilter}" and woactivity.owner="VKRISHNA"`; endpoint += `&oslc.pageSize=500`; logger.debug(`Work hours query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No work activities found for the last 7 days (since ${dateFilter})` } ] }; } const workByTicket = {}; response.member.forEach(ticket => { const ticketId = ticket.ticketid; const description = ticket.description; if (!workByTicket[ticketId]) { workByTicket[ticketId] = { description: description, totalHours: 0 }; } const activities = ticket.woactivity || []; const activityArray = Array.isArray(activities) ? activities : [activities]; activityArray.forEach(activity => { if (activity && activity.owner === "VKRISHNA" && activity.estdur) { workByTicket[ticketId].totalHours += parseFloat(activity.estdur) || 0; } }); }); const workSummary = Object.entries(workByTicket) .filter(([_, data]) => data.totalHours > 0) .map(([ticketId, data]) => `${ticketId} (${data.description.substring(0, 50)}...) → ${data.totalHours} hours`) .join('\n'); const totalHours = Object.values(workByTicket).reduce((sum, data) => sum + data.totalHours, 0); return { content: [ { type: 'text', text: `Work Done Last Week (since ${dateFilter}):\n\n${workSummary}\n\nTotal Hours: ${totalHours}` } ] }; } case 'get_work_done_lastweek_bycustomer': { logger.info('Getting work hours by customer/site for last 7 days'); const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); const dateFilter = sevenDaysAgo.toISOString().split('T')[0]; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,SITEID,woactivity.ESTDUR,woactivity.OWNER&lean=1`; endpoint += `&oslc.where=REPORTDATE>="${dateFilter}" and woactivity.owner="VKRISHNA"`; endpoint += `&oslc.pageSize=500`; logger.debug(`Work hours by customer query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No work activities found for the last 7 days (since ${dateFilter})` } ] }; } const workBySite = {}; response.member.forEach(ticket => { const siteId = ticket.siteid; if (!workBySite[siteId]) { workBySite[siteId] = 0; } const activities = ticket.woactivity || []; const activityArray = Array.isArray(activities) ? activities : [activities]; activityArray.forEach(activity => { if (activity && activity.owner === "VKRISHNA" && activity.estdur) { workBySite[siteId] += parseFloat(activity.estdur) || 0; } }); }); const workSummary = Object.entries(workBySite) .filter(([_, hours]) => hours > 0) .sort(([,a], [,b]) => b - a) .map(([siteId, hours]) => `${siteId} → ${hours} hours`) .join('\n'); const totalHours = Object.values(workBySite).reduce((sum, hours) => sum + hours, 0); return { content: [ { type: 'text', text: `Work Done Last Week by Customer (since ${dateFilter}):\n\n${workSummary}\n\nTotal Hours: ${totalHours}` } ] }; } case 'get_tickets_daterange': { logger.info(`Getting tickets for date range: ${args.start_date} to ${args.end_date}`); const datePattern = /^\d{4}-\d{2}-\d{2}$/; if (!datePattern.test(args.start_date) || !datePattern.test(args.end_date)) { return { content: [ { type: 'text', text: 'Error: Dates must be in YYYY-MM-DD format (e.g., 2025-06-01)' } ] }; } const startDate = new Date(args.start_date); const endDate = new Date(args.end_date); if (startDate > endDate) { return { content: [ { type: 'text', text: 'Error: Start date must be before or equal to end date' } ] }; } const limit = args.limit || 100; const includeWorkHours = args.include_work_hours || false; const statusFilter = args.status_filter || 'all'; const selectFields = includeWorkHours ? 'TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE,REPORTDATE,SITEID,woactivity.ESTDUR,woactivity.OWNER' : 'TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE,REPORTDATE,SITEID'; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=${selectFields}&lean=1`; let whereClause = `woactivity.owner="VKRISHNA" and REPORTDATE>="${args.start_date}" and REPORTDATE<="${args.end_date}"`; if (statusFilter === 'active') { whereClause += ` and status!="RESOLVED" and status!="CLOSED" and status!="CANCELLED"`; } else if (statusFilter !== 'all') { whereClause += ` and status="${statusFilter.toUpperCase()}"`; } endpoint += `&oslc.where=${encodeURIComponent(whereClause)}`; endpoint += `&oslc.pageSize=${limit}`; logger.debug(`Date range query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No tickets found between ${args.start_date} and ${args.end_date}` } ] }; } logger.info(`Retrieved ${response.member.length} tickets for date range`); if (includeWorkHours) { const ticketsWithHours = {}; let totalHours = 0; response.member.forEach(ticket => { const ticketId = ticket.ticketid; if (!ticketsWithHours[ticketId]) { ticketsWithHours[ticketId] = { description: ticket.description, status: ticket.status, priority: ticket.reportedpriority, site: ticket.siteid, reportDate: ticket.reportdate, hours: 0 }; } const activities = ticket.woactivity || []; const activityArray = Array.isArray(activities) ? activities : [activities]; activityArray.forEach(activity => { if (activity && activity.owner === "VKRISHNA" && activity.estdur) { const hours = parseFloat(activity.estdur) || 0; ticketsWithHours[ticketId].hours += hours; totalHours += hours; } }); }); const ticketSummary = Object.entries(ticketsWithHours) .map(([ticketId, data]) => `${ticketId} (${data.description.substring(0, 40)}...) → ${data.hours} hours\n` + ` Status: ${data.status} | Site: ${data.site} | Date: ${data.reportDate}` ) .join('\n\n'); return { content: [ { type: 'text', text: `Tickets from ${args.start_date} to ${args.end_date} with Work Hours:\n\n${ticketSummary}\n\nTotal Hours: ${totalHours}\nTotal Tickets: ${Object.keys(ticketsWithHours).length}` } ] }; } else { const ticketSummary = response.member.map(ticket => ({ ticketId: ticket.ticketid, description: ticket.description, status: ticket.status, priority: ticket.reportedpriority, site: ticket.siteid, reportDate: ticket.reportdate })); return { content: [ { type: 'text', text: `Tickets from ${args.start_date} to ${args.end_date} (${response.member.length} found):\n\n${JSON.stringify(ticketSummary, null, 2)}` } ] }; } } case 'get_tickets_by_work_activity_date': { logger.info(`Getting tickets by work activity date range: ${args.start_date} to ${args.end_date}`); const startDate = args.start_date; const endDate = args.end_date; const limit = args.limit || 100; const dateRegex = /^\d{4}-\d{2}-\d{2}$/; if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) { return { content: [ { type: 'text', text: 'Invalid date format. Please use YYYY-MM-DD format (e.g., 2025-06-15)' } ] }; } let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,SITEID,REPORTDATE,woactivity{OWNER,ACTSTART,DESCRIPTION,ESTDUR}&lean=1`; endpoint += `&oslc.where=woactivity.ACTSTART>="${startDate}T00:00:00" and woactivity.ACTSTART<="${endDate}T23:59:59" and woactivity.OWNER="VKRISHNA"`; if (limit) { endpoint += `&oslc.paging=true&oslc.pageSize=${limit}`; } logger.debug(`Query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); logger.debug('Raw API response:', JSON.stringify(response, null, 2)); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No tickets found with work activities between ${startDate} and ${endDate}` } ] }; } const tickets = response.member; logger.info(`Found ${tickets.length} tickets with work activities in date range`); if (tickets.length > 0) { logger.debug('First ticket structure:', JSON.stringify(tickets[0], null, 2)); } let result = `**Tickets with Work Activities: ${startDate} to ${endDate}**\n\n`; result += `Found ${tickets.length} tickets:\n\n`; tickets.forEach(ticket => { const ticketId = ticket.TICKETID || ticket.ticketid || ticket.TicketID || 'N/A'; const description = ticket.DESCRIPTION || ticket.description || ticket.Description || 'N/A'; const status = ticket.STATUS || ticket.status || ticket.Status || 'N/A'; const priority = ticket.REPORTEDPRIORITY || ticket.reportedpriority || ticket.ReportedPriority || 'N/A'; const siteId = ticket.SITEID || ticket.siteid || ticket.SiteID || 'N/A'; const workActivities = ticket.woactivity || ticket.WOACTIVITY || []; const relevantActivities = workActivities.filter(wa => wa.ACTSTART && wa.ACTSTART >= `${startDate}T00:00:00` && wa.ACTSTART <= `${endDate}T23:59:59` ); result += `**${ticketId}** - ${description}\n`; result += ` Site: ${siteId} | Status: ${status} | Priority: ${priority}\n`; if (relevantActivities.length > 0) { result += ` Work Activities:\n`; relevantActivities.forEach(wa => { const actDate = wa.ACTSTART ? new Date(wa.ACTSTART).toLocaleDateString() : 'N/A'; const hours = wa.ESTDUR || wa.estdur || 0; const waDesc = wa.DESCRIPTION || wa.description || 'No description'; result += ` • ${actDate}: ${waDesc} (${hours}h)\n`; }); } result += `\n`; }); return { content: [ { type: 'text', text: result } ] }; } case 'add_work_activity': { logger.info(`Adding work activity to ticket: ${args.ticket_id}`); const ticketEndpoint = `/maximo/oslc/os/MXSR?oslc.select=*&oslc.where=TICKETID="${args.ticket_id}"&lean=1`; logger.debug(`Getting ticket details: ${ticketEndpoint}`); const ticketResponse = await makeMaximoRequest(ticketEndpoint); if (!ticketResponse.member || ticketResponse.member.length === 0) { logger.warn(`Ticket ${args.ticket_id} not found`); return { content: [ { type: 'text', text: `Ticket ${args.ticket_id} not found. Please verify the ticket ID exists.` } ] }; } const ticket = ticketResponse.member[0]; const ticketHref = ticket.href; logger.info(`Found ticket ${args.ticket_id} in site ${ticket.siteid} with href: ${ticketHref}`); const now = new Date(); const reportDate = now.toISOString(); const actStart = new Date(now.getTime() - 5 * 60 * 1000).toISOString(); const workActivityPayload = { "woactivity": [ { "_action": "Add", "description": args.activity_description, "owner": "VKRISHNA", "estdur": parseFloat(args.hours), "actlabhrs": parseFloat(args.hours), "siteid": ticket.siteid || "BEDFORD", "orgid": ticket.orgid || "EAGLENA", "status": "INPRG", "reportdate": reportDate, "actstart": actStart, "woclass": "ACTIVITY", "worktype": "CM", "assignedownergroup": ticket.ownergroup || "", "parentwonum": args.ticket_id, "historyflag": false, "istask": false, "haschildren": false, "template": false } ] }; logger.debug('Work activity payload:', JSON.stringify(workActivityPayload, null, 2)); const resourcePath = ticketHref.replace(MAXIMO_HOST, ''); const updateEndpoint = `${resourcePath}?lean=1`; logger.debug(`Update endpoint: ${updateEndpoint}`); try { const response = await makeMaximoRequest(updateEndpoint, { method: 'POST', headers: { 'MAXAUTH': MAXAUTH, 'Content-Type': 'application/json', 'x-method-override': 'PATCH', 'patchtype': 'MERGE', 'properties': 'wonum,description,status' }, body: JSON.stringify(workActivityPayload) }); logger.info('Work activity update response received'); logger.debug('Update response:', JSON.stringify(response, null, 2)); const verifyEndpoint = `/maximo/oslc/os/MXSR?oslc.select=ticketid,woactivity{wonum,description,owner,estdur,status}&oslc.where=TICKETID="${args.ticket_id}"&lean=1`; logger.debug(`Verifying work activity creation: ${verifyEndpoint}`); const verifyResponse = await makeMaximoRequest(verifyEndpoint); if (verifyResponse && verifyResponse.member && verifyResponse.member.length > 0) { const updatedTicket = verifyResponse.member[0]; const workActivities = updatedTicket.woactivity || []; const latestActivity = workActivities.find(activity => activity.owner === "VKRISHNA" && activity.description === args.activity_description ); if (latestActivity) { logger.info(`Work activity successfully created with WONUM: ${latestActivity.wonum}`); const successMessage = `Work activity added successfully!\n\nSR Ticket: ${args.ticket_id}\nWork Activity: ${latestActivity.wonum}\nOwner: VKRISHNA\nHours: ${args.hours}\nDescription: ${args.activity_description}\nStatus: ${latestActivity.status}\nDate: ${new Date().toLocaleDateString()}`; return { content: [ { type: 'text', text: successMessage } ] }; } else { logger.warn('Work activity was not found in verification check'); return { content: [ { type: 'text', text: `Work activity may have been added to ticket ${args.ticket_id}, but verification failed. Please check the ticket manually.` } ] }; } } else { logger.error('Failed to verify work activity creation'); return { content: [ { type: 'text', text: `Failed to verify work activity creation for ticket ${args.ticket_id}. Please check the ticket manually.` } ] ]; } } catch (error) { logger.error('Error adding work activity:', error); let errorMessage = `Failed to add work activity to ticket ${args.ticket_id}.`; if (error.message) { errorMessage += `\n\nError details: ${error.message}`; } if (error.response) { errorMessage += `\nHTTP Status: ${error.response.status}`; } errorMessage += `\n\nPlease check:\n• Ticket ${args.ticket_id} exists and is accessible\n• You have permission to add work activities\n• The ticket is in a status that allows work activities`; return { content: [ { type: 'text', text: errorMessage } ] }; } } default: logger.error(`Unknown tool: ${name}`); throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { logger.error(`Error in tool ${name}:`, error); return { content: [ { type: 'text', text: `Error: ${error.message}\n\nFull error details logged to maximo-mcp-sse.log` } ], isError: true }; } }); // Create Express app for HTTP/SSE const app = express(); // Enable CORS for cross-origin requests app.use(cors({ origin: '*', // In production, specify your Claude Desktop origin credentials: true })); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'healthy', server: 'maximo-mcp-sse', version: '2.0.0', timestamp: new Date().toISOString() }); }); // SSE endpoint for MCP communication app.get('/sse', async (req, res) => { logger.info('New SSE connection established'); // Set up SSE headers res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type', }); try { // Create SSE transport const transport = new SSEServerTransport('/sse', res); // Connect MCP server to SSE transport await server.connect(transport); logger.info('MCP Server connected via SSE transport'); } catch (error) { logger.error('Failed to establish SSE connection:', error); res.write(`event: error\ndata: ${JSON.stringify({ error: error.message })}\n\n`); res.end(); } }); // Start the HTTP server async function startServer() { try { const server = app.listen(SERVER_PORT, SERVER_HOST, () => { logger.info(`Maximo MCP SSE Server running on http://${SERVER_HOST}:${SERVER_PORT}`); logger.info(`SSE endpoint available at: http://${SERVER_HOST}:${SERVER_PORT}/sse`); logger.info(`Health check available at: http://${SERVER_HOST}:${SERVER_PORT}/health`); }); // Graceful shutdown handling process.on('SIGTERM', () => { logger.info('SIGTERM received, shutting down gracefully'); server.close(() => { logger.info('Server closed'); process.exit(0); }); }); process.on('SIGINT', () => { logger.info('SIGINT received, shutting down gracefully'); server.close(() => { logger.info('Server closed'); process.exit(0); }); }); } catch (error) { logger.error('Failed to start server:', error); process.exit(1); } } startServer(); }, end_date: { type: 'string', description: 'End date in YYYY-MM-DD format (e.g., 2025-06-20)', pattern: '^\\d{4}-\\d{2}-\\d{2} ] }; }); // Tool request handler (keeping all the original tool logic) server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; logger.info(`Tool called via SSE: ${name}`, args); try { switch (name) { case 'query_tickets': { logger.info('Querying tickets for user VKRISHNA'); const statusFilter = args.status_filter || 'active'; const limit = args.limit || 50; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE&lean=1`; if (statusFilter === 'active') { endpoint += `&oslc.where=woactivity.owner="VKRISHNA" and status!="RESOLVED" and status!="CLOSED" and status!="CANCELLED"`; } else if (statusFilter !== 'all') { endpoint += `&oslc.where=woactivity.owner="VKRISHNA" and status="${statusFilter.toUpperCase()}"`; } else { endpoint += `&oslc.where=woactivity.owner="VKRISHNA"`; } endpoint += `&oslc.pageSize=${limit}`; logger.debug(`Query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); logger.info(`Retrieved ${response.member ? response.member.length : 0} tickets`); return { content: [ { type: 'text', text: JSON.stringify(response, null, 2) } ] }; } // ... (include all other case statements from original - query_tickets_by_site, create_ticket, etc.) // I'm including the create_ticket case as an example: case 'create_ticket': { logger.info('Creating new ticket via SSE', { siteid: args.siteid, title: args.title, priority: args.priority, is_production: args.is_production, sr_type: args.sr_type }); const mappedSiteId = SITE_MAPPINGS[args.siteid] || args.siteid; logger.info(`Mapped site ID: ${args.siteid} -> ${mappedSiteId}`); const ticketData = { "_action": "AddChange", "siteid": mappedSiteId, "description": args.title, "description_longdescription": args.description, "reportedby": "VKRISHNA", "affectedperson": "VKRISHNA", "reportedemail": "vijay.krishna@zprosolutions.com", "reportedpriority": args.priority, "zp_production": args.is_production ? "YES" : "NO", "zp_srtype": args.sr_type }; logger.debug('Ticket payload:', ticketData); const endpoint = `/maximo/oslc/os/MXSR?lean=1&ignorecollectionref=1&ignorekeyref=1&ignorers=1&mxlaction=addchange`; const response = await makeMaximoRequest(endpoint, { method: 'POST', headers: { 'MAXAUTH': MAXAUTH, 'x-method-override': 'BULK' }, body: JSON.stringify([ticketData]) }); // ... (rest of create_ticket logic same as original) let ticketNumber = 'Unknown'; if (response && Array.isArray(response) && response.length > 0) { const responseItem = response[0]; if (responseItem.TICKETID || responseItem.ticketid) { ticketNumber = responseItem.TICKETID || responseItem.ticketid; } else if (responseItem._responsemeta && responseItem._responsemeta.Location) { try { const locationUrl = responseItem._responsemeta.Location; logger.debug(`Extracting ticket ID from Location: ${locationUrl}`); const encodedPart = locationUrl.split('/').pop(); let cleanEncoded = encodedPart.replace(/^_+/, '').replace(/-+$/, ''); const standardBase64 = cleanEncoded.replace(/_/g, '/').replace(/-/g, '+'); let paddedBase64 = standardBase64; while (paddedBase64.length % 4) { paddedBase64 += '='; } const decoded = Buffer.from(paddedBase64, 'base64').toString('utf-8'); const ticketMatch = decoded.match(/SR\/(\d+)/); if (ticketMatch && ticketMatch[1]) { ticketNumber = ticketMatch[1]; logger.info(`Successfully extracted ticket ID: ${ticketNumber}`); } } catch (decodeError) { logger.warn('Failed to decode ticket ID from Location URL:', decodeError); } } } const successMessage = ticketNumber !== 'Unknown' ? `Ticket created successfully!\n\nTicket ID: ${ticketNumber}\nSite: ${mappedSiteId}\nPriority: ${args.priority}\nType: ${args.sr_type}\nProduction: ${args.is_production ? 'YES' : 'NO'}\n\nTitle: ${args.title}` : `Ticket created but ID could not be extracted.\n\nPlease check the logs and query recent tickets to find the new ticket.\n\nFull Response:\n${JSON.stringify(response, null, 2)}`; return { content: [ { type: 'text', text: successMessage } ] }; } case 'query_tickets_by_site': { logger.info('Querying tickets by site and/or status', { site_id: args.site_id, status_values: args.status_values, limit: args.limit }); const limit = args.limit || 50; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE,SITEID&lean=1`; let whereConditions = ['woactivity.owner="VKRISHNA"']; if (args.site_id) { whereConditions.push(`siteid="${args.site_id}"`); } if (args.status_values && Array.isArray(args.status_values) && args.status_values.length > 0) { const statusConditions = args.status_values.map(status => `status="${status.toUpperCase()}"`); if (statusConditions.length === 1) { whereConditions.push(statusConditions[0]); } else { whereConditions.push(`(${statusConditions.join(' or ')})`); } } const whereClause = whereConditions.join(' and '); endpoint += `&oslc.where=${encodeURIComponent(whereClause)}`; endpoint += `&oslc.pageSize=${limit}`; logger.debug(`Site/Status query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); logger.info(`Retrieved ${response.member ? response.member.length : 0} tickets matching criteria`); if (!response.member || response.member.length === 0) { const filterDescription = []; if (args.site_id) filterDescription.push(`Site: ${args.site_id}`); if (args.status_values) filterDescription.push(`Status: ${args.status_values.join(', ')}`); return { content: [ { type: 'text', text: `No tickets found matching criteria: ${filterDescription.join(', ')}` } ] }; } const ticketSummary = response.member.map(ticket => ({ ticketId: ticket.ticketid, description: ticket.description, status: ticket.status, priority: ticket.reportedpriority, site: ticket.siteid, production: ticket.zp_production, srType: ticket.zp_srtype })); const filterDescription = []; if (args.site_id) filterDescription.push(`Site: ${args.site_id}`); if (args.status_values) filterDescription.push(`Status: ${args.status_values.join(', ')}`); return { content: [ { type: 'text', text: `Tickets matching criteria (${filterDescription.join(', ')}): ${response.member.length} found\n\n${JSON.stringify(ticketSummary, null, 2)}` } ] }; } case 'get_ticket_details': { logger.info(`Getting details for ticket: ${args.ticket_id}`); const endpoint = `/maximo/oslc/os/MXSR?oslc.select=*&oslc.where=TICKETID="${args.ticket_id}"&lean=1`; const response = await makeMaximoRequest(endpoint); if (!response.member || response.member.length === 0) { logger.warn(`Ticket ${args.ticket_id} not found`); return { content: [ { type: 'text', text: `Ticket ${args.ticket_id} not found. Please verify the ticket ID and ensure you have access to this ticket.` } ] }; } logger.info(`Retrieved details for ticket ${args.ticket_id}`); const ticketData = response.member[0]; const formattedResponse = { ticketId: ticketData.ticketid || 'Unknown', status: ticketData.status || 'Unknown', description: ticketData.description || 'No description', longDescription: ticketData.description_longdescription || 'No detailed description', priority: ticketData.reportedpriority || 'Unknown', siteId: ticketData.siteid || 'Unknown', production: ticketData.zp_production || 'Unknown', srType: ticketData.zp_srtype || 'Unknown', creationDate: ticketData.creationdate || 'Unknown', reportedBy: ticketData.reportedby || 'Unknown', affectedPerson: ticketData.affectedperson || 'Unknown', fullDetails: response }; return { content: [ { type: 'text', text: `Ticket Details:\n\n${JSON.stringify(formattedResponse, null, 2)}` } ] }; } case 'get_lastweek_tickets': { logger.info('Getting tickets from last 7 days'); const limit = args.limit || 100; const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); const dateFilter = sevenDaysAgo.toISOString().split('T')[0]; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE,REPORTDATE,SITEID&lean=1`; endpoint += `&oslc.where=woactivity.owner="VKRISHNA" and REPORTDATE>="${dateFilter}"`; endpoint += `&oslc.pageSize=${limit}`; logger.debug(`Last week query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); logger.info(`Retrieved ${response.member ? response.member.length : 0} tickets from last 7 days`); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No tickets found in the last 7 days (since ${dateFilter})` } ] }; } const ticketSummary = response.member.map(ticket => ({ ticketId: ticket.ticketid, description: ticket.description, status: ticket.status, priority: ticket.reportedpriority, site: ticket.siteid, reportDate: ticket.reportdate })); return { content: [ { type: 'text', text: `Last Week's Tickets (${response.member.length} found since ${dateFilter}):\n\n${JSON.stringify(ticketSummary, null, 2)}` } ] }; } case 'get_work_done_lastweek': { logger.info('Getting work hours by ticket for last 7 days'); const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); const dateFilter = sevenDaysAgo.toISOString().split('T')[0]; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,SITEID,woactivity.ESTDUR,woactivity.OWNER&lean=1`; endpoint += `&oslc.where=REPORTDATE>="${dateFilter}" and woactivity.owner="VKRISHNA"`; endpoint += `&oslc.pageSize=500`; logger.debug(`Work hours query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No work activities found for the last 7 days (since ${dateFilter})` } ] }; } const workByTicket = {}; response.member.forEach(ticket => { const ticketId = ticket.ticketid; const description = ticket.description; if (!workByTicket[ticketId]) { workByTicket[ticketId] = { description: description, totalHours: 0 }; } const activities = ticket.woactivity || []; const activityArray = Array.isArray(activities) ? activities : [activities]; activityArray.forEach(activity => { if (activity && activity.owner === "VKRISHNA" && activity.estdur) { workByTicket[ticketId].totalHours += parseFloat(activity.estdur) || 0; } }); }); const workSummary = Object.entries(workByTicket) .filter(([_, data]) => data.totalHours > 0) .map(([ticketId, data]) => `${ticketId} (${data.description.substring(0, 50)}...) → ${data.totalHours} hours`) .join('\n'); const totalHours = Object.values(workByTicket).reduce((sum, data) => sum + data.totalHours, 0); return { content: [ { type: 'text', text: `Work Done Last Week (since ${dateFilter}):\n\n${workSummary}\n\nTotal Hours: ${totalHours}` } ] }; } case 'get_work_done_lastweek_bycustomer': { logger.info('Getting work hours by customer/site for last 7 days'); const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); const dateFilter = sevenDaysAgo.toISOString().split('T')[0]; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,SITEID,woactivity.ESTDUR,woactivity.OWNER&lean=1`; endpoint += `&oslc.where=REPORTDATE>="${dateFilter}" and woactivity.owner="VKRISHNA"`; endpoint += `&oslc.pageSize=500`; logger.debug(`Work hours by customer query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No work activities found for the last 7 days (since ${dateFilter})` } ] }; } const workBySite = {}; response.member.forEach(ticket => { const siteId = ticket.siteid; if (!workBySite[siteId]) { workBySite[siteId] = 0; } const activities = ticket.woactivity || []; const activityArray = Array.isArray(activities) ? activities : [activities]; activityArray.forEach(activity => { if (activity && activity.owner === "VKRISHNA" && activity.estdur) { workBySite[siteId] += parseFloat(activity.estdur) || 0; } }); }); const workSummary = Object.entries(workBySite) .filter(([_, hours]) => hours > 0) .sort(([,a], [,b]) => b - a) .map(([siteId, hours]) => `${siteId} → ${hours} hours`) .join('\n'); const totalHours = Object.values(workBySite).reduce((sum, hours) => sum + hours, 0); return { content: [ { type: 'text', text: `Work Done Last Week by Customer (since ${dateFilter}):\n\n${workSummary}\n\nTotal Hours: ${totalHours}` } ] }; } case 'get_tickets_daterange': { logger.info(`Getting tickets for date range: ${args.start_date} to ${args.end_date}`); const datePattern = /^\d{4}-\d{2}-\d{2}$/; if (!datePattern.test(args.start_date) || !datePattern.test(args.end_date)) { return { content: [ { type: 'text', text: 'Error: Dates must be in YYYY-MM-DD format (e.g., 2025-06-01)' } ] }; } const startDate = new Date(args.start_date); const endDate = new Date(args.end_date); if (startDate > endDate) { return { content: [ { type: 'text', text: 'Error: Start date must be before or equal to end date' } ] }; } const limit = args.limit || 100; const includeWorkHours = args.include_work_hours || false; const statusFilter = args.status_filter || 'all'; const selectFields = includeWorkHours ? 'TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE,REPORTDATE,SITEID,woactivity.ESTDUR,woactivity.OWNER' : 'TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE,REPORTDATE,SITEID'; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=${selectFields}&lean=1`; let whereClause = `woactivity.owner="VKRISHNA" and REPORTDATE>="${args.start_date}" and REPORTDATE<="${args.end_date}"`; if (statusFilter === 'active') { whereClause += ` and status!="RESOLVED" and status!="CLOSED" and status!="CANCELLED"`; } else if (statusFilter !== 'all') { whereClause += ` and status="${statusFilter.toUpperCase()}"`; } endpoint += `&oslc.where=${encodeURIComponent(whereClause)}`; endpoint += `&oslc.pageSize=${limit}`; logger.debug(`Date range query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No tickets found between ${args.start_date} and ${args.end_date}` } ] }; } logger.info(`Retrieved ${response.member.length} tickets for date range`); if (includeWorkHours) { const ticketsWithHours = {}; let totalHours = 0; response.member.forEach(ticket => { const ticketId = ticket.ticketid; if (!ticketsWithHours[ticketId]) { ticketsWithHours[ticketId] = { description: ticket.description, status: ticket.status, priority: ticket.reportedpriority, site: ticket.siteid, reportDate: ticket.reportdate, hours: 0 }; } const activities = ticket.woactivity || []; const activityArray = Array.isArray(activities) ? activities : [activities]; activityArray.forEach(activity => { if (activity && activity.owner === "VKRISHNA" && activity.estdur) { const hours = parseFloat(activity.estdur) || 0; ticketsWithHours[ticketId].hours += hours; totalHours += hours; } }); }); const ticketSummary = Object.entries(ticketsWithHours) .map(([ticketId, data]) => `${ticketId} (${data.description.substring(0, 40)}...) → ${data.hours} hours\n` + ` Status: ${data.status} | Site: ${data.site} | Date: ${data.reportDate}` ) .join('\n\n'); return { content: [ { type: 'text', text: `Tickets from ${args.start_date} to ${args.end_date} with Work Hours:\n\n${ticketSummary}\n\nTotal Hours: ${totalHours}\nTotal Tickets: ${Object.keys(ticketsWithHours).length}` } ] }; } else { const ticketSummary = response.member.map(ticket => ({ ticketId: ticket.ticketid, description: ticket.description, status: ticket.status, priority: ticket.reportedpriority, site: ticket.siteid, reportDate: ticket.reportdate })); return { content: [ { type: 'text', text: `Tickets from ${args.start_date} to ${args.end_date} (${response.member.length} found):\n\n${JSON.stringify(ticketSummary, null, 2)}` } ] }; } } case 'get_tickets_by_work_activity_date': { logger.info(`Getting tickets by work activity date range: ${args.start_date} to ${args.end_date}`); const startDate = args.start_date; const endDate = args.end_date; const limit = args.limit || 100; const dateRegex = /^\d{4}-\d{2}-\d{2}$/; if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) { return { content: [ { type: 'text', text: 'Invalid date format. Please use YYYY-MM-DD format (e.g., 2025-06-15)' } ] }; } let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,SITEID,REPORTDATE,woactivity{OWNER,ACTSTART,DESCRIPTION,ESTDUR}&lean=1`; endpoint += `&oslc.where=woactivity.ACTSTART>="${startDate}T00:00:00" and woactivity.ACTSTART<="${endDate}T23:59:59" and woactivity.OWNER="VKRISHNA"`; if (limit) { endpoint += `&oslc.paging=true&oslc.pageSize=${limit}`; } logger.debug(`Query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); logger.debug('Raw API response:', JSON.stringify(response, null, 2)); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No tickets found with work activities between ${startDate} and ${endDate}` } ] }; } const tickets = response.member; logger.info(`Found ${tickets.length} tickets with work activities in date range`); if (tickets.length > 0) { logger.debug('First ticket structure:', JSON.stringify(tickets[0], null, 2)); } let result = `**Tickets with Work Activities: ${startDate} to ${endDate}**\n\n`; result += `Found ${tickets.length} tickets:\n\n`; tickets.forEach(ticket => { const ticketId = ticket.TICKETID || ticket.ticketid || ticket.TicketID || 'N/A'; const description = ticket.DESCRIPTION || ticket.description || ticket.Description || 'N/A'; const status = ticket.STATUS || ticket.status || ticket.Status || 'N/A'; const priority = ticket.REPORTEDPRIORITY || ticket.reportedpriority || ticket.ReportedPriority || 'N/A'; const siteId = ticket.SITEID || ticket.siteid || ticket.SiteID || 'N/A'; const workActivities = ticket.woactivity || ticket.WOACTIVITY || []; const relevantActivities = workActivities.filter(wa => wa.ACTSTART && wa.ACTSTART >= `${startDate}T00:00:00` && wa.ACTSTART <= `${endDate}T23:59:59` ); result += `**${ticketId}** - ${description}\n`; result += ` Site: ${siteId} | Status: ${status} | Priority: ${priority}\n`; if (relevantActivities.length > 0) { result += ` Work Activities:\n`; relevantActivities.forEach(wa => { const actDate = wa.ACTSTART ? new Date(wa.ACTSTART).toLocaleDateString() : 'N/A'; const hours = wa.ESTDUR || wa.estdur || 0; const waDesc = wa.DESCRIPTION || wa.description || 'No description'; result += ` • ${actDate}: ${waDesc} (${hours}h)\n`; }); } result += `\n`; }); return { content: [ { type: 'text', text: result } ] }; } case 'add_work_activity': { logger.info(`Adding work activity to ticket: ${args.ticket_id}`); const ticketEndpoint = `/maximo/oslc/os/MXSR?oslc.select=*&oslc.where=TICKETID="${args.ticket_id}"&lean=1`; logger.debug(`Getting ticket details: ${ticketEndpoint}`); const ticketResponse = await makeMaximoRequest(ticketEndpoint); if (!ticketResponse.member || ticketResponse.member.length === 0) { logger.warn(`Ticket ${args.ticket_id} not found`); return { content: [ { type: 'text', text: `Ticket ${args.ticket_id} not found. Please verify the ticket ID exists.` } ] }; } const ticket = ticketResponse.member[0]; const ticketHref = ticket.href; logger.info(`Found ticket ${args.ticket_id} in site ${ticket.siteid} with href: ${ticketHref}`); const now = new Date(); const reportDate = now.toISOString(); const actStart = new Date(now.getTime() - 5 * 60 * 1000).toISOString(); const workActivityPayload = { "woactivity": [ { "_action": "Add", "description": args.activity_description, "owner": "VKRISHNA", "estdur": parseFloat(args.hours), "actlabhrs": parseFloat(args.hours), "siteid": ticket.siteid || "BEDFORD", "orgid": ticket.orgid || "EAGLENA", "status": "INPRG", "reportdate": reportDate, "actstart": actStart, "woclass": "ACTIVITY", "worktype": "CM", "assignedownergroup": ticket.ownergroup || "", "parentwonum": args.ticket_id, "historyflag": false, "istask": false, "haschildren": false, "template": false } ] }; logger.debug('Work activity payload:', JSON.stringify(workActivityPayload, null, 2)); const resourcePath = ticketHref.replace(MAXIMO_HOST, ''); const updateEndpoint = `${resourcePath}?lean=1`; logger.debug(`Update endpoint: ${updateEndpoint}`); try { const response = await makeMaximoRequest(updateEndpoint, { method: 'POST', headers: { 'MAXAUTH': MAXAUTH, 'Content-Type': 'application/json', 'x-method-override': 'PATCH', 'patchtype': 'MERGE', 'properties': 'wonum,description,status' }, body: JSON.stringify(workActivityPayload) }); logger.info('Work activity update response received'); logger.debug('Update response:', JSON.stringify(response, null, 2)); const verifyEndpoint = `/maximo/oslc/os/MXSR?oslc.select=ticketid,woactivity{wonum,description,owner,estdur,status}&oslc.where=TICKETID="${args.ticket_id}"&lean=1`; logger.debug(`Verifying work activity creation: ${verifyEndpoint}`); const verifyResponse = await makeMaximoRequest(verifyEndpoint); if (verifyResponse && verifyResponse.member && verifyResponse.member.length > 0) { const updatedTicket = verifyResponse.member[0]; const workActivities = updatedTicket.woactivity || []; const latestActivity = workActivities.find(activity => activity.owner === "VKRISHNA" && activity.description === args.activity_description ); if (latestActivity) { logger.info(`Work activity successfully created with WONUM: ${latestActivity.wonum}`); const successMessage = `Work activity added successfully!\n\nSR Ticket: ${args.ticket_id}\nWork Activity: ${latestActivity.wonum}\nOwner: VKRISHNA\nHours: ${args.hours}\nDescription: ${args.activity_description}\nStatus: ${latestActivity.status}\nDate: ${new Date().toLocaleDateString()}`; return { content: [ { type: 'text', text: successMessage } ] }; } else { logger.warn('Work activity was not found in verification check'); return { content: [ { type: 'text', text: `Work activity may have been added to ticket ${args.ticket_id}, but verification failed. Please check the ticket manually.` } ] }; } } else { logger.error('Failed to verify work activity creation'); return { content: [ { type: 'text', text: `Failed to verify work activity creation for ticket ${args.ticket_id}. Please check the ticket manually.` } ] ]; } } catch (error) { logger.error('Error adding work activity:', error); let errorMessage = `Failed to add work activity to ticket ${args.ticket_id}.`; if (error.message) { errorMessage += `\n\nError details: ${error.message}`; } if (error.response) { errorMessage += `\nHTTP Status: ${error.response.status}`; } errorMessage += `\n\nPlease check:\n• Ticket ${args.ticket_id} exists and is accessible\n• You have permission to add work activities\n• The ticket is in a status that allows work activities`; return { content: [ { type: 'text', text: errorMessage } ] }; } } default: logger.error(`Unknown tool: ${name}`); throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { logger.error(`Error in tool ${name}:`, error); return { content: [ { type: 'text', text: `Error: ${error.message}\n\nFull error details logged to maximo-mcp-sse.log` } ], isError: true }; } }); // Create Express app for HTTP/SSE const app = express(); // Enable CORS for cross-origin requests app.use(cors({ origin: '*', // In production, specify your Claude Desktop origin credentials: true })); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'healthy', server: 'maximo-mcp-sse', version: '2.0.0', timestamp: new Date().toISOString() }); }); // SSE endpoint for MCP communication app.get('/sse', async (req, res) => { logger.info('New SSE connection established'); // Set up SSE headers res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type', }); try { // Create SSE transport const transport = new SSEServerTransport('/sse', res); // Connect MCP server to SSE transport await server.connect(transport); logger.info('MCP Server connected via SSE transport'); } catch (error) { logger.error('Failed to establish SSE connection:', error); res.write(`event: error\ndata: ${JSON.stringify({ error: error.message })}\n\n`); res.end(); } }); // Start the HTTP server async function startServer() { try { const server = app.listen(SERVER_PORT, SERVER_HOST, () => { logger.info(`Maximo MCP SSE Server running on http://${SERVER_HOST}:${SERVER_PORT}`); logger.info(`SSE endpoint available at: http://${SERVER_HOST}:${SERVER_PORT}/sse`); logger.info(`Health check available at: http://${SERVER_HOST}:${SERVER_PORT}/health`); }); // Graceful shutdown handling process.on('SIGTERM', () => { logger.info('SIGTERM received, shutting down gracefully'); server.close(() => { logger.info('Server closed'); process.exit(0); }); }); process.on('SIGINT', () => { logger.info('SIGINT received, shutting down gracefully'); server.close(() => { logger.info('Server closed'); process.exit(0); }); }); } catch (error) { logger.error('Failed to start server:', error); process.exit(1); } } startServer(); }, limit: { type: 'number', description: 'Maximum number of tickets to return (default: 100)', minimum: 1, maximum: 500, default: 100 } }, required: ['username', 'start_date', 'end_date'] } }, { name: 'add_work_activity', description: 'Add work activity and hours to a ticket under specified username', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'Maximo username to assign work activity to (e.g., VKRISHNA)', required: true }, ticket_id: { type: 'string', description: 'The ticket ID to add work activity to', required: true }, activity_description: { type: 'string', description: 'Description of the work performed', required: true }, hours: { type: 'number', description: 'Number of hours worked (can include decimals like 1.5)', minimum: 0.1, maximum: 24, required: true } }, required: ['username', 'ticket_id', 'activity_description', 'hours'] } } ] }; }); // Tool request handler (keeping all the original tool logic) server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; logger.info(`Tool called via SSE: ${name}`, args); try { switch (name) { case 'query_tickets': { logger.info('Querying tickets for user VKRISHNA'); const statusFilter = args.status_filter || 'active'; const limit = args.limit || 50; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE&lean=1`; if (statusFilter === 'active') { endpoint += `&oslc.where=woactivity.owner="VKRISHNA" and status!="RESOLVED" and status!="CLOSED" and status!="CANCELLED"`; } else if (statusFilter !== 'all') { endpoint += `&oslc.where=woactivity.owner="VKRISHNA" and status="${statusFilter.toUpperCase()}"`; } else { endpoint += `&oslc.where=woactivity.owner="VKRISHNA"`; } endpoint += `&oslc.pageSize=${limit}`; logger.debug(`Query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); logger.info(`Retrieved ${response.member ? response.member.length : 0} tickets`); return { content: [ { type: 'text', text: JSON.stringify(response, null, 2) } ] }; } // ... (include all other case statements from original - query_tickets_by_site, create_ticket, etc.) // I'm including the create_ticket case as an example: case 'create_ticket': { logger.info('Creating new ticket via SSE', { siteid: args.siteid, title: args.title, priority: args.priority, is_production: args.is_production, sr_type: args.sr_type }); const mappedSiteId = SITE_MAPPINGS[args.siteid] || args.siteid; logger.info(`Mapped site ID: ${args.siteid} -> ${mappedSiteId}`); const ticketData = { "_action": "AddChange", "siteid": mappedSiteId, "description": args.title, "description_longdescription": args.description, "reportedby": "VKRISHNA", "affectedperson": "VKRISHNA", "reportedemail": "vijay.krishna@zprosolutions.com", "reportedpriority": args.priority, "zp_production": args.is_production ? "YES" : "NO", "zp_srtype": args.sr_type }; logger.debug('Ticket payload:', ticketData); const endpoint = `/maximo/oslc/os/MXSR?lean=1&ignorecollectionref=1&ignorekeyref=1&ignorers=1&mxlaction=addchange`; const response = await makeMaximoRequest(endpoint, { method: 'POST', headers: { 'MAXAUTH': MAXAUTH, 'x-method-override': 'BULK' }, body: JSON.stringify([ticketData]) }); // ... (rest of create_ticket logic same as original) let ticketNumber = 'Unknown'; if (response && Array.isArray(response) && response.length > 0) { const responseItem = response[0]; if (responseItem.TICKETID || responseItem.ticketid) { ticketNumber = responseItem.TICKETID || responseItem.ticketid; } else if (responseItem._responsemeta && responseItem._responsemeta.Location) { try { const locationUrl = responseItem._responsemeta.Location; logger.debug(`Extracting ticket ID from Location: ${locationUrl}`); const encodedPart = locationUrl.split('/').pop(); let cleanEncoded = encodedPart.replace(/^_+/, '').replace(/-+$/, ''); const standardBase64 = cleanEncoded.replace(/_/g, '/').replace(/-/g, '+'); let paddedBase64 = standardBase64; while (paddedBase64.length % 4) { paddedBase64 += '='; } const decoded = Buffer.from(paddedBase64, 'base64').toString('utf-8'); const ticketMatch = decoded.match(/SR\/(\d+)/); if (ticketMatch && ticketMatch[1]) { ticketNumber = ticketMatch[1]; logger.info(`Successfully extracted ticket ID: ${ticketNumber}`); } } catch (decodeError) { logger.warn('Failed to decode ticket ID from Location URL:', decodeError); } } } const successMessage = ticketNumber !== 'Unknown' ? `Ticket created successfully!\n\nTicket ID: ${ticketNumber}\nSite: ${mappedSiteId}\nPriority: ${args.priority}\nType: ${args.sr_type}\nProduction: ${args.is_production ? 'YES' : 'NO'}\n\nTitle: ${args.title}` : `Ticket created but ID could not be extracted.\n\nPlease check the logs and query recent tickets to find the new ticket.\n\nFull Response:\n${JSON.stringify(response, null, 2)}`; return { content: [ { type: 'text', text: successMessage } ] }; } case 'query_tickets_by_site': { logger.info('Querying tickets by site and/or status', { site_id: args.site_id, status_values: args.status_values, limit: args.limit }); const limit = args.limit || 50; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE,SITEID&lean=1`; let whereConditions = ['woactivity.owner="VKRISHNA"']; if (args.site_id) { whereConditions.push(`siteid="${args.site_id}"`); } if (args.status_values && Array.isArray(args.status_values) && args.status_values.length > 0) { const statusConditions = args.status_values.map(status => `status="${status.toUpperCase()}"`); if (statusConditions.length === 1) { whereConditions.push(statusConditions[0]); } else { whereConditions.push(`(${statusConditions.join(' or ')})`); } } const whereClause = whereConditions.join(' and '); endpoint += `&oslc.where=${encodeURIComponent(whereClause)}`; endpoint += `&oslc.pageSize=${limit}`; logger.debug(`Site/Status query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); logger.info(`Retrieved ${response.member ? response.member.length : 0} tickets matching criteria`); if (!response.member || response.member.length === 0) { const filterDescription = []; if (args.site_id) filterDescription.push(`Site: ${args.site_id}`); if (args.status_values) filterDescription.push(`Status: ${args.status_values.join(', ')}`); return { content: [ { type: 'text', text: `No tickets found matching criteria: ${filterDescription.join(', ')}` } ] }; } const ticketSummary = response.member.map(ticket => ({ ticketId: ticket.ticketid, description: ticket.description, status: ticket.status, priority: ticket.reportedpriority, site: ticket.siteid, production: ticket.zp_production, srType: ticket.zp_srtype })); const filterDescription = []; if (args.site_id) filterDescription.push(`Site: ${args.site_id}`); if (args.status_values) filterDescription.push(`Status: ${args.status_values.join(', ')}`); return { content: [ { type: 'text', text: `Tickets matching criteria (${filterDescription.join(', ')}): ${response.member.length} found\n\n${JSON.stringify(ticketSummary, null, 2)}` } ] }; } case 'get_ticket_details': { logger.info(`Getting details for ticket: ${args.ticket_id}`); const endpoint = `/maximo/oslc/os/MXSR?oslc.select=*&oslc.where=TICKETID="${args.ticket_id}"&lean=1`; const response = await makeMaximoRequest(endpoint); if (!response.member || response.member.length === 0) { logger.warn(`Ticket ${args.ticket_id} not found`); return { content: [ { type: 'text', text: `Ticket ${args.ticket_id} not found. Please verify the ticket ID and ensure you have access to this ticket.` } ] }; } logger.info(`Retrieved details for ticket ${args.ticket_id}`); const ticketData = response.member[0]; const formattedResponse = { ticketId: ticketData.ticketid || 'Unknown', status: ticketData.status || 'Unknown', description: ticketData.description || 'No description', longDescription: ticketData.description_longdescription || 'No detailed description', priority: ticketData.reportedpriority || 'Unknown', siteId: ticketData.siteid || 'Unknown', production: ticketData.zp_production || 'Unknown', srType: ticketData.zp_srtype || 'Unknown', creationDate: ticketData.creationdate || 'Unknown', reportedBy: ticketData.reportedby || 'Unknown', affectedPerson: ticketData.affectedperson || 'Unknown', fullDetails: response }; return { content: [ { type: 'text', text: `Ticket Details:\n\n${JSON.stringify(formattedResponse, null, 2)}` } ] }; } case 'get_lastweek_tickets': { logger.info('Getting tickets from last 7 days'); const limit = args.limit || 100; const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); const dateFilter = sevenDaysAgo.toISOString().split('T')[0]; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE,REPORTDATE,SITEID&lean=1`; endpoint += `&oslc.where=woactivity.owner="VKRISHNA" and REPORTDATE>="${dateFilter}"`; endpoint += `&oslc.pageSize=${limit}`; logger.debug(`Last week query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); logger.info(`Retrieved ${response.member ? response.member.length : 0} tickets from last 7 days`); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No tickets found in the last 7 days (since ${dateFilter})` } ] }; } const ticketSummary = response.member.map(ticket => ({ ticketId: ticket.ticketid, description: ticket.description, status: ticket.status, priority: ticket.reportedpriority, site: ticket.siteid, reportDate: ticket.reportdate })); return { content: [ { type: 'text', text: `Last Week's Tickets (${response.member.length} found since ${dateFilter}):\n\n${JSON.stringify(ticketSummary, null, 2)}` } ] }; } case 'get_work_done_lastweek': { logger.info('Getting work hours by ticket for last 7 days'); const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); const dateFilter = sevenDaysAgo.toISOString().split('T')[0]; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,SITEID,woactivity.ESTDUR,woactivity.OWNER&lean=1`; endpoint += `&oslc.where=REPORTDATE>="${dateFilter}" and woactivity.owner="VKRISHNA"`; endpoint += `&oslc.pageSize=500`; logger.debug(`Work hours query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No work activities found for the last 7 days (since ${dateFilter})` } ] }; } const workByTicket = {}; response.member.forEach(ticket => { const ticketId = ticket.ticketid; const description = ticket.description; if (!workByTicket[ticketId]) { workByTicket[ticketId] = { description: description, totalHours: 0 }; } const activities = ticket.woactivity || []; const activityArray = Array.isArray(activities) ? activities : [activities]; activityArray.forEach(activity => { if (activity && activity.owner === "VKRISHNA" && activity.estdur) { workByTicket[ticketId].totalHours += parseFloat(activity.estdur) || 0; } }); }); const workSummary = Object.entries(workByTicket) .filter(([_, data]) => data.totalHours > 0) .map(([ticketId, data]) => `${ticketId} (${data.description.substring(0, 50)}...) → ${data.totalHours} hours`) .join('\n'); const totalHours = Object.values(workByTicket).reduce((sum, data) => sum + data.totalHours, 0); return { content: [ { type: 'text', text: `Work Done Last Week (since ${dateFilter}):\n\n${workSummary}\n\nTotal Hours: ${totalHours}` } ] }; } case 'get_work_done_lastweek_bycustomer': { logger.info('Getting work hours by customer/site for last 7 days'); const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); const dateFilter = sevenDaysAgo.toISOString().split('T')[0]; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,SITEID,woactivity.ESTDUR,woactivity.OWNER&lean=1`; endpoint += `&oslc.where=REPORTDATE>="${dateFilter}" and woactivity.owner="VKRISHNA"`; endpoint += `&oslc.pageSize=500`; logger.debug(`Work hours by customer query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No work activities found for the last 7 days (since ${dateFilter})` } ] }; } const workBySite = {}; response.member.forEach(ticket => { const siteId = ticket.siteid; if (!workBySite[siteId]) { workBySite[siteId] = 0; } const activities = ticket.woactivity || []; const activityArray = Array.isArray(activities) ? activities : [activities]; activityArray.forEach(activity => { if (activity && activity.owner === "VKRISHNA" && activity.estdur) { workBySite[siteId] += parseFloat(activity.estdur) || 0; } }); }); const workSummary = Object.entries(workBySite) .filter(([_, hours]) => hours > 0) .sort(([,a], [,b]) => b - a) .map(([siteId, hours]) => `${siteId} → ${hours} hours`) .join('\n'); const totalHours = Object.values(workBySite).reduce((sum, hours) => sum + hours, 0); return { content: [ { type: 'text', text: `Work Done Last Week by Customer (since ${dateFilter}):\n\n${workSummary}\n\nTotal Hours: ${totalHours}` } ] }; } case 'get_tickets_daterange': { logger.info(`Getting tickets for date range: ${args.start_date} to ${args.end_date}`); const datePattern = /^\d{4}-\d{2}-\d{2}$/; if (!datePattern.test(args.start_date) || !datePattern.test(args.end_date)) { return { content: [ { type: 'text', text: 'Error: Dates must be in YYYY-MM-DD format (e.g., 2025-06-01)' } ] }; } const startDate = new Date(args.start_date); const endDate = new Date(args.end_date); if (startDate > endDate) { return { content: [ { type: 'text', text: 'Error: Start date must be before or equal to end date' } ] }; } const limit = args.limit || 100; const includeWorkHours = args.include_work_hours || false; const statusFilter = args.status_filter || 'all'; const selectFields = includeWorkHours ? 'TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE,REPORTDATE,SITEID,woactivity.ESTDUR,woactivity.OWNER' : 'TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,ZP_PRODUCTION,ZP_SRTYPE,REPORTDATE,SITEID'; let endpoint = `/maximo/oslc/os/MXSR?oslc.select=${selectFields}&lean=1`; let whereClause = `woactivity.owner="VKRISHNA" and REPORTDATE>="${args.start_date}" and REPORTDATE<="${args.end_date}"`; if (statusFilter === 'active') { whereClause += ` and status!="RESOLVED" and status!="CLOSED" and status!="CANCELLED"`; } else if (statusFilter !== 'all') { whereClause += ` and status="${statusFilter.toUpperCase()}"`; } endpoint += `&oslc.where=${encodeURIComponent(whereClause)}`; endpoint += `&oslc.pageSize=${limit}`; logger.debug(`Date range query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No tickets found between ${args.start_date} and ${args.end_date}` } ] }; } logger.info(`Retrieved ${response.member.length} tickets for date range`); if (includeWorkHours) { const ticketsWithHours = {}; let totalHours = 0; response.member.forEach(ticket => { const ticketId = ticket.ticketid; if (!ticketsWithHours[ticketId]) { ticketsWithHours[ticketId] = { description: ticket.description, status: ticket.status, priority: ticket.reportedpriority, site: ticket.siteid, reportDate: ticket.reportdate, hours: 0 }; } const activities = ticket.woactivity || []; const activityArray = Array.isArray(activities) ? activities : [activities]; activityArray.forEach(activity => { if (activity && activity.owner === "VKRISHNA" && activity.estdur) { const hours = parseFloat(activity.estdur) || 0; ticketsWithHours[ticketId].hours += hours; totalHours += hours; } }); }); const ticketSummary = Object.entries(ticketsWithHours) .map(([ticketId, data]) => `${ticketId} (${data.description.substring(0, 40)}...) → ${data.hours} hours\n` + ` Status: ${data.status} | Site: ${data.site} | Date: ${data.reportDate}` ) .join('\n\n'); return { content: [ { type: 'text', text: `Tickets from ${args.start_date} to ${args.end_date} with Work Hours:\n\n${ticketSummary}\n\nTotal Hours: ${totalHours}\nTotal Tickets: ${Object.keys(ticketsWithHours).length}` } ] }; } else { const ticketSummary = response.member.map(ticket => ({ ticketId: ticket.ticketid, description: ticket.description, status: ticket.status, priority: ticket.reportedpriority, site: ticket.siteid, reportDate: ticket.reportdate })); return { content: [ { type: 'text', text: `Tickets from ${args.start_date} to ${args.end_date} (${response.member.length} found):\n\n${JSON.stringify(ticketSummary, null, 2)}` } ] }; } } case 'get_tickets_by_work_activity_date': { logger.info(`Getting tickets by work activity date range: ${args.start_date} to ${args.end_date}`); const startDate = args.start_date; const endDate = args.end_date; const limit = args.limit || 100; const dateRegex = /^\d{4}-\d{2}-\d{2}$/; if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) { return { content: [ { type: 'text', text: 'Invalid date format. Please use YYYY-MM-DD format (e.g., 2025-06-15)' } ] }; } let endpoint = `/maximo/oslc/os/MXSR?oslc.select=TICKETID,DESCRIPTION,STATUS,REPORTEDPRIORITY,SITEID,REPORTDATE,woactivity{OWNER,ACTSTART,DESCRIPTION,ESTDUR}&lean=1`; endpoint += `&oslc.where=woactivity.ACTSTART>="${startDate}T00:00:00" and woactivity.ACTSTART<="${endDate}T23:59:59" and woactivity.OWNER="VKRISHNA"`; if (limit) { endpoint += `&oslc.paging=true&oslc.pageSize=${limit}`; } logger.debug(`Query endpoint: ${endpoint}`); const response = await makeMaximoRequest(endpoint); logger.debug('Raw API response:', JSON.stringify(response, null, 2)); if (!response.member || response.member.length === 0) { return { content: [ { type: 'text', text: `No tickets found with work activities between ${startDate} and ${endDate}` } ] }; } const tickets = response.member; logger.info(`Found ${tickets.length} tickets with work activities in date range`); if (tickets.length > 0) { logger.debug('First ticket structure:', JSON.stringify(tickets[0], null, 2)); } let result = `**Tickets with Work Activities: ${startDate} to ${endDate}**\n\n`; result += `Found ${tickets.length} tickets:\n\n`; tickets.forEach(ticket => { const ticketId = ticket.TICKETID || ticket.ticketid || ticket.TicketID || 'N/A'; const description = ticket.DESCRIPTION || ticket.description || ticket.Description || 'N/A'; const status = ticket.STATUS || ticket.status || ticket.Status || 'N/A'; const priority = ticket.REPORTEDPRIORITY || ticket.reportedpriority || ticket.ReportedPriority || 'N/A'; const siteId = ticket.SITEID || ticket.siteid || ticket.SiteID || 'N/A'; const workActivities = ticket.woactivity || ticket.WOACTIVITY || []; const relevantActivities = workActivities.filter(wa => wa.ACTSTART && wa.ACTSTART >= `${startDate}T00:00:00` && wa.ACTSTART <= `${endDate}T23:59:59` ); result += `**${ticketId}** - ${description}\n`; result += ` Site: ${siteId} | Status: ${status} | Priority: ${priority}\n`; if (relevantActivities.length > 0) { result += ` Work Activities:\n`; relevantActivities.forEach(wa => { const actDate = wa.ACTSTART ? new Date(wa.ACTSTART).toLocaleDateString() : 'N/A'; const hours = wa.ESTDUR || wa.estdur || 0; const waDesc = wa.DESCRIPTION || wa.description || 'No description'; result += ` • ${actDate}: ${waDesc} (${hours}h)\n`; }); } result += `\n`; }); return { content: [ { type: 'text', text: result } ] }; } case 'add_work_activity': { logger.info(`Adding work activity to ticket: ${args.ticket_id}`); const ticketEndpoint = `/maximo/oslc/os/MXSR?oslc.select=*&oslc.where=TICKETID="${args.ticket_id}"&lean=1`; logger.debug(`Getting ticket details: ${ticketEndpoint}`); const ticketResponse = await makeMaximoRequest(ticketEndpoint); if (!ticketResponse.member || ticketResponse.member.length === 0) { logger.warn(`Ticket ${args.ticket_id} not found`); return { content: [ { type: 'text', text: `Ticket ${args.ticket_id} not found. Please verify the ticket ID exists.` } ] }; } const ticket = ticketResponse.member[0]; const ticketHref = ticket.href; logger.info(`Found ticket ${args.ticket_id} in site ${ticket.siteid} with href: ${ticketHref}`); const now = new Date(); const reportDate = now.toISOString(); const actStart = new Date(now.getTime() - 5 * 60 * 1000).toISOString(); const workActivityPayload = { "woactivity": [ { "_action": "Add", "description": args.activity_description, "owner": "VKRISHNA", "estdur": parseFloat(args.hours), "actlabhrs": parseFloat(args.hours), "siteid": ticket.siteid || "BEDFORD", "orgid": ticket.orgid || "EAGLENA", "status": "INPRG", "reportdate": reportDate, "actstart": actStart, "woclass": "ACTIVITY", "worktype": "CM", "assignedownergroup": ticket.ownergroup || "", "parentwonum": args.ticket_id, "historyflag": false, "istask": false, "haschildren": false, "template": false } ] }; logger.debug('Work activity payload:', JSON.stringify(workActivityPayload, null, 2)); const resourcePath = ticketHref.replace(MAXIMO_HOST, ''); const updateEndpoint = `${resourcePath}?lean=1`; logger.debug(`Update endpoint: ${updateEndpoint}`); try { const response = await makeMaximoRequest(updateEndpoint, { method: 'POST', headers: { 'MAXAUTH': MAXAUTH, 'Content-Type': 'application/json', 'x-method-override': 'PATCH', 'patchtype': 'MERGE', 'properties': 'wonum,description,status' }, body: JSON.stringify(workActivityPayload) }); logger.info('Work activity update response received'); logger.debug('Update response:', JSON.stringify(response, null, 2)); const verifyEndpoint = `/maximo/oslc/os/MXSR?oslc.select=ticketid,woactivity{wonum,description,owner,estdur,status}&oslc.where=TICKETID="${args.ticket_id}"&lean=1`; logger.debug(`Verifying work activity creation: ${verifyEndpoint}`); const verifyResponse = await makeMaximoRequest(verifyEndpoint); if (verifyResponse && verifyResponse.member && verifyResponse.member.length > 0) { const updatedTicket = verifyResponse.member[0]; const workActivities = updatedTicket.woactivity || []; const latestActivity = workActivities.find(activity => activity.owner === "VKRISHNA" && activity.description === args.activity_description ); if (latestActivity) { logger.info(`Work activity successfully created with WONUM: ${latestActivity.wonum}`); const successMessage = `Work activity added successfully!\n\nSR Ticket: ${args.ticket_id}\nWork Activity: ${latestActivity.wonum}\nOwner: VKRISHNA\nHours: ${args.hours}\nDescription: ${args.activity_description}\nStatus: ${latestActivity.status}\nDate: ${new Date().toLocaleDateString()}`; return { content: [ { type: 'text', text: successMessage } ] }; } else { logger.warn('Work activity was not found in verification check'); return { content: [ { type: 'text', text: `Work activity may have been added to ticket ${args.ticket_id}, but verification failed. Please check the ticket manually.` } ] }; } } else { logger.error('Failed to verify work activity creation'); return { content: [ { type: 'text', text: `Failed to verify work activity creation for ticket ${args.ticket_id}. Please check the ticket manually.` } ] ]; } } catch (error) { logger.error('Error adding work activity:', error); let errorMessage = `Failed to add work activity to ticket ${args.ticket_id}.`; if (error.message) { errorMessage += `\n\nError details: ${error.message}`; } if (error.response) { errorMessage += `\nHTTP Status: ${error.response.status}`; } errorMessage += `\n\nPlease check:\n• Ticket ${args.ticket_id} exists and is accessible\n• You have permission to add work activities\n• The ticket is in a status that allows work activities`; return { content: [ { type: 'text', text: errorMessage } ] }; } } default: logger.error(`Unknown tool: ${name}`); throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { logger.error(`Error in tool ${name}:`, error); return { content: [ { type: 'text', text: `Error: ${error.message}\n\nFull error details logged to maximo-mcp-sse.log` } ], isError: true }; } }); // Create Express app for HTTP/SSE const app = express(); // Enable CORS for cross-origin requests app.use(cors({ origin: '*', // In production, specify your Claude Desktop origin credentials: true })); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'healthy', server: 'maximo-mcp-sse', version: '2.0.0', timestamp: new Date().toISOString() }); }); // SSE endpoint for MCP communication app.get('/sse', async (req, res) => { logger.info('New SSE connection established'); // Set up SSE headers res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type', }); try { // Create SSE transport const transport = new SSEServerTransport('/sse', res); // Connect MCP server to SSE transport await server.connect(transport); logger.info('MCP Server connected via SSE transport'); } catch (error) { logger.error('Failed to establish SSE connection:', error); res.write(`event: error\ndata: ${JSON.stringify({ error: error.message })}\n\n`); res.end(); } }); // Start the HTTP server async function startServer() { try { const server = app.listen(SERVER_PORT, SERVER_HOST, () => { logger.info(`Maximo MCP SSE Server running on http://${SERVER_HOST}:${SERVER_PORT}`); logger.info(`SSE endpoint available at: http://${SERVER_HOST}:${SERVER_PORT}/sse`); logger.info(`Health check available at: http://${SERVER_HOST}:${SERVER_PORT}/health`); }); // Graceful shutdown handling process.on('SIGTERM', () => { logger.info('SIGTERM received, shutting down gracefully'); server.close(() => { logger.info('Server closed'); process.exit(0); }); }); process.on('SIGINT', () => { logger.info('SIGINT received, shutting down gracefully'); server.close(() => { logger.info('Server closed'); process.exit(0); }); }); } catch (error) { logger.error('Failed to start server:', error); process.exit(1); } } startServer();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/vijaykrishnazpro/remote-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server