@access-mcp/system-status 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -3
- package/dist/__tests__/server.integration.test.d.ts +1 -0
- package/dist/__tests__/server.integration.test.js +225 -0
- package/dist/__tests__/server.test.d.ts +1 -0
- package/dist/__tests__/server.test.js +380 -0
- package/dist/index.js +4 -5
- package/dist/server.d.ts +28 -0
- package/dist/server.js +275 -62
- package/package.json +16 -1
package/README.md
CHANGED
|
@@ -1,10 +1,44 @@
|
|
|
1
1
|
# System Status MCP Server
|
|
2
2
|
|
|
3
|
-
MCP server providing real-time system status information for ACCESS-CI resources.
|
|
3
|
+
MCP server providing real-time system status information for ACCESS-CI resources. Provides critical operational information about ACCESS-CI systems, including current outages, scheduled maintenance, and system-wide announcements.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Usage Examples
|
|
6
|
+
|
|
7
|
+
### **Monitor Current Issues**
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
"Are there any current outages on ACCESS-CI?"
|
|
11
|
+
"Is Delta currently operational?"
|
|
12
|
+
"What systems are experiencing issues right now?"
|
|
13
|
+
"Show me all systems that are down"
|
|
14
|
+
```
|
|
6
15
|
|
|
7
|
-
|
|
16
|
+
### **Track Maintenance Windows**
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
"When is the next maintenance for Expanse?"
|
|
20
|
+
"Show me all scheduled maintenance for this week"
|
|
21
|
+
"Is there upcoming maintenance on Bridges-2?"
|
|
22
|
+
"What maintenance is planned for GPU systems?"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### **System Announcements**
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
"What are the latest system announcements?"
|
|
29
|
+
"Are there any important notices for ACCESS users?"
|
|
30
|
+
"Show me recent updates about system changes"
|
|
31
|
+
"Any policy updates I should know about?"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### **Check Resource Status**
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
"What's the current status of Anvil?"
|
|
38
|
+
"Is Frontera available for job submission?"
|
|
39
|
+
"Check if all GPU systems are operational"
|
|
40
|
+
"Get status for all TACC resources"
|
|
41
|
+
```
|
|
8
42
|
|
|
9
43
|
## Tools
|
|
10
44
|
|
|
@@ -16,6 +50,14 @@ Get current system outages and issues affecting ACCESS-CI resources.
|
|
|
16
50
|
|
|
17
51
|
- `resource_filter` (string, optional): Filter by specific resource name or ID
|
|
18
52
|
|
|
53
|
+
**Example:**
|
|
54
|
+
```typescript
|
|
55
|
+
// User: "Are there any current outages on Delta?"
|
|
56
|
+
const outages = await get_current_outages({
|
|
57
|
+
resource_filter: "delta"
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
19
61
|
### get_scheduled_maintenance
|
|
20
62
|
|
|
21
63
|
Get scheduled maintenance and future outages for ACCESS-CI resources.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { SystemStatusServer } from "../server.js";
|
|
3
|
+
describe("SystemStatusServer Integration Tests", () => {
|
|
4
|
+
let server;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
server = new SystemStatusServer();
|
|
7
|
+
});
|
|
8
|
+
describe("Real API Integration", () => {
|
|
9
|
+
it("should fetch current outages from real API", async () => {
|
|
10
|
+
const result = await server["handleToolCall"]({
|
|
11
|
+
params: {
|
|
12
|
+
name: "get_current_outages",
|
|
13
|
+
arguments: { limit: 5 }
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
const responseData = JSON.parse(result.content[0].text);
|
|
17
|
+
expect(responseData).toHaveProperty("total_outages");
|
|
18
|
+
expect(responseData).toHaveProperty("affected_resources");
|
|
19
|
+
expect(responseData).toHaveProperty("severity_counts");
|
|
20
|
+
expect(responseData.severity_counts).toHaveProperty("high");
|
|
21
|
+
expect(responseData.severity_counts).toHaveProperty("medium");
|
|
22
|
+
expect(responseData.severity_counts).toHaveProperty("low");
|
|
23
|
+
expect(responseData.severity_counts).toHaveProperty("unknown");
|
|
24
|
+
expect(Array.isArray(responseData.outages)).toBe(true);
|
|
25
|
+
// Check enhanced fields
|
|
26
|
+
if (responseData.outages.length > 0) {
|
|
27
|
+
const outage = responseData.outages[0];
|
|
28
|
+
expect(outage).toHaveProperty("severity");
|
|
29
|
+
expect(outage).toHaveProperty("posted_time");
|
|
30
|
+
expect(outage).toHaveProperty("last_updated");
|
|
31
|
+
}
|
|
32
|
+
}, 10000);
|
|
33
|
+
it("should fetch scheduled maintenance from real API", async () => {
|
|
34
|
+
const result = await server["handleToolCall"]({
|
|
35
|
+
params: {
|
|
36
|
+
name: "get_scheduled_maintenance",
|
|
37
|
+
arguments: { limit: 5 }
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
const responseData = JSON.parse(result.content[0].text);
|
|
41
|
+
expect(responseData).toHaveProperty("total_scheduled");
|
|
42
|
+
expect(responseData).toHaveProperty("upcoming_24h");
|
|
43
|
+
expect(responseData).toHaveProperty("upcoming_week");
|
|
44
|
+
expect(responseData).toHaveProperty("affected_resources");
|
|
45
|
+
expect(Array.isArray(responseData.maintenance)).toBe(true);
|
|
46
|
+
// Check enhanced fields
|
|
47
|
+
if (responseData.maintenance.length > 0) {
|
|
48
|
+
const maintenance = responseData.maintenance[0];
|
|
49
|
+
expect(maintenance).toHaveProperty("hours_until_start");
|
|
50
|
+
expect(maintenance).toHaveProperty("has_scheduled_time");
|
|
51
|
+
expect(maintenance.hours_until_start).toSatisfy((val) => val === null || typeof val === "number");
|
|
52
|
+
}
|
|
53
|
+
}, 10000);
|
|
54
|
+
it("should fetch past outages from real API", async () => {
|
|
55
|
+
const result = await server["handleToolCall"]({
|
|
56
|
+
params: {
|
|
57
|
+
name: "get_past_outages",
|
|
58
|
+
arguments: { limit: 10 }
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
const responseData = JSON.parse(result.content[0].text);
|
|
62
|
+
expect(responseData).toHaveProperty("total_past_outages");
|
|
63
|
+
expect(responseData).toHaveProperty("recent_outages_30_days");
|
|
64
|
+
expect(responseData).toHaveProperty("affected_resources");
|
|
65
|
+
expect(responseData).toHaveProperty("outage_types");
|
|
66
|
+
expect(responseData).toHaveProperty("average_duration_hours");
|
|
67
|
+
expect(Array.isArray(responseData.outages)).toBe(true);
|
|
68
|
+
// Check enhanced fields
|
|
69
|
+
if (responseData.outages.length > 0) {
|
|
70
|
+
const outage = responseData.outages[0];
|
|
71
|
+
expect(outage).toHaveProperty("duration_hours");
|
|
72
|
+
expect(outage).toHaveProperty("days_ago");
|
|
73
|
+
expect(outage).toHaveProperty("outage_type");
|
|
74
|
+
expect(outage.days_ago).toSatisfy((val) => val === null || typeof val === "number");
|
|
75
|
+
}
|
|
76
|
+
}, 10000);
|
|
77
|
+
it("should get comprehensive system announcements", async () => {
|
|
78
|
+
const result = await server["handleToolCall"]({
|
|
79
|
+
params: {
|
|
80
|
+
name: "get_system_announcements",
|
|
81
|
+
arguments: { limit: 20 }
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
const responseData = JSON.parse(result.content[0].text);
|
|
85
|
+
expect(responseData).toHaveProperty("total_announcements");
|
|
86
|
+
expect(responseData).toHaveProperty("current_outages");
|
|
87
|
+
expect(responseData).toHaveProperty("scheduled_maintenance");
|
|
88
|
+
expect(responseData).toHaveProperty("recent_past_outages");
|
|
89
|
+
expect(responseData).toHaveProperty("categories");
|
|
90
|
+
expect(responseData.categories).toHaveProperty("current");
|
|
91
|
+
expect(responseData.categories).toHaveProperty("scheduled");
|
|
92
|
+
expect(responseData.categories).toHaveProperty("recent_past");
|
|
93
|
+
expect(Array.isArray(responseData.announcements)).toBe(true);
|
|
94
|
+
// Check categorization
|
|
95
|
+
if (responseData.announcements.length > 0) {
|
|
96
|
+
const announcement = responseData.announcements[0];
|
|
97
|
+
expect(announcement).toHaveProperty("category");
|
|
98
|
+
expect(["current", "scheduled", "recent_past"]).toContain(announcement.category);
|
|
99
|
+
}
|
|
100
|
+
}, 15000);
|
|
101
|
+
it("should check resource status with direct method", async () => {
|
|
102
|
+
// Test with common resource names that might exist
|
|
103
|
+
const result = await server["handleToolCall"]({
|
|
104
|
+
params: {
|
|
105
|
+
name: "check_resource_status",
|
|
106
|
+
arguments: {
|
|
107
|
+
resource_ids: ["anvil", "bridges", "jetstream"],
|
|
108
|
+
use_group_api: false
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
const responseData = JSON.parse(result.content[0].text);
|
|
113
|
+
expect(responseData).toHaveProperty("checked_at");
|
|
114
|
+
expect(responseData).toHaveProperty("resources_checked", 3);
|
|
115
|
+
expect(responseData).toHaveProperty("operational");
|
|
116
|
+
expect(responseData).toHaveProperty("affected");
|
|
117
|
+
expect(responseData).toHaveProperty("api_method", "direct_outages_check");
|
|
118
|
+
expect(Array.isArray(responseData.resource_status)).toBe(true);
|
|
119
|
+
expect(responseData.resource_status).toHaveLength(3);
|
|
120
|
+
// Check resource status structure
|
|
121
|
+
responseData.resource_status.forEach((resource) => {
|
|
122
|
+
expect(resource).toHaveProperty("resource_id");
|
|
123
|
+
expect(resource).toHaveProperty("status");
|
|
124
|
+
expect(["operational", "affected"]).toContain(resource.status);
|
|
125
|
+
expect(resource).toHaveProperty("active_outages");
|
|
126
|
+
expect(Array.isArray(resource.outage_details)).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
}, 10000);
|
|
129
|
+
it("should test group API functionality", async () => {
|
|
130
|
+
// Test group API with a resource that might have a group ID
|
|
131
|
+
const result = await server["handleToolCall"]({
|
|
132
|
+
params: {
|
|
133
|
+
name: "check_resource_status",
|
|
134
|
+
arguments: {
|
|
135
|
+
resource_ids: ["anvil"],
|
|
136
|
+
use_group_api: true
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
const responseData = JSON.parse(result.content[0].text);
|
|
141
|
+
expect(responseData).toHaveProperty("api_method", "resource_group_api");
|
|
142
|
+
expect(responseData).toHaveProperty("resources_checked", 1);
|
|
143
|
+
expect(responseData.resource_status).toHaveLength(1);
|
|
144
|
+
const resourceStatus = responseData.resource_status[0];
|
|
145
|
+
expect(resourceStatus).toHaveProperty("resource_id", "anvil");
|
|
146
|
+
expect(resourceStatus).toHaveProperty("api_method");
|
|
147
|
+
expect(["group_specific", "group_specific_failed"]).toContain(resourceStatus.api_method);
|
|
148
|
+
// If it succeeded, check structure
|
|
149
|
+
if (resourceStatus.api_method === "group_specific") {
|
|
150
|
+
expect(resourceStatus).toHaveProperty("status");
|
|
151
|
+
expect(["operational", "affected"]).toContain(resourceStatus.status);
|
|
152
|
+
}
|
|
153
|
+
// If it failed, check error handling
|
|
154
|
+
if (resourceStatus.api_method === "group_specific_failed") {
|
|
155
|
+
expect(resourceStatus.status).toBe("unknown");
|
|
156
|
+
expect(resourceStatus).toHaveProperty("error");
|
|
157
|
+
}
|
|
158
|
+
}, 10000);
|
|
159
|
+
it("should filter outages by resource correctly", async () => {
|
|
160
|
+
const result = await server["handleToolCall"]({
|
|
161
|
+
params: {
|
|
162
|
+
name: "get_current_outages",
|
|
163
|
+
arguments: { resource_filter: "anvil" }
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
const responseData = JSON.parse(result.content[0].text);
|
|
167
|
+
expect(responseData).toHaveProperty("total_outages");
|
|
168
|
+
// If there are any results, they should match the filter
|
|
169
|
+
responseData.outages.forEach((outage) => {
|
|
170
|
+
const matchesFilter = outage.Subject?.toLowerCase().includes("anvil") ||
|
|
171
|
+
outage.AffectedResources?.some((resource) => resource.ResourceName?.toLowerCase().includes("anvil") ||
|
|
172
|
+
resource.ResourceID?.toString().includes("anvil"));
|
|
173
|
+
expect(matchesFilter).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
}, 10000);
|
|
176
|
+
it("should handle resource reads for all endpoints", async () => {
|
|
177
|
+
const resources = [
|
|
178
|
+
"accessci://system-status",
|
|
179
|
+
"accessci://outages/current",
|
|
180
|
+
"accessci://outages/scheduled",
|
|
181
|
+
"accessci://outages/past"
|
|
182
|
+
];
|
|
183
|
+
for (const uri of resources) {
|
|
184
|
+
const result = await server["handleResourceRead"]({
|
|
185
|
+
params: { uri }
|
|
186
|
+
});
|
|
187
|
+
expect(result.contents).toHaveLength(1);
|
|
188
|
+
expect(result.contents[0]).toHaveProperty("uri", uri);
|
|
189
|
+
expect(result.contents[0]).toHaveProperty("mimeType");
|
|
190
|
+
expect(result.contents[0]).toHaveProperty("text");
|
|
191
|
+
if (uri !== "accessci://system-status") {
|
|
192
|
+
// JSON resources should have valid JSON
|
|
193
|
+
expect(() => JSON.parse(result.contents[0].text)).not.toThrow();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}, 15000);
|
|
197
|
+
});
|
|
198
|
+
describe("Edge Cases and Error Handling", () => {
|
|
199
|
+
it("should handle empty API responses", async () => {
|
|
200
|
+
// This tests the robustness of our logic with potentially empty responses
|
|
201
|
+
const result = await server["handleToolCall"]({
|
|
202
|
+
params: {
|
|
203
|
+
name: "get_current_outages",
|
|
204
|
+
arguments: { resource_filter: "nonexistent-resource-xyz" }
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
const responseData = JSON.parse(result.content[0].text);
|
|
208
|
+
expect(responseData).toHaveProperty("total_outages", 0);
|
|
209
|
+
expect(responseData.outages).toHaveLength(0);
|
|
210
|
+
expect(responseData.affected_resources).toHaveLength(0);
|
|
211
|
+
}, 10000);
|
|
212
|
+
it("should handle large limit values gracefully", async () => {
|
|
213
|
+
const result = await server["handleToolCall"]({
|
|
214
|
+
params: {
|
|
215
|
+
name: "get_past_outages",
|
|
216
|
+
arguments: { limit: 1000 }
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
const responseData = JSON.parse(result.content[0].text);
|
|
220
|
+
expect(responseData).toHaveProperty("total_past_outages");
|
|
221
|
+
// Should not crash or timeout
|
|
222
|
+
expect(responseData.outages.length).toBeLessThanOrEqual(1000);
|
|
223
|
+
}, 15000);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
|
|
2
|
+
import { SystemStatusServer } from "../server.js";
|
|
3
|
+
// Mock axios
|
|
4
|
+
vi.mock("axios");
|
|
5
|
+
describe("SystemStatusServer", () => {
|
|
6
|
+
let server;
|
|
7
|
+
let mockHttpClient;
|
|
8
|
+
const mockCurrentOutagesData = [
|
|
9
|
+
{
|
|
10
|
+
id: "1",
|
|
11
|
+
Subject: "Emergency maintenance on Anvil",
|
|
12
|
+
Content: "Critical issue requiring immediate attention",
|
|
13
|
+
CreationTime: "2024-08-27T10:00:00Z",
|
|
14
|
+
LastModificationTime: "2024-08-27T11:00:00Z",
|
|
15
|
+
AffectedResources: [
|
|
16
|
+
{ ResourceName: "Anvil", ResourceID: "anvil-1" }
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "2",
|
|
21
|
+
Subject: "Scheduled maintenance on Bridges-2",
|
|
22
|
+
Content: "Regular maintenance window",
|
|
23
|
+
CreationTime: "2024-08-27T08:00:00Z",
|
|
24
|
+
LastModificationTime: "2024-08-27T08:30:00Z",
|
|
25
|
+
AffectedResources: [
|
|
26
|
+
{ ResourceName: "Bridges-2", ResourceID: "bridges2-1" }
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
];
|
|
30
|
+
const mockFutureOutagesData = [
|
|
31
|
+
{
|
|
32
|
+
id: "3",
|
|
33
|
+
Subject: "Scheduled Jetstream maintenance",
|
|
34
|
+
Content: "Planned maintenance",
|
|
35
|
+
CreationTime: "2024-08-27T09:00:00Z",
|
|
36
|
+
LastModificationTime: "2024-08-27T09:00:00Z",
|
|
37
|
+
OutageStartDateTime: "2024-08-30T10:00:00Z",
|
|
38
|
+
OutageEndDateTime: "2024-08-30T14:00:00Z",
|
|
39
|
+
AffectedResources: [
|
|
40
|
+
{ ResourceName: "Jetstream", ResourceID: "jetstream-1" }
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
];
|
|
44
|
+
const mockPastOutagesData = [
|
|
45
|
+
{
|
|
46
|
+
id: "4",
|
|
47
|
+
Subject: "Past maintenance on Stampede3",
|
|
48
|
+
Content: "Completed maintenance",
|
|
49
|
+
CreationTime: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), // 5 days ago
|
|
50
|
+
LastModificationTime: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), // 2 days ago
|
|
51
|
+
OutageStartDateTime: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), // 3 days ago
|
|
52
|
+
OutageEndDateTime: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000 + 6 * 60 * 60 * 1000).toISOString(), // 3 days ago + 6 hours
|
|
53
|
+
OutageType: "Full",
|
|
54
|
+
AffectedResources: [
|
|
55
|
+
{ ResourceName: "Stampede3", ResourceID: "stampede3-1" }
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
server = new SystemStatusServer();
|
|
61
|
+
// Set up mock HTTP client
|
|
62
|
+
mockHttpClient = {
|
|
63
|
+
get: vi.fn(),
|
|
64
|
+
};
|
|
65
|
+
// Override the httpClient getter
|
|
66
|
+
Object.defineProperty(server, "httpClient", {
|
|
67
|
+
get: () => mockHttpClient,
|
|
68
|
+
configurable: true,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
vi.clearAllMocks();
|
|
73
|
+
});
|
|
74
|
+
describe("Server Initialization", () => {
|
|
75
|
+
it("should initialize with correct server info", () => {
|
|
76
|
+
expect(server).toBeDefined();
|
|
77
|
+
expect(server["serverName"]).toBe("access-mcp-system-status");
|
|
78
|
+
expect(server["version"]).toBe("0.4.0");
|
|
79
|
+
expect(server["baseURL"]).toBe("https://operations-api.access-ci.org");
|
|
80
|
+
});
|
|
81
|
+
it("should provide correct tools", () => {
|
|
82
|
+
const tools = server["getTools"]();
|
|
83
|
+
expect(tools).toHaveLength(5);
|
|
84
|
+
expect(tools.map(t => t.name)).toEqual([
|
|
85
|
+
"get_current_outages",
|
|
86
|
+
"get_scheduled_maintenance",
|
|
87
|
+
"get_past_outages",
|
|
88
|
+
"get_system_announcements",
|
|
89
|
+
"check_resource_status"
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
it("should provide correct resources", () => {
|
|
93
|
+
const resources = server["getResources"]();
|
|
94
|
+
expect(resources).toHaveLength(4);
|
|
95
|
+
expect(resources.map(r => r.uri)).toEqual([
|
|
96
|
+
"accessci://system-status",
|
|
97
|
+
"accessci://outages/current",
|
|
98
|
+
"accessci://outages/scheduled",
|
|
99
|
+
"accessci://outages/past"
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe("getCurrentOutages", () => {
|
|
104
|
+
it("should fetch and enhance current outages", async () => {
|
|
105
|
+
mockHttpClient.get.mockResolvedValue({
|
|
106
|
+
status: 200,
|
|
107
|
+
data: { results: mockCurrentOutagesData }
|
|
108
|
+
});
|
|
109
|
+
const result = await server["handleToolCall"]({
|
|
110
|
+
params: { name: "get_current_outages", arguments: {} }
|
|
111
|
+
});
|
|
112
|
+
expect(mockHttpClient.get).toHaveBeenCalledWith("/wh2/news/v1/affiliation/access-ci.org/current_outages/");
|
|
113
|
+
const response = JSON.parse(result.content[0].text);
|
|
114
|
+
expect(response.total_outages).toBe(2);
|
|
115
|
+
expect(response.affected_resources).toEqual(["Anvil", "Bridges-2"]);
|
|
116
|
+
expect(response.severity_counts).toHaveProperty("high", 1); // Emergency
|
|
117
|
+
expect(response.severity_counts).toHaveProperty("low", 1); // Scheduled maintenance
|
|
118
|
+
expect(response.outages[0]).toHaveProperty("severity");
|
|
119
|
+
expect(response.outages[0]).toHaveProperty("posted_time");
|
|
120
|
+
});
|
|
121
|
+
it("should filter outages by resource", async () => {
|
|
122
|
+
mockHttpClient.get.mockResolvedValue({
|
|
123
|
+
status: 200,
|
|
124
|
+
data: { results: mockCurrentOutagesData }
|
|
125
|
+
});
|
|
126
|
+
const result = await server["handleToolCall"]({
|
|
127
|
+
params: {
|
|
128
|
+
name: "get_current_outages",
|
|
129
|
+
arguments: { resource_filter: "Anvil" }
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
const response = JSON.parse(result.content[0].text);
|
|
133
|
+
expect(response.total_outages).toBe(1);
|
|
134
|
+
expect(response.outages[0].Subject).toContain("Anvil");
|
|
135
|
+
});
|
|
136
|
+
it("should categorize severity correctly", async () => {
|
|
137
|
+
mockHttpClient.get.mockResolvedValue({
|
|
138
|
+
status: 200,
|
|
139
|
+
data: { results: mockCurrentOutagesData }
|
|
140
|
+
});
|
|
141
|
+
const result = await server["handleToolCall"]({
|
|
142
|
+
params: { name: "get_current_outages", arguments: {} }
|
|
143
|
+
});
|
|
144
|
+
const response = JSON.parse(result.content[0].text);
|
|
145
|
+
const emergencyOutage = response.outages.find((o) => o.Subject.includes("Emergency"));
|
|
146
|
+
const maintenanceOutage = response.outages.find((o) => o.Subject.includes("Scheduled"));
|
|
147
|
+
expect(emergencyOutage.severity).toBe("high");
|
|
148
|
+
expect(maintenanceOutage.severity).toBe("low");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe("getScheduledMaintenance", () => {
|
|
152
|
+
it("should fetch and enhance scheduled maintenance", async () => {
|
|
153
|
+
mockHttpClient.get.mockResolvedValue({
|
|
154
|
+
status: 200,
|
|
155
|
+
data: { results: mockFutureOutagesData }
|
|
156
|
+
});
|
|
157
|
+
const result = await server["handleToolCall"]({
|
|
158
|
+
params: { name: "get_scheduled_maintenance", arguments: {} }
|
|
159
|
+
});
|
|
160
|
+
const response = JSON.parse(result.content[0].text);
|
|
161
|
+
expect(response.total_scheduled).toBe(1);
|
|
162
|
+
expect(response.affected_resources).toEqual(["Jetstream"]);
|
|
163
|
+
expect(response.maintenance[0]).toHaveProperty("hours_until_start");
|
|
164
|
+
expect(response.maintenance[0]).toHaveProperty("duration_hours", 4); // 10am to 2pm = 4 hours
|
|
165
|
+
expect(response.maintenance[0]).toHaveProperty("has_scheduled_time", true);
|
|
166
|
+
});
|
|
167
|
+
it("should handle missing scheduled times", async () => {
|
|
168
|
+
const dataWithoutSchedule = [{
|
|
169
|
+
...mockFutureOutagesData[0],
|
|
170
|
+
OutageStartDateTime: null,
|
|
171
|
+
OutageEndDateTime: null
|
|
172
|
+
}];
|
|
173
|
+
mockHttpClient.get.mockResolvedValue({
|
|
174
|
+
status: 200,
|
|
175
|
+
data: { results: dataWithoutSchedule }
|
|
176
|
+
});
|
|
177
|
+
const result = await server["handleToolCall"]({
|
|
178
|
+
params: { name: "get_scheduled_maintenance", arguments: {} }
|
|
179
|
+
});
|
|
180
|
+
const response = JSON.parse(result.content[0].text);
|
|
181
|
+
expect(response.maintenance[0].has_scheduled_time).toBe(false);
|
|
182
|
+
expect(response.maintenance[0].duration_hours).toBe(null);
|
|
183
|
+
});
|
|
184
|
+
it("should sort by scheduled start time", async () => {
|
|
185
|
+
const multipleMaintenanceData = [
|
|
186
|
+
{
|
|
187
|
+
...mockFutureOutagesData[0],
|
|
188
|
+
OutageStartDateTime: "2024-08-31T10:00:00Z", // Later
|
|
189
|
+
Subject: "Later maintenance"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
...mockFutureOutagesData[0],
|
|
193
|
+
OutageStartDateTime: "2024-08-30T10:00:00Z", // Earlier
|
|
194
|
+
Subject: "Earlier maintenance"
|
|
195
|
+
}
|
|
196
|
+
];
|
|
197
|
+
mockHttpClient.get.mockResolvedValue({
|
|
198
|
+
status: 200,
|
|
199
|
+
data: { results: multipleMaintenanceData }
|
|
200
|
+
});
|
|
201
|
+
const result = await server["handleToolCall"]({
|
|
202
|
+
params: { name: "get_scheduled_maintenance", arguments: {} }
|
|
203
|
+
});
|
|
204
|
+
const response = JSON.parse(result.content[0].text);
|
|
205
|
+
expect(response.maintenance[0].Subject).toBe("Earlier maintenance");
|
|
206
|
+
expect(response.maintenance[1].Subject).toBe("Later maintenance");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
describe("getPastOutages", () => {
|
|
210
|
+
it("should fetch and enhance past outages", async () => {
|
|
211
|
+
mockHttpClient.get.mockResolvedValue({
|
|
212
|
+
status: 200,
|
|
213
|
+
data: { results: mockPastOutagesData }
|
|
214
|
+
});
|
|
215
|
+
const result = await server["handleToolCall"]({
|
|
216
|
+
params: { name: "get_past_outages", arguments: {} }
|
|
217
|
+
});
|
|
218
|
+
const response = JSON.parse(result.content[0].text);
|
|
219
|
+
expect(response.total_past_outages).toBe(1);
|
|
220
|
+
expect(response.outage_types).toEqual(["Full"]);
|
|
221
|
+
expect(response.average_duration_hours).toBe(6); // 6 hour duration
|
|
222
|
+
expect(response.outages[0]).toHaveProperty("duration_hours", 6);
|
|
223
|
+
expect(response.outages[0]).toHaveProperty("days_ago");
|
|
224
|
+
});
|
|
225
|
+
it("should apply limit correctly", async () => {
|
|
226
|
+
const manyOutages = Array(50).fill(0).map((_, i) => ({
|
|
227
|
+
...mockPastOutagesData[0],
|
|
228
|
+
id: `past-${i}`,
|
|
229
|
+
Subject: `Past outage ${i}`
|
|
230
|
+
}));
|
|
231
|
+
mockHttpClient.get.mockResolvedValue({
|
|
232
|
+
status: 200,
|
|
233
|
+
data: { results: manyOutages }
|
|
234
|
+
});
|
|
235
|
+
const result = await server["handleToolCall"]({
|
|
236
|
+
params: {
|
|
237
|
+
name: "get_past_outages",
|
|
238
|
+
arguments: { limit: 10 }
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
const response = JSON.parse(result.content[0].text);
|
|
242
|
+
expect(response.outages).toHaveLength(10);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
describe("getSystemAnnouncements", () => {
|
|
246
|
+
it("should combine current, future, and recent past outages", async () => {
|
|
247
|
+
mockHttpClient.get
|
|
248
|
+
.mockResolvedValueOnce({ status: 200, data: { results: mockCurrentOutagesData } })
|
|
249
|
+
.mockResolvedValueOnce({ status: 200, data: { results: mockFutureOutagesData } })
|
|
250
|
+
.mockResolvedValueOnce({ status: 200, data: { results: mockPastOutagesData } });
|
|
251
|
+
const result = await server["handleToolCall"]({
|
|
252
|
+
params: { name: "get_system_announcements", arguments: {} }
|
|
253
|
+
});
|
|
254
|
+
expect(mockHttpClient.get).toHaveBeenCalledTimes(3);
|
|
255
|
+
const response = JSON.parse(result.content[0].text);
|
|
256
|
+
expect(response.current_outages).toBe(2);
|
|
257
|
+
expect(response.scheduled_maintenance).toBe(1);
|
|
258
|
+
expect(response.recent_past_outages).toBe(1); // Within 30 days
|
|
259
|
+
expect(response.categories).toHaveProperty("current");
|
|
260
|
+
expect(response.categories).toHaveProperty("scheduled");
|
|
261
|
+
expect(response.categories).toHaveProperty("recent_past");
|
|
262
|
+
});
|
|
263
|
+
it("should prioritize current outages in sorting", async () => {
|
|
264
|
+
mockHttpClient.get
|
|
265
|
+
.mockResolvedValueOnce({ status: 200, data: { results: mockCurrentOutagesData } })
|
|
266
|
+
.mockResolvedValueOnce({ status: 200, data: { results: mockFutureOutagesData } })
|
|
267
|
+
.mockResolvedValueOnce({ status: 200, data: { results: mockPastOutagesData } });
|
|
268
|
+
const result = await server["handleToolCall"]({
|
|
269
|
+
params: { name: "get_system_announcements", arguments: {} }
|
|
270
|
+
});
|
|
271
|
+
const response = JSON.parse(result.content[0].text);
|
|
272
|
+
const firstAnnouncement = response.announcements[0];
|
|
273
|
+
expect(firstAnnouncement.category).toBe("current");
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
describe("checkResourceStatus", () => {
|
|
277
|
+
it("should check resource status efficiently (direct method)", async () => {
|
|
278
|
+
mockHttpClient.get.mockResolvedValue({
|
|
279
|
+
status: 200,
|
|
280
|
+
data: { results: mockCurrentOutagesData }
|
|
281
|
+
});
|
|
282
|
+
const result = await server["handleToolCall"]({
|
|
283
|
+
params: {
|
|
284
|
+
name: "check_resource_status",
|
|
285
|
+
arguments: { resource_ids: ["anvil-1", "unknown-resource"] }
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
const response = JSON.parse(result.content[0].text);
|
|
289
|
+
expect(response.api_method).toBe("direct_outages_check");
|
|
290
|
+
expect(response.resources_checked).toBe(2);
|
|
291
|
+
expect(response.operational).toBe(1); // unknown-resource
|
|
292
|
+
expect(response.affected).toBe(1); // anvil-1
|
|
293
|
+
const anvilStatus = response.resource_status.find((r) => r.resource_id === "anvil-1");
|
|
294
|
+
expect(anvilStatus.status).toBe("affected");
|
|
295
|
+
expect(anvilStatus.severity).toBe("high"); // Emergency maintenance
|
|
296
|
+
});
|
|
297
|
+
it("should use group API when requested", async () => {
|
|
298
|
+
mockHttpClient.get.mockResolvedValue({
|
|
299
|
+
status: 200,
|
|
300
|
+
data: { results: [] } // No outages for this group
|
|
301
|
+
});
|
|
302
|
+
const result = await server["handleToolCall"]({
|
|
303
|
+
params: {
|
|
304
|
+
name: "check_resource_status",
|
|
305
|
+
arguments: {
|
|
306
|
+
resource_ids: ["anvil"],
|
|
307
|
+
use_group_api: true
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
expect(mockHttpClient.get).toHaveBeenCalledWith("/wh2/news/v1/info_groupid/anvil/");
|
|
312
|
+
const response = JSON.parse(result.content[0].text);
|
|
313
|
+
expect(response.api_method).toBe("resource_group_api");
|
|
314
|
+
expect(response.resource_status[0].status).toBe("operational");
|
|
315
|
+
expect(response.resource_status[0].api_method).toBe("group_specific");
|
|
316
|
+
});
|
|
317
|
+
it("should handle group API failures gracefully", async () => {
|
|
318
|
+
mockHttpClient.get.mockRejectedValue(new Error("API Error"));
|
|
319
|
+
const result = await server["handleToolCall"]({
|
|
320
|
+
params: {
|
|
321
|
+
name: "check_resource_status",
|
|
322
|
+
arguments: {
|
|
323
|
+
resource_ids: ["invalid-resource"],
|
|
324
|
+
use_group_api: true
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
const response = JSON.parse(result.content[0].text);
|
|
329
|
+
expect(response.unknown).toBe(1);
|
|
330
|
+
expect(response.resource_status[0].status).toBe("unknown");
|
|
331
|
+
expect(response.resource_status[0].api_method).toBe("group_specific_failed");
|
|
332
|
+
expect(response.resource_status[0]).toHaveProperty("error");
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
describe("Error Handling", () => {
|
|
336
|
+
it("should handle API errors gracefully", async () => {
|
|
337
|
+
mockHttpClient.get.mockResolvedValue({
|
|
338
|
+
status: 500,
|
|
339
|
+
statusText: "Internal Server Error"
|
|
340
|
+
});
|
|
341
|
+
const result = await server["handleToolCall"]({
|
|
342
|
+
params: { name: "get_current_outages", arguments: {} }
|
|
343
|
+
});
|
|
344
|
+
expect(result.content[0].text).toContain("Error");
|
|
345
|
+
});
|
|
346
|
+
it("should handle network errors", async () => {
|
|
347
|
+
mockHttpClient.get.mockRejectedValue(new Error("Network error"));
|
|
348
|
+
const result = await server["handleToolCall"]({
|
|
349
|
+
params: { name: "get_current_outages", arguments: {} }
|
|
350
|
+
});
|
|
351
|
+
expect(result.content[0].text).toContain("Error");
|
|
352
|
+
});
|
|
353
|
+
it("should handle unknown tools", async () => {
|
|
354
|
+
const result = await server["handleToolCall"]({
|
|
355
|
+
params: { name: "unknown_tool", arguments: {} }
|
|
356
|
+
});
|
|
357
|
+
expect(result.content[0].text).toContain("Unknown tool");
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
describe("Resource Handling", () => {
|
|
361
|
+
it("should handle resource reads correctly", async () => {
|
|
362
|
+
mockHttpClient.get.mockResolvedValue({
|
|
363
|
+
status: 200,
|
|
364
|
+
data: { results: mockCurrentOutagesData }
|
|
365
|
+
});
|
|
366
|
+
const result = await server["handleResourceRead"]({
|
|
367
|
+
params: { uri: "accessci://outages/current" }
|
|
368
|
+
});
|
|
369
|
+
expect(result.contents[0].mimeType).toBe("application/json");
|
|
370
|
+
expect(result.contents[0].text).toBeDefined();
|
|
371
|
+
});
|
|
372
|
+
it("should handle unknown resources", async () => {
|
|
373
|
+
await expect(async () => {
|
|
374
|
+
await server["handleResourceRead"]({
|
|
375
|
+
params: { uri: "accessci://unknown" }
|
|
376
|
+
});
|
|
377
|
+
}).rejects.toThrow("Unknown resource");
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { SystemStatusServer } from "./server.js";
|
|
3
|
-
import { startWebServer } from "./web-server.js";
|
|
4
3
|
async function main() {
|
|
5
|
-
// Check if we should run as
|
|
4
|
+
// Check if we should run as HTTP server (for deployment)
|
|
6
5
|
const port = process.env.PORT;
|
|
6
|
+
const server = new SystemStatusServer();
|
|
7
7
|
if (port) {
|
|
8
|
-
// Running in
|
|
9
|
-
|
|
8
|
+
// Running in HTTP mode (deployment)
|
|
9
|
+
await server.start({ httpPort: parseInt(port) });
|
|
10
10
|
}
|
|
11
11
|
else {
|
|
12
12
|
// Running in MCP mode (stdio)
|
|
13
|
-
const server = new SystemStatusServer();
|
|
14
13
|
await server.start();
|
|
15
14
|
}
|
|
16
15
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -13,6 +13,26 @@ export declare class SystemStatusServer extends BaseAccessServer {
|
|
|
13
13
|
};
|
|
14
14
|
limit?: undefined;
|
|
15
15
|
resource_ids?: undefined;
|
|
16
|
+
use_group_api?: undefined;
|
|
17
|
+
};
|
|
18
|
+
required: never[];
|
|
19
|
+
};
|
|
20
|
+
} | {
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: string;
|
|
25
|
+
properties: {
|
|
26
|
+
resource_filter: {
|
|
27
|
+
type: string;
|
|
28
|
+
description: string;
|
|
29
|
+
};
|
|
30
|
+
limit: {
|
|
31
|
+
type: string;
|
|
32
|
+
description: string;
|
|
33
|
+
};
|
|
34
|
+
resource_ids?: undefined;
|
|
35
|
+
use_group_api?: undefined;
|
|
16
36
|
};
|
|
17
37
|
required: never[];
|
|
18
38
|
};
|
|
@@ -28,6 +48,7 @@ export declare class SystemStatusServer extends BaseAccessServer {
|
|
|
28
48
|
};
|
|
29
49
|
resource_filter?: undefined;
|
|
30
50
|
resource_ids?: undefined;
|
|
51
|
+
use_group_api?: undefined;
|
|
31
52
|
};
|
|
32
53
|
required: never[];
|
|
33
54
|
};
|
|
@@ -44,6 +65,11 @@ export declare class SystemStatusServer extends BaseAccessServer {
|
|
|
44
65
|
};
|
|
45
66
|
description: string;
|
|
46
67
|
};
|
|
68
|
+
use_group_api: {
|
|
69
|
+
type: string;
|
|
70
|
+
description: string;
|
|
71
|
+
default: boolean;
|
|
72
|
+
};
|
|
47
73
|
resource_filter?: undefined;
|
|
48
74
|
limit?: undefined;
|
|
49
75
|
};
|
|
@@ -71,6 +97,8 @@ export declare class SystemStatusServer extends BaseAccessServer {
|
|
|
71
97
|
}>;
|
|
72
98
|
private getCurrentOutages;
|
|
73
99
|
private getScheduledMaintenance;
|
|
100
|
+
private getPastOutages;
|
|
74
101
|
private getSystemAnnouncements;
|
|
75
102
|
private checkResourceStatus;
|
|
103
|
+
private checkResourceStatusViaGroups;
|
|
76
104
|
}
|
package/dist/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BaseAccessServer, handleApiError } from "@access-mcp/shared";
|
|
2
2
|
export class SystemStatusServer extends BaseAccessServer {
|
|
3
3
|
constructor() {
|
|
4
|
-
super("access-mcp-system-status", "0.
|
|
4
|
+
super("access-mcp-system-status", "0.4.0", "https://operations-api.access-ci.org");
|
|
5
5
|
}
|
|
6
6
|
getTools() {
|
|
7
7
|
return [
|
|
@@ -33,6 +33,24 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
33
33
|
required: [],
|
|
34
34
|
},
|
|
35
35
|
},
|
|
36
|
+
{
|
|
37
|
+
name: "get_past_outages",
|
|
38
|
+
description: "Get historical outages and past incidents affecting ACCESS-CI resources",
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
resource_filter: {
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "Optional: filter by specific resource name or ID",
|
|
45
|
+
},
|
|
46
|
+
limit: {
|
|
47
|
+
type: "number",
|
|
48
|
+
description: "Maximum number of past outages to return (default: 100)",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
required: [],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
36
54
|
{
|
|
37
55
|
name: "get_system_announcements",
|
|
38
56
|
description: "Get all system announcements (current and scheduled)",
|
|
@@ -56,7 +74,12 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
56
74
|
resource_ids: {
|
|
57
75
|
type: "array",
|
|
58
76
|
items: { type: "string" },
|
|
59
|
-
description: "List of resource IDs to check status for",
|
|
77
|
+
description: "List of resource IDs or names to check status for",
|
|
78
|
+
},
|
|
79
|
+
use_group_api: {
|
|
80
|
+
type: "boolean",
|
|
81
|
+
description: "Use resource group API for more efficient querying (default: false)",
|
|
82
|
+
default: false,
|
|
60
83
|
},
|
|
61
84
|
},
|
|
62
85
|
required: ["resource_ids"],
|
|
@@ -84,6 +107,12 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
84
107
|
description: "Upcoming scheduled maintenance and planned outages",
|
|
85
108
|
mimeType: "application/json",
|
|
86
109
|
},
|
|
110
|
+
{
|
|
111
|
+
uri: "accessci://outages/past",
|
|
112
|
+
name: "Past Outages",
|
|
113
|
+
description: "Historical outages and past incidents",
|
|
114
|
+
mimeType: "application/json",
|
|
115
|
+
},
|
|
87
116
|
];
|
|
88
117
|
}
|
|
89
118
|
async handleToolCall(request) {
|
|
@@ -94,10 +123,12 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
94
123
|
return await this.getCurrentOutages(args.resource_filter);
|
|
95
124
|
case "get_scheduled_maintenance":
|
|
96
125
|
return await this.getScheduledMaintenance(args.resource_filter);
|
|
126
|
+
case "get_past_outages":
|
|
127
|
+
return await this.getPastOutages(args.resource_filter, args.limit);
|
|
97
128
|
case "get_system_announcements":
|
|
98
129
|
return await this.getSystemAnnouncements(args.limit);
|
|
99
130
|
case "check_resource_status":
|
|
100
|
-
return await this.checkResourceStatus(args.resource_ids);
|
|
131
|
+
return await this.checkResourceStatus(args.resource_ids, args.use_group_api);
|
|
101
132
|
default:
|
|
102
133
|
throw new Error(`Unknown tool: ${name}`);
|
|
103
134
|
}
|
|
@@ -148,6 +179,17 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
148
179
|
},
|
|
149
180
|
],
|
|
150
181
|
};
|
|
182
|
+
case "accessci://outages/past":
|
|
183
|
+
const pastOutages = await this.getPastOutages();
|
|
184
|
+
return {
|
|
185
|
+
contents: [
|
|
186
|
+
{
|
|
187
|
+
uri,
|
|
188
|
+
mimeType: "application/json",
|
|
189
|
+
text: pastOutages.content[0].text,
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
};
|
|
151
193
|
default:
|
|
152
194
|
throw new Error(`Unknown resource: ${uri}`);
|
|
153
195
|
}
|
|
@@ -188,8 +230,8 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
188
230
|
return {
|
|
189
231
|
...outage,
|
|
190
232
|
severity,
|
|
191
|
-
posted_time: outage.CreationTime,
|
|
192
|
-
last_updated: outage.LastModificationTime,
|
|
233
|
+
posted_time: outage.CreationTime || outage.DateTime || outage.created_at || outage.date_posted,
|
|
234
|
+
last_updated: outage.LastModificationTime || outage.DateTime || outage.updated_at || outage.date_modified,
|
|
193
235
|
};
|
|
194
236
|
});
|
|
195
237
|
const summary = {
|
|
@@ -202,10 +244,7 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
202
244
|
content: [
|
|
203
245
|
{
|
|
204
246
|
type: "text",
|
|
205
|
-
text: JSON.stringify(
|
|
206
|
-
...summary,
|
|
207
|
-
affected_resources: summary.affected_resources,
|
|
208
|
-
}, null, 2),
|
|
247
|
+
text: JSON.stringify(summary, null, 2),
|
|
209
248
|
},
|
|
210
249
|
],
|
|
211
250
|
};
|
|
@@ -226,92 +265,211 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
226
265
|
const dateB = new Date(b.OutageStartDateTime || b.CreationTime);
|
|
227
266
|
return dateA.getTime() - dateB.getTime();
|
|
228
267
|
});
|
|
268
|
+
// Initialize tracking variables
|
|
269
|
+
const affectedResources = new Set();
|
|
270
|
+
let upcoming24h = 0;
|
|
271
|
+
let upcomingWeek = 0;
|
|
272
|
+
const enhancedMaintenance = maintenance.map((item) => {
|
|
273
|
+
// Track affected resources
|
|
274
|
+
item.AffectedResources?.forEach((resource) => {
|
|
275
|
+
affectedResources.add(resource.ResourceName);
|
|
276
|
+
});
|
|
277
|
+
// Check timing - only use OutageStartDateTime for scheduling, fallback shows warning
|
|
278
|
+
const hasScheduledTime = !!item.OutageStartDateTime;
|
|
279
|
+
const startTime = new Date(item.OutageStartDateTime || item.CreationTime);
|
|
280
|
+
const now = new Date();
|
|
281
|
+
const hoursUntil = (startTime.getTime() - now.getTime()) / (1000 * 60 * 60);
|
|
282
|
+
if (hoursUntil <= 24)
|
|
283
|
+
upcoming24h++;
|
|
284
|
+
if (hoursUntil <= 168)
|
|
285
|
+
upcomingWeek++; // 7 days * 24 hours
|
|
286
|
+
return {
|
|
287
|
+
...item,
|
|
288
|
+
scheduled_start: item.OutageStartDateTime,
|
|
289
|
+
scheduled_end: item.OutageEndDateTime,
|
|
290
|
+
hours_until_start: Math.max(0, Math.round(hoursUntil)),
|
|
291
|
+
duration_hours: item.OutageEndDateTime && item.OutageStartDateTime
|
|
292
|
+
? Math.round((new Date(item.OutageEndDateTime).getTime() -
|
|
293
|
+
new Date(item.OutageStartDateTime).getTime()) /
|
|
294
|
+
(1000 * 60 * 60))
|
|
295
|
+
: null,
|
|
296
|
+
has_scheduled_time: hasScheduledTime,
|
|
297
|
+
};
|
|
298
|
+
});
|
|
229
299
|
const summary = {
|
|
230
300
|
total_scheduled: maintenance.length,
|
|
231
|
-
upcoming_24h:
|
|
232
|
-
upcoming_week:
|
|
233
|
-
affected_resources:
|
|
234
|
-
maintenance:
|
|
235
|
-
// Track affected resources
|
|
236
|
-
item.AffectedResources?.forEach((resource) => {
|
|
237
|
-
summary.affected_resources.add(resource.ResourceName);
|
|
238
|
-
});
|
|
239
|
-
// Check timing
|
|
240
|
-
const startTime = new Date(item.OutageStartDateTime || item.CreationTime);
|
|
241
|
-
const now = new Date();
|
|
242
|
-
const hoursUntil = (startTime.getTime() - now.getTime()) / (1000 * 60 * 60);
|
|
243
|
-
if (hoursUntil <= 24)
|
|
244
|
-
summary.upcoming_24h++;
|
|
245
|
-
if (hoursUntil <= 168)
|
|
246
|
-
summary.upcoming_week++; // 7 days * 24 hours
|
|
247
|
-
return {
|
|
248
|
-
...item,
|
|
249
|
-
scheduled_start: item.OutageStartDateTime,
|
|
250
|
-
scheduled_end: item.OutageEndDateTime,
|
|
251
|
-
hours_until_start: Math.max(0, Math.round(hoursUntil)),
|
|
252
|
-
duration_hours: item.OutageEndDateTime && item.OutageStartDateTime
|
|
253
|
-
? Math.round((new Date(item.OutageEndDateTime).getTime() -
|
|
254
|
-
new Date(item.OutageStartDateTime).getTime()) /
|
|
255
|
-
(1000 * 60 * 60))
|
|
256
|
-
: null,
|
|
257
|
-
};
|
|
258
|
-
}),
|
|
301
|
+
upcoming_24h: upcoming24h,
|
|
302
|
+
upcoming_week: upcomingWeek,
|
|
303
|
+
affected_resources: Array.from(affectedResources),
|
|
304
|
+
maintenance: enhancedMaintenance,
|
|
259
305
|
};
|
|
260
306
|
return {
|
|
261
307
|
content: [
|
|
262
308
|
{
|
|
263
309
|
type: "text",
|
|
264
|
-
text: JSON.stringify(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
310
|
+
text: JSON.stringify(summary, null, 2),
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
async getPastOutages(resourceFilter, limit = 100) {
|
|
316
|
+
const response = await this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/past_outages/");
|
|
317
|
+
let pastOutages = response.data.results || [];
|
|
318
|
+
// Filter by resource if specified
|
|
319
|
+
if (resourceFilter) {
|
|
320
|
+
const filter = resourceFilter.toLowerCase();
|
|
321
|
+
pastOutages = pastOutages.filter((outage) => outage.Subject?.toLowerCase().includes(filter) ||
|
|
322
|
+
outage.AffectedResources?.some((resource) => resource.ResourceName?.toLowerCase().includes(filter) ||
|
|
323
|
+
resource.ResourceID?.toString().includes(filter)));
|
|
324
|
+
}
|
|
325
|
+
// Sort by outage end time (most recent first)
|
|
326
|
+
pastOutages.sort((a, b) => {
|
|
327
|
+
const dateA = new Date(a.OutageEndDateTime || a.LastModificationTime);
|
|
328
|
+
const dateB = new Date(b.OutageEndDateTime || b.LastModificationTime);
|
|
329
|
+
return dateB.getTime() - dateA.getTime();
|
|
330
|
+
});
|
|
331
|
+
// Apply limit
|
|
332
|
+
if (limit && pastOutages.length > limit) {
|
|
333
|
+
pastOutages = pastOutages.slice(0, limit);
|
|
334
|
+
}
|
|
335
|
+
// Initialize tracking variables
|
|
336
|
+
const affectedResources = new Set();
|
|
337
|
+
const outageTypes = new Set();
|
|
338
|
+
const recentOutages = pastOutages.filter((outage) => {
|
|
339
|
+
const endTime = new Date(outage.OutageEndDateTime || outage.LastModificationTime);
|
|
340
|
+
const daysAgo = (Date.now() - endTime.getTime()) / (1000 * 60 * 60 * 24);
|
|
341
|
+
return daysAgo <= 30; // Last 30 days
|
|
342
|
+
});
|
|
343
|
+
// Enhance outages with calculated fields
|
|
344
|
+
const enhancedOutages = pastOutages.map((outage) => {
|
|
345
|
+
// Track affected resources
|
|
346
|
+
outage.AffectedResources?.forEach((resource) => {
|
|
347
|
+
affectedResources.add(resource.ResourceName);
|
|
348
|
+
});
|
|
349
|
+
// Track outage types
|
|
350
|
+
if (outage.OutageType) {
|
|
351
|
+
outageTypes.add(outage.OutageType);
|
|
352
|
+
}
|
|
353
|
+
// Calculate duration
|
|
354
|
+
const startTime = new Date(outage.OutageStartDateTime);
|
|
355
|
+
const endTime = new Date(outage.OutageEndDateTime);
|
|
356
|
+
const durationHours = Math.round((endTime.getTime() - startTime.getTime()) / (1000 * 60 * 60));
|
|
357
|
+
// Calculate how long ago it ended
|
|
358
|
+
const daysAgo = Math.round((Date.now() - endTime.getTime()) / (1000 * 60 * 60 * 24));
|
|
359
|
+
return {
|
|
360
|
+
...outage,
|
|
361
|
+
outage_start: outage.OutageStartDateTime,
|
|
362
|
+
outage_end: outage.OutageEndDateTime,
|
|
363
|
+
duration_hours: durationHours,
|
|
364
|
+
days_ago: daysAgo,
|
|
365
|
+
outage_type: outage.OutageType,
|
|
366
|
+
posted_time: outage.CreationTime,
|
|
367
|
+
last_updated: outage.LastModificationTime,
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
const summary = {
|
|
371
|
+
total_past_outages: enhancedOutages.length,
|
|
372
|
+
recent_outages_30_days: recentOutages.length,
|
|
373
|
+
affected_resources: Array.from(affectedResources),
|
|
374
|
+
outage_types: Array.from(outageTypes),
|
|
375
|
+
average_duration_hours: enhancedOutages.length > 0
|
|
376
|
+
? Math.round(enhancedOutages
|
|
377
|
+
.filter((o) => o.duration_hours > 0)
|
|
378
|
+
.reduce((sum, o) => sum + o.duration_hours, 0) /
|
|
379
|
+
enhancedOutages.filter((o) => o.duration_hours > 0).length)
|
|
380
|
+
: 0,
|
|
381
|
+
outages: enhancedOutages,
|
|
382
|
+
};
|
|
383
|
+
return {
|
|
384
|
+
content: [
|
|
385
|
+
{
|
|
386
|
+
type: "text",
|
|
387
|
+
text: JSON.stringify(summary, null, 2),
|
|
268
388
|
},
|
|
269
389
|
],
|
|
270
390
|
};
|
|
271
391
|
}
|
|
272
392
|
async getSystemAnnouncements(limit = 50) {
|
|
273
|
-
// Get
|
|
274
|
-
const [currentResponse, futureResponse] = await Promise.all([
|
|
393
|
+
// Get current, future, and recent past announcements for comprehensive view
|
|
394
|
+
const [currentResponse, futureResponse, pastResponse] = await Promise.all([
|
|
275
395
|
this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/current_outages/"),
|
|
276
396
|
this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/future_outages/"),
|
|
397
|
+
this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/past_outages/"),
|
|
277
398
|
]);
|
|
278
399
|
const currentOutages = currentResponse.data.results || [];
|
|
279
400
|
const futureOutages = futureResponse.data.results || [];
|
|
280
|
-
|
|
281
|
-
|
|
401
|
+
const pastOutages = pastResponse.data.results || [];
|
|
402
|
+
// Filter recent past outages (last 30 days) for announcements
|
|
403
|
+
const recentPastOutages = pastOutages.filter((outage) => {
|
|
404
|
+
const endTime = new Date(outage.OutageEndDateTime || outage.LastModificationTime);
|
|
405
|
+
const daysAgo = (Date.now() - endTime.getTime()) / (1000 * 60 * 60 * 24);
|
|
406
|
+
return daysAgo <= 30;
|
|
407
|
+
});
|
|
408
|
+
// Combine all announcements and sort by most relevant date
|
|
409
|
+
const allAnnouncements = [
|
|
410
|
+
...currentOutages.map((item) => ({ ...item, category: 'current' })),
|
|
411
|
+
...futureOutages.map((item) => ({ ...item, category: 'scheduled' })),
|
|
412
|
+
...recentPastOutages.map((item) => ({ ...item, category: 'recent_past' })),
|
|
413
|
+
]
|
|
282
414
|
.sort((a, b) => {
|
|
283
|
-
|
|
284
|
-
|
|
415
|
+
// Sort by most relevant date: current first, then future by start time, then past by end time
|
|
416
|
+
if (a.category === 'current' && b.category !== 'current')
|
|
417
|
+
return -1;
|
|
418
|
+
if (b.category === 'current' && a.category !== 'current')
|
|
419
|
+
return 1;
|
|
420
|
+
const dateA = new Date(a.OutageStartDateTime || a.CreationTime);
|
|
421
|
+
const dateB = new Date(b.OutageStartDateTime || b.CreationTime);
|
|
285
422
|
return dateB.getTime() - dateA.getTime(); // Most recent first
|
|
286
423
|
})
|
|
287
424
|
.slice(0, limit);
|
|
425
|
+
const summary = {
|
|
426
|
+
total_announcements: allAnnouncements.length,
|
|
427
|
+
current_outages: currentOutages.length,
|
|
428
|
+
scheduled_maintenance: futureOutages.length,
|
|
429
|
+
recent_past_outages: recentPastOutages.length,
|
|
430
|
+
categories: {
|
|
431
|
+
current: allAnnouncements.filter(a => a.category === 'current').length,
|
|
432
|
+
scheduled: allAnnouncements.filter(a => a.category === 'scheduled').length,
|
|
433
|
+
recent_past: allAnnouncements.filter(a => a.category === 'recent_past').length,
|
|
434
|
+
},
|
|
435
|
+
announcements: allAnnouncements,
|
|
436
|
+
};
|
|
288
437
|
return {
|
|
289
438
|
content: [
|
|
290
439
|
{
|
|
291
440
|
type: "text",
|
|
292
|
-
text: JSON.stringify(
|
|
293
|
-
total_announcements: allAnnouncements.length,
|
|
294
|
-
current_outages: currentOutages.length,
|
|
295
|
-
scheduled_maintenance: futureOutages.length,
|
|
296
|
-
announcements: allAnnouncements,
|
|
297
|
-
}, null, 2),
|
|
441
|
+
text: JSON.stringify(summary, null, 2),
|
|
298
442
|
},
|
|
299
443
|
],
|
|
300
444
|
};
|
|
301
445
|
}
|
|
302
|
-
async checkResourceStatus(resourceIds) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
446
|
+
async checkResourceStatus(resourceIds, useGroupApi = false) {
|
|
447
|
+
if (useGroupApi) {
|
|
448
|
+
return await this.checkResourceStatusViaGroups(resourceIds);
|
|
449
|
+
}
|
|
450
|
+
// Efficient approach: fetch raw current outages data once
|
|
451
|
+
const response = await this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/current_outages/");
|
|
452
|
+
const rawOutages = response.data.results || [];
|
|
306
453
|
const resourceStatus = resourceIds.map((resourceId) => {
|
|
307
|
-
const affectedOutages =
|
|
454
|
+
const affectedOutages = rawOutages.filter((outage) => outage.AffectedResources?.some((resource) => resource.ResourceID?.toString() === resourceId ||
|
|
308
455
|
resource.ResourceName?.toLowerCase().includes(resourceId.toLowerCase())));
|
|
309
456
|
let status = "operational";
|
|
310
457
|
let severity = null;
|
|
311
458
|
if (affectedOutages.length > 0) {
|
|
312
459
|
status = "affected";
|
|
313
|
-
// Get highest severity
|
|
314
|
-
const severities = affectedOutages.map((
|
|
460
|
+
// Get highest severity using same logic as getCurrentOutages
|
|
461
|
+
const severities = affectedOutages.map((outage) => {
|
|
462
|
+
const subject = outage.Subject?.toLowerCase() || "";
|
|
463
|
+
if (subject.includes("emergency") || subject.includes("critical")) {
|
|
464
|
+
return "high";
|
|
465
|
+
}
|
|
466
|
+
else if (subject.includes("maintenance") || subject.includes("scheduled")) {
|
|
467
|
+
return "low";
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
return "medium";
|
|
471
|
+
}
|
|
472
|
+
});
|
|
315
473
|
if (severities.includes("high"))
|
|
316
474
|
severity = "high";
|
|
317
475
|
else if (severities.includes("medium"))
|
|
@@ -326,8 +484,9 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
326
484
|
active_outages: affectedOutages.length,
|
|
327
485
|
outage_details: affectedOutages.map((outage) => ({
|
|
328
486
|
subject: outage.Subject,
|
|
329
|
-
severity
|
|
330
|
-
posted: outage.
|
|
487
|
+
severity,
|
|
488
|
+
posted: outage.CreationTime,
|
|
489
|
+
last_updated: outage.LastModificationTime,
|
|
331
490
|
})),
|
|
332
491
|
};
|
|
333
492
|
});
|
|
@@ -341,6 +500,60 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
341
500
|
operational: resourceStatus.filter((r) => r.status === "operational").length,
|
|
342
501
|
affected: resourceStatus.filter((r) => r.status === "affected")
|
|
343
502
|
.length,
|
|
503
|
+
api_method: "direct_outages_check",
|
|
504
|
+
resource_status: resourceStatus,
|
|
505
|
+
}, null, 2),
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
async checkResourceStatusViaGroups(resourceIds) {
|
|
511
|
+
// Try to use the more efficient group-based API
|
|
512
|
+
const statusPromises = resourceIds.map(async (resourceId) => {
|
|
513
|
+
try {
|
|
514
|
+
const response = await this.httpClient.get(`/wh2/news/v1/info_groupid/${resourceId}/`);
|
|
515
|
+
const groupData = response.data.results || [];
|
|
516
|
+
const hasOutages = groupData.length > 0;
|
|
517
|
+
return {
|
|
518
|
+
resource_id: resourceId,
|
|
519
|
+
status: hasOutages ? "affected" : "operational",
|
|
520
|
+
severity: hasOutages ? "medium" : null,
|
|
521
|
+
active_outages: groupData.length,
|
|
522
|
+
outage_details: groupData.map((outage) => ({
|
|
523
|
+
subject: outage.Subject,
|
|
524
|
+
posted: outage.CreationTime,
|
|
525
|
+
last_updated: outage.LastModificationTime,
|
|
526
|
+
})),
|
|
527
|
+
api_method: "group_specific",
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
// Fallback to general check if group API fails
|
|
532
|
+
return {
|
|
533
|
+
resource_id: resourceId,
|
|
534
|
+
status: "unknown",
|
|
535
|
+
severity: null,
|
|
536
|
+
active_outages: 0,
|
|
537
|
+
outage_details: [],
|
|
538
|
+
error: `Group API failed for ${resourceId}`,
|
|
539
|
+
api_method: "group_specific_failed",
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
const resourceStatus = await Promise.all(statusPromises);
|
|
544
|
+
return {
|
|
545
|
+
content: [
|
|
546
|
+
{
|
|
547
|
+
type: "text",
|
|
548
|
+
text: JSON.stringify({
|
|
549
|
+
checked_at: new Date().toISOString(),
|
|
550
|
+
resources_checked: resourceIds.length,
|
|
551
|
+
operational: resourceStatus.filter((r) => r.status === "operational").length,
|
|
552
|
+
affected: resourceStatus.filter((r) => r.status === "affected")
|
|
553
|
+
.length,
|
|
554
|
+
unknown: resourceStatus.filter((r) => r.status === "unknown")
|
|
555
|
+
.length,
|
|
556
|
+
api_method: "resource_group_api",
|
|
344
557
|
resource_status: resourceStatus,
|
|
345
558
|
}, null, 2),
|
|
346
559
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@access-mcp/system-status",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "MCP server for ACCESS-CI System Status and Outages API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsc",
|
|
16
|
+
"start": "node dist/index.js",
|
|
17
|
+
"dev": "npm run build && npm start",
|
|
18
|
+
"test": "vitest run --exclude '**/**.integration.test.ts'",
|
|
19
|
+
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
20
|
+
"test:all": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
22
|
+
"test:ui": "vitest --ui",
|
|
23
|
+
"test:coverage": "vitest run --coverage --exclude '**/**.integration.test.ts'",
|
|
16
24
|
"prepublishOnly": "npm run build"
|
|
17
25
|
},
|
|
18
26
|
"keywords": [
|
|
@@ -38,5 +46,12 @@
|
|
|
38
46
|
"dependencies": {
|
|
39
47
|
"@access-mcp/shared": "^0.3.0",
|
|
40
48
|
"express": "^4.18.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^22.0.0",
|
|
52
|
+
"@vitest/ui": "^2.1.9",
|
|
53
|
+
"c8": "^10.1.3",
|
|
54
|
+
"typescript": "^5.0.0",
|
|
55
|
+
"vitest": "^2.1.9"
|
|
41
56
|
}
|
|
42
57
|
}
|