@etm-professional-control/winccoa-mcp-server
Version:
MCP Server for WinCC OA with field-specific configurations
438 lines • 18.3 kB
JavaScript
/**
* Pmon TCP Client
*
* Client for communicating with WinCC OA Process Monitor (Pmon) via TCP protocol.
* Supports all Pmon commands for manager administration.
*/
import * as net from 'net';
export class PmonClient {
constructor(config = {}) {
this.host = config.host || process.env.WINCCOA_PMON_HOST || 'localhost';
this.port = config.port || parseInt(process.env.WINCCOA_PMON_PORT || '4999', 10);
this.user = config.user || process.env.WINCCOA_PMON_USER || '';
this.password = config.password || process.env.WINCCOA_PMON_PASSWORD || '';
this.timeout = config.timeout || 5000;
}
/**
* Send a command to Pmon via TCP
* @param command - The Pmon protocol command to send
* @returns Promise with the raw response string
*/
async sendCommand(command) {
return new Promise((resolve, reject) => {
const client = new net.Socket();
let response = '';
let dataReceived = false;
// Build authentication prefix
// According to docs: user#cleartextPassword#<command>
// If no user/password, use ##<command>
const authPrefix = (this.user || this.password)
? `${this.user}#${this.password}#`
: '##';
const fullCommand = authPrefix + command;
// Set timeout
const timeoutHandle = setTimeout(() => {
client.destroy();
// If we received data, return it instead of timing out
if (dataReceived && response.length > 0) {
resolve(response);
}
else {
reject(new Error(`Connection timeout after ${this.timeout}ms`));
}
}, this.timeout);
// Connect to Pmon
client.connect(this.port, this.host, () => {
client.write(fullCommand + '\n');
});
// Receive data
client.on('data', (data) => {
dataReceived = true;
response += data.toString();
// Check if response looks complete (ends with semicolon on its own line for LIST commands)
// or if it's a simple response that's complete
if (response.includes('\n;') || response.endsWith(';')) {
clearTimeout(timeoutHandle);
client.end();
resolve(response);
}
});
// Connection closed by server
client.on('end', () => {
clearTimeout(timeoutHandle);
if (dataReceived) {
resolve(response);
}
});
// Handle errors
client.on('error', (err) => {
clearTimeout(timeoutHandle);
reject(new Error(`Pmon connection error: ${err.message}`));
});
});
}
/**
* Get list of all managers with their status
* @returns Promise with parsed manager status
*/
async getManagerStatus() {
try {
const response = await this.sendCommand('MGRLIST:STATI');
return this.parseManagerStatus(response);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get manager status: ${errorMessage}`);
}
}
/**
* Get list of all managers with their configuration
* @returns Promise with manager list
*/
async getManagerList() {
try {
const response = await this.sendCommand('MGRLIST:LIST');
return this.parseManagerList(response);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get manager list: ${errorMessage}`);
}
}
/**
* Add a new manager to the Pmon configuration
* @param index - Position where to insert (1-based, 0 is Pmon itself)
* @param manager - Manager name (without .exe extension)
* @param startMode - Start mode: manual, once, or always
* @param secKill - Seconds to wait before SIGKILL (default: 30)
* @param restartCount - Number of restart attempts (default: 3)
* @param resetMin - Minutes to reset restart counter (default: 5)
* @param options - Command line options (default: '')
* @returns Promise with operation result
*/
async addManager(index, manager, startMode = 'always', secKill = 30, restartCount = 3, resetMin = 5, options = '') {
try {
// Validate inputs
if (index < 1 || index > 100) {
throw new Error('Manager index must be between 1 and 100');
}
if (!manager || manager.trim() === '') {
throw new Error('Manager name is required');
}
const command = `SINGLE_MGR:INS ${index} ${manager} ${startMode} ${secKill} ${restartCount} ${resetMin} ${options}`;
const response = await this.sendCommand(command);
return {
success: true,
data: response.trim()
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
error: errorMessage
};
}
}
/**
* Remove a manager from the Pmon configuration
* @param index - Manager index to remove (1-based)
* @returns Promise with operation result
*/
async removeManager(index) {
try {
if (index < 1) {
throw new Error('Manager index must be at least 1 (cannot remove Pmon itself)');
}
const command = `SINGLE_MGR:DEL ${index}`;
const response = await this.sendCommand(command);
return {
success: true,
data: response.trim()
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
error: errorMessage
};
}
}
/**
* Start a manager
* @param index - Manager index to start (1-based)
* @returns Promise with operation result
*/
async startManager(index) {
try {
if (index < 1) {
throw new Error('Manager index must be at least 1');
}
const command = `SINGLE_MGR:START ${index}`;
const response = await this.sendCommand(command);
return {
success: true,
data: response.trim()
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
error: errorMessage
};
}
}
/**
* Stop a manager (sends SIGTERM)
* @param index - Manager index to stop (1-based)
* @param ownManagerNumber - Optional: Own manager number to prevent self-stop
* @returns Promise with operation result
*/
async stopManager(index, ownManagerNumber) {
try {
if (index < 1) {
throw new Error('Manager index must be at least 1');
}
// Safety check: prevent stopping own manager
if (ownManagerNumber !== undefined && ownManagerNumber !== null) {
try {
console.log(`🔒 [DEBUG] stopManager: Safety check - ownManagerNumber=${ownManagerNumber}, targetIndex=${index}`);
const managerStatus = await this.getManagerStatus();
// Find the manager by its index field, not array position
const targetManager = managerStatus.managers.find(m => m.index === index);
console.log(`🔒 [DEBUG] stopManager: Target manager found:`, targetManager ? `index=${targetManager.index}, manNum=${targetManager.manNum}, pid=${targetManager.pid}` : 'NOT FOUND');
if (targetManager && targetManager.manNum === ownManagerNumber) {
console.log(`🚫 [DEBUG] stopManager: BLOCKED! Attempt to stop own manager detected!`);
return {
success: false,
error: `Cannot stop own manager (index ${index}, manager number ${ownManagerNumber}). This would terminate the MCP server and prevent any response.`
};
}
console.log(`✅ [DEBUG] stopManager: Safety check passed, different manager`);
}
catch (checkError) {
// If we can't verify, log warning but continue (better safe than sorry approach)
console.warn('Could not verify manager identity for safety check:', checkError);
}
}
const command = `SINGLE_MGR:STOP ${index}`;
const response = await this.sendCommand(command);
return {
success: true,
data: response.trim()
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
error: errorMessage
};
}
}
/**
* Kill a manager (sends SIGKILL)
* @param index - Manager index to kill (1-based)
* @param ownManagerNumber - Optional: Own manager number to prevent self-kill
* @returns Promise with operation result
*/
async killManager(index, ownManagerNumber) {
try {
if (index < 1) {
throw new Error('Manager index must be at least 1');
}
// Safety check: prevent killing own manager
if (ownManagerNumber !== undefined && ownManagerNumber !== null) {
try {
console.log(`🔒 [DEBUG] killManager: Safety check - ownManagerNumber=${ownManagerNumber}, targetIndex=${index}`);
const managerStatus = await this.getManagerStatus();
// Find the manager by its index field, not array position
const targetManager = managerStatus.managers.find(m => m.index === index);
console.log(`🔒 [DEBUG] killManager: Target manager found:`, targetManager ? `index=${targetManager.index}, manNum=${targetManager.manNum}, pid=${targetManager.pid}` : 'NOT FOUND');
if (targetManager && targetManager.manNum === ownManagerNumber) {
console.log(`🚫 [DEBUG] killManager: BLOCKED! Attempt to kill own manager detected!`);
return {
success: false,
error: `Cannot kill own manager (index ${index}, manager number ${ownManagerNumber}). This would terminate the MCP server and prevent any response.`
};
}
console.log(`✅ [DEBUG] killManager: Safety check passed, different manager`);
}
catch (checkError) {
// If we can't verify, log warning but continue (better safe than sorry approach)
console.warn('Could not verify manager identity for safety check:', checkError);
}
}
const command = `SINGLE_MGR:KILL ${index}`;
const response = await this.sendCommand(command);
return {
success: true,
data: response.trim()
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
error: errorMessage
};
}
}
/**
* Get manager properties
* @param index - Manager index (1-based)
* @returns Promise with manager properties
*/
async getManagerProperties(index) {
try {
if (index < 1) {
throw new Error('Manager index must be at least 1');
}
const command = `SINGLE_MGR:PROP_GET ${index}`;
const response = await this.sendCommand(command);
return this.parseManagerProperties(response);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get manager properties: ${errorMessage}`);
}
}
/**
* Update manager properties
* @param index - Manager index (1-based)
* @param startMode - Start mode: manual, once, or always
* @param secKill - Seconds to wait before SIGKILL
* @param restartCount - Number of restart attempts
* @param resetMin - Minutes to reset restart counter
* @param options - Command line options
* @returns Promise with operation result
*/
async updateManagerProperties(index, startMode, secKill, restartCount, resetMin, options = '') {
try {
if (index < 1) {
throw new Error('Manager index must be at least 1');
}
const command = `SINGLE_MGR:PROP_PUT ${index} ${startMode} ${secKill} ${restartCount} ${resetMin} ${options}`;
const response = await this.sendCommand(command);
return {
success: true,
data: response.trim()
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
error: errorMessage
};
}
}
/**
* Parse MGRLIST:STATI response into structured data
* @param response - Raw response from Pmon
* @returns Parsed manager status
*/
parseManagerStatus(response) {
const lines = response.trim().split('\n');
const managers = [];
// First line should be "LIST:<count>"
if (!lines[0] || !lines[0].startsWith('LIST:')) {
throw new Error(`Invalid MGRLIST:STATI response format. First line: ${JSON.stringify(lines[0])}`);
}
const count = parseInt(lines[0].substring(5), 10);
// Parse manager lines
for (let i = 1; i < lines.length; i++) {
const line = lines[i]?.trim();
// Skip empty lines
if (!line)
continue;
// Check for status line (ends with ;)
if (line.endsWith(';')) {
// This is the final status line
const parts = line.slice(0, -1).trim().split(/\s+/);
return {
managers,
modeNumeric: parseInt(parts[0] || '0', 10),
modeString: parts[1] || 'UNKNOWN',
emergencyActive: parseInt(parts[2] || '0', 10),
demoModeActive: parseInt(parts[3] || '0', 10)
};
}
// Parse manager line: <state>;<PID>;<startMode>;<startTime>;<manNum>
const parts = line.split(';');
if (parts.length >= 5) {
managers.push({
index: i - 1, // Index in the list
state: parseInt(parts[0] || '0', 10),
pid: parseInt(parts[1] || '0', 10),
startMode: parseInt(parts[2] || '0', 10),
startTime: parts[3] || '',
manNum: parseInt(parts[4] || '0', 10)
});
}
}
// Default return if parsing didn't complete normally
return {
managers,
modeNumeric: 0,
modeString: 'UNKNOWN',
emergencyActive: 0,
demoModeActive: 0
};
}
/**
* Parse MGRLIST:LIST response into structured data
* @param response - Raw response from Pmon
* @returns Parsed manager list
*/
parseManagerList(response) {
const lines = response.trim().split('\n');
const managers = [];
// First line should be "LIST:<count>"
if (!lines[0] || !lines[0].startsWith('LIST:')) {
throw new Error('Invalid MGRLIST:LIST response format');
}
// Parse manager lines
for (let i = 1; i < lines.length; i++) {
const line = lines[i]?.trim();
// Skip empty lines and terminator
if (!line || line === ';')
continue;
// Parse manager line: <manager>;<startMode>;<secKill>;<restartCount>;<resetMin>;<CommandlineOptions>
const parts = line.split(';');
if (parts.length >= 5) {
managers.push({
index: i - 1,
manager: parts[0] || '',
startMode: parts[1] || '',
secKill: parseInt(parts[2] || '0', 10),
restartCount: parseInt(parts[3] || '0', 10),
resetMin: parseInt(parts[4] || '0', 10),
commandlineOptions: parts.slice(5).join(';') // Rejoin in case options contain semicolons
});
}
}
return managers;
}
/**
* Parse SINGLE_MGR:PROP_GET response into structured data
* @param response - Raw response from Pmon
* @returns Parsed manager properties
*/
parseManagerProperties(response) {
const parts = response.trim().split(/\s+/);
if (parts.length < 4) {
throw new Error('Invalid PROP_GET response format');
}
return {
startMode: parts[0] || '',
secKill: parseInt(parts[1] || '0', 10),
restartCount: parseInt(parts[2] || '0', 10),
resetMin: parseInt(parts[3] || '0', 10),
commandlineOptions: parts.slice(4).join(' ')
};
}
}
//# sourceMappingURL=PmonClient.js.map