#!/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();