@etm-professional-control/winccoa-mcp-server
Version:
MCP Server for WinCC OA with field-specific configurations
155 lines (148 loc) • 6.37 kB
JavaScript
/**
* Archive Query Tool
*
* MCP tool for querying historical archived data from datapoint elements.
*/
import { z } from 'zod';
import { createSuccessResponse, createErrorResponse } from '../../utils/helpers.js';
/**
* Query archived data for datapoint elements using dpGetPeriodSplit
*/
async function queryArchiveData(winccoa, dpe, startTime, endTime, count = 0) {
console.log(`Querying archive data for ${Array.isArray(dpe) ? dpe.length : 1} datapoint(s)`);
console.log(`Time range: ${startTime} to ${endTime}`);
// Parse time strings to Date objects
const start = new Date(startTime);
const end = new Date(endTime);
// Validate dates
if (isNaN(start.getTime())) {
throw new Error(`Invalid start time: ${startTime}. Use ISO 8601 format (e.g., "2024-01-01T00:00:00Z")`);
}
if (isNaN(end.getTime())) {
throw new Error(`Invalid end time: ${endTime}. Use ISO 8601 format (e.g., "2024-12-31T23:59:59Z")`);
}
if (start >= end) {
throw new Error('Start time must be before end time');
}
// Convert to array for processing
const dpeArray = Array.isArray(dpe) ? dpe : [dpe];
// Validate that all DPEs exist
for (const d of dpeArray) {
if (!winccoa.dpExists(d)) {
throw new Error(`DPE ${d} does not exist in the system`);
}
}
// First call to dpGetPeriodSplit
console.log('Starting archive query (chunked retrieval)...');
let result = await winccoa.dpGetPeriodSplit(start, end, dpeArray, count);
// Accumulate all data chunks
const allData = [];
// Add first chunk
if (result.data && result.data.length > 0) {
allData.push(...result.data);
}
console.log(`Progress: ${result.progress}%`);
// Loop until all data is retrieved (progress reaches 100%)
while (result.progress !== 100) {
result = await winccoa.dpGetPeriodSplit(result.id);
if (result.data && result.data.length > 0) {
allData.push(...result.data);
}
console.log(`Progress: ${result.progress}%`);
}
console.log(`✓ Archive query complete. Retrieved data for ${allData.length} datapoint(s)`);
// Format results for each DPE
const results = allData.map((dpeData, index) => {
const values = dpeData.values || [];
const timestamps = dpeData.times || [];
return {
dpe: dpeArray[index],
values: values,
timestamps: timestamps.map((t) => t instanceof Date ? t.toISOString() : t),
count: values.length
};
});
// Calculate total values across all DPEs
const totalValues = results.reduce((sum, r) => sum + r.count, 0);
return {
results: Array.isArray(dpe) ? results : results[0],
metadata: {
startTime: start.toISOString(),
endTime: end.toISOString(),
totalDatapoints: dpeArray.length,
totalValues: totalValues
}
};
}
/**
* Register archive query tools
* @param server - MCP server instance
* @param context - Server context with winccoa, configs, etc.
* @returns Number of tools registered
*/
export function registerTools(server, context) {
const { winccoa } = context;
server.tool("archive-query", `Query historical archived data from WinCC OA datapoint elements.
Uses chunked retrieval (dpGetPeriodSplit) to handle large datasets without timeouts.
Returns values with timestamps for the specified time period.
Time Format: ISO 8601 strings (e.g., "2024-01-01T00:00:00Z")
Supports: Single DPE or multiple DPEs (array)
Examples:
Single datapoint query:
{
"dpe": "System1:Temperature.",
"startTime": "2024-01-01T00:00:00Z",
"endTime": "2024-01-31T23:59:59Z"
}
Multiple datapoints:
{
"dpe": ["System1:Temperature.", "System1:Pressure."],
"startTime": "2024-01-01T00:00:00Z",
"endTime": "2024-01-31T23:59:59Z"
}
With context values (values before/after period):
{
"dpe": "System1:Temperature.",
"startTime": "2024-01-01T00:00:00Z",
"endTime": "2024-01-31T23:59:59Z",
"count": 5
}
NOTE: Datapoint must have archive configuration enabled (use archive-set tool).
Returns empty values array if no archived data exists for the period.
`, {
config: z.union([
z.object({
dpe: z.union([z.string(), z.array(z.string())]).describe('Datapoint element(s) to query'),
startTime: z.string().describe('Start time (ISO 8601: "2024-01-01T00:00:00Z")'),
endTime: z.string().describe('End time (ISO 8601: "2024-12-31T23:59:59Z")'),
count: z.number().optional().describe('Optional: Extra values before/after period (default: 0)')
}),
z.string()
])
}, async ({ config }) => {
try {
// Parse string if needed
let parsedConfig = typeof config === 'string' ? JSON.parse(config) : config;
console.log('========================================');
console.log('Querying Archive Data');
console.log('========================================');
console.log(`DPE(s): ${Array.isArray(parsedConfig.dpe) ? parsedConfig.dpe.join(', ') : parsedConfig.dpe}`);
console.log(`Period: ${parsedConfig.startTime} to ${parsedConfig.endTime}`);
const result = await queryArchiveData(winccoa, parsedConfig.dpe, parsedConfig.startTime, parsedConfig.endTime, parsedConfig.count || 0);
console.log('========================================');
console.log('✓ Archive Query Complete');
console.log('========================================');
return createSuccessResponse(result);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('========================================');
console.error('✗ Archive Query Failed');
console.error('========================================');
console.error(`Error: ${errorMessage}`);
return createErrorResponse(`Failed to query archive data: ${errorMessage}`);
}
});
return 1; // Number of tools registered
}
//# sourceMappingURL=archive_query.js.map