halton-iot-mcp-server
Version:
MCP Server for Halton IoT Data API - run with npx or Docker
303 lines • 11.3 kB
JavaScript
/**
* Halton IoT MCP Server
* Model Context Protocol server for Halton IoT Data API
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { HaltonApiClient } from "./api-client.js";
// Get API token from environment variable
const API_TOKEN = process.env.HALTON_API_TOKEN;
if (!API_TOKEN) {
console.error("Error: HALTON_API_TOKEN environment variable is required");
process.exit(1);
}
const apiClient = new HaltonApiClient(API_TOKEN);
// Create server instance
const server = new McpServer({
name: "halton-iot",
version: "1.0.0",
});
// ============ System Tools ============
server.tool("get_all_systems", "Get basic information about all accessible IoT systems. Use this first to discover available system IDs before querying variables or data.", {}, async () => {
try {
const result = await apiClient.getAllSystems();
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
server.tool("get_systems", "Get basic information about specified IoT systems including location, description and connection status.", {
systems: z
.array(z.string())
.describe("Array of system IDs to retrieve (e.g., ['FI02728', 'US02145'])"),
}, async ({ systems }) => {
try {
const result = await apiClient.getSystems(systems);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
// ============ Variable Tools ============
server.tool("get_system_variables", "IMPORTANT: Use this tool to discover the exact variable names available on a system BEFORE attempting to get values. Returns all variable metadata including names, units, data types, and display names. Variable names are system-specific and cannot be guessed - they must be retrieved using this tool or search_variables first.", {
systems: z
.array(z.string())
.describe("Array of system IDs to get variables for"),
}, async ({ systems }) => {
try {
const result = await apiClient.getSystemVariables(systems);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
server.tool("search_variables", "RECOMMENDED: Search for variables by keyword to find exact variable names before querying data. Use this to find variables related to concepts like 'Airflow', 'Pressure', 'Temperature', 'UV', etc. Returns the exact variable names, units, and data types needed for get_last_value or get_aggregate_data calls. Always search first - never guess variable names.", {
systems: z.array(z.string()).describe("Array of system IDs to search in"),
searchWords: z
.array(z.string())
.describe("Array of search terms (e.g., ['Airflow', 'Temperature', 'Pressure', 'UV'])"),
pageNumber: z.number().optional().default(1).describe("Page number for pagination"),
pageSize: z.number().optional().default(100).describe("Number of results per page"),
includeLastEventValues: z
.boolean()
.optional()
.default(false)
.describe("Include the last recorded value for each variable"),
}, async ({ systems, searchWords, pageNumber, pageSize, includeLastEventValues }) => {
try {
const result = await apiClient.searchSystemVariables({
systems,
searchWords,
pagination: {
pageNumber: pageNumber ?? 1,
pageSize: pageSize ?? 100,
orderByType: "Ascending",
},
}, includeLastEventValues);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
// ============ Time Series Data Tools ============
server.tool("get_aggregate_data", "Get aggregated time series data for a single variable (includes min, max, avg, median, etc.). Default interval is hourly. PREREQUISITE: You must first use search_variables or get_system_variables to discover the exact variable name - do not guess variable names as they are system-specific.", {
variable: z.string().describe("The exact variable name from search_variables or get_system_variables (e.g., 'M01_H01_UV_DuctDiffPress_MR')"),
systems: z.array(z.string()).describe("Array of system IDs"),
fromDate: z.string().describe("Start date/time in ISO 8601 format"),
toDate: z.string().describe("End date/time in ISO 8601 format"),
intervalSeconds: z
.number()
.optional()
.default(3600)
.describe("Aggregation interval in seconds (default: 3600 = hourly)"),
}, async ({ variable, systems, fromDate, toDate, intervalSeconds }) => {
try {
const result = await apiClient.getAggregateSeries(variable, {
systems,
dateTimeRange: {
fromProperty: fromDate,
to: toDate,
},
intervalSeconds: intervalSeconds ?? 3600,
});
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
server.tool("get_aggregate_data_multiple", "Get aggregated time series data for multiple variables. Default interval is hourly. PREREQUISITE: You must first use search_variables or get_system_variables to discover exact variable names - do not guess variable names as they are system-specific.", {
variables: z
.array(z.string())
.describe("Array of exact variable names from search_variables or get_system_variables"),
systems: z.array(z.string()).describe("Array of system IDs"),
fromDate: z.string().describe("Start date/time in ISO 8601 format"),
toDate: z.string().describe("End date/time in ISO 8601 format"),
intervalSeconds: z
.number()
.optional()
.default(3600)
.describe("Aggregation interval in seconds (default: 3600 = hourly)"),
}, async ({ variables, systems, fromDate, toDate, intervalSeconds }) => {
try {
const result = await apiClient.getAggregateSeriesMultiple({
variables,
systems,
dateTimeRange: {
fromProperty: fromDate,
to: toDate,
},
intervalSeconds: intervalSeconds ?? 3600,
});
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
server.tool("get_last_value", "Get the most recent value for a single variable. PREREQUISITE: You must first use search_variables or get_system_variables to discover the exact variable name - do not guess variable names as they are system-specific and will fail if incorrect.", {
variable: z.string().describe("The exact variable name from search_variables or get_system_variables (e.g., 'M01_H01_UV_DuctDiffPress_MR')"),
systems: z.array(z.string()).describe("Array of system IDs"),
}, async ({ variable, systems }) => {
try {
const result = await apiClient.getLastEventValue(variable, systems);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
server.tool("get_last_values_multiple", "Get the most recent values for multiple variables. PREREQUISITE: You must first use search_variables or get_system_variables to discover exact variable names - do not guess variable names as they are system-specific and will fail if incorrect.", {
variables: z
.array(z.string())
.describe("Array of exact variable names from search_variables or get_system_variables"),
systems: z.array(z.string()).describe("Array of system IDs"),
}, async ({ variables, systems }) => {
try {
const result = await apiClient.getLastEventValueMultiple({
variables,
systems,
});
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
// ============ Main ============
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Halton IoT MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
//# sourceMappingURL=index.js.map