@access-mcp/system-status 0.5.0 → 0.5.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 +16 -132
- package/dist/__tests__/server.integration.test.js +52 -31
- package/dist/__tests__/server.test.js +113 -79
- package/dist/index.js +1 -1
- package/dist/server.d.ts +6 -56
- package/dist/server.js +79 -61
- package/dist/web-server.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# System Status MCP Server
|
|
2
2
|
|
|
3
|
-
MCP server
|
|
3
|
+
MCP server for real-time ACCESS-CI system status including current outages, scheduled maintenance, and operational status.
|
|
4
4
|
|
|
5
5
|
## Usage Examples
|
|
6
6
|
|
|
7
|
-
###
|
|
8
|
-
|
|
7
|
+
### Current Status
|
|
9
8
|
```
|
|
10
9
|
"Current ACCESS-CI outages"
|
|
11
10
|
"Delta operational status"
|
|
@@ -13,8 +12,7 @@ MCP server providing real-time system status information for ACCESS-CI resources
|
|
|
13
12
|
"GPU systems status check"
|
|
14
13
|
```
|
|
15
14
|
|
|
16
|
-
###
|
|
17
|
-
|
|
15
|
+
### Maintenance & Incidents
|
|
18
16
|
```
|
|
19
17
|
"Scheduled maintenance this week"
|
|
20
18
|
"Next Expanse maintenance window"
|
|
@@ -24,36 +22,33 @@ MCP server providing real-time system status information for ACCESS-CI resources
|
|
|
24
22
|
|
|
25
23
|
## Tools
|
|
26
24
|
|
|
27
|
-
### get_infrastructure_news
|
|
25
|
+
### `get_infrastructure_news`
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
Get infrastructure status, outages, and maintenance information for ACCESS-CI resources.
|
|
30
28
|
|
|
31
29
|
**Parameters:**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
| Parameter | Type | Description |
|
|
31
|
+
|-----------|------|-------------|
|
|
32
|
+
| `resource` | string | Filter by resource name (e.g., "delta", "bridges2") |
|
|
33
|
+
| `time` | enum | Time period: `current`, `scheduled`, `past`, `all` (default: "current") |
|
|
34
|
+
| `resource_ids` | array | Check status for specific resource IDs |
|
|
35
|
+
| `limit` | number | Max results (default: 50 for "all", 100 for "past") |
|
|
36
|
+
| `use_group_api` | boolean | Use resource group API for status checking (default: false) |
|
|
38
37
|
|
|
39
38
|
**Examples:**
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
39
|
+
```javascript
|
|
42
40
|
// Get current outages across all resources
|
|
43
41
|
get_infrastructure_news({})
|
|
44
42
|
|
|
45
43
|
// Get scheduled maintenance
|
|
46
44
|
get_infrastructure_news({ time: "scheduled" })
|
|
47
45
|
|
|
48
|
-
// Get comprehensive overview
|
|
46
|
+
// Get comprehensive overview
|
|
49
47
|
get_infrastructure_news({ time: "all" })
|
|
50
48
|
|
|
51
49
|
// Check current status for specific resource
|
|
52
50
|
get_infrastructure_news({ resource: "delta", time: "current" })
|
|
53
51
|
|
|
54
|
-
// Get all news for specific resource
|
|
55
|
-
get_infrastructure_news({ resource: "delta", time: "all" })
|
|
56
|
-
|
|
57
52
|
// Check operational status of specific resources
|
|
58
53
|
get_infrastructure_news({
|
|
59
54
|
resource_ids: ["delta.ncsa.access-ci.org", "bridges2.psc.access-ci.org"]
|
|
@@ -63,10 +58,6 @@ get_infrastructure_news({
|
|
|
63
58
|
get_infrastructure_news({ time: "past", limit: 50 })
|
|
64
59
|
```
|
|
65
60
|
|
|
66
|
-
## Resources
|
|
67
|
-
|
|
68
|
-
- `accessci://system-status`: Current operational status of all ACCESS-CI resources
|
|
69
|
-
|
|
70
61
|
## Installation
|
|
71
62
|
|
|
72
63
|
```bash
|
|
@@ -75,8 +66,6 @@ npm install -g @access-mcp/system-status
|
|
|
75
66
|
|
|
76
67
|
## Configuration
|
|
77
68
|
|
|
78
|
-
Add to your Claude Desktop configuration:
|
|
79
|
-
|
|
80
69
|
```json
|
|
81
70
|
{
|
|
82
71
|
"mcpServers": {
|
|
@@ -88,111 +77,6 @@ Add to your Claude Desktop configuration:
|
|
|
88
77
|
}
|
|
89
78
|
```
|
|
90
79
|
|
|
91
|
-
##
|
|
92
|
-
|
|
93
|
-
### 🚨 **Monitor Current Issues**
|
|
94
|
-
|
|
95
|
-
- "Are there any current outages on ACCESS-CI?"
|
|
96
|
-
- "Is Delta currently operational?"
|
|
97
|
-
- "What systems are experiencing issues right now?"
|
|
98
|
-
|
|
99
|
-
### 🔧 **Track Maintenance Windows**
|
|
100
|
-
|
|
101
|
-
- "When is the next maintenance for Expanse?"
|
|
102
|
-
- "Show me all scheduled maintenance for this week"
|
|
103
|
-
- "Is there upcoming maintenance on Bridges-2?"
|
|
104
|
-
|
|
105
|
-
### 📢 **System Announcements**
|
|
106
|
-
|
|
107
|
-
- "What are the latest system announcements?"
|
|
108
|
-
- "Are there any important notices for ACCESS users?"
|
|
109
|
-
- "Show me recent updates about system changes"
|
|
110
|
-
|
|
111
|
-
### ✅ **Check Resource Status**
|
|
112
|
-
|
|
113
|
-
- "What's the current status of Anvil?"
|
|
114
|
-
- "Is Frontera available for job submission?"
|
|
115
|
-
- "Check if all GPU systems are operational"
|
|
116
|
-
|
|
117
|
-
## Detailed Usage Examples
|
|
118
|
-
|
|
119
|
-
### Checking Current Outages
|
|
120
|
-
|
|
121
|
-
**Natural Language**: "Are there any systems down right now?"
|
|
122
|
-
|
|
123
|
-
**Tool Call**:
|
|
124
|
-
```typescript
|
|
125
|
-
const outages = await get_infrastructure_news({});
|
|
126
|
-
// or explicitly: get_infrastructure_news({ time: "current" })
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
**Returns**: List of active outages with:
|
|
130
|
-
- Affected resources
|
|
131
|
-
- Severity categorization (high/medium/low)
|
|
132
|
-
- Start time and expected resolution
|
|
133
|
-
- Impact description
|
|
134
|
-
- Summary statistics (total outages, affected resources)
|
|
135
|
-
|
|
136
|
-
### Finding Scheduled Maintenance
|
|
137
|
-
|
|
138
|
-
**Natural Language**: "When is Delta scheduled for maintenance?"
|
|
139
|
-
|
|
140
|
-
**Tool Call**:
|
|
141
|
-
```typescript
|
|
142
|
-
const maintenance = await get_infrastructure_news({
|
|
143
|
-
resource: "delta",
|
|
144
|
-
time: "scheduled"
|
|
145
|
-
});
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
**Returns**: Upcoming maintenance windows including:
|
|
149
|
-
- Scheduled start and end times
|
|
150
|
-
- Systems affected
|
|
151
|
-
- Duration in hours
|
|
152
|
-
- Hours until maintenance starts
|
|
153
|
-
- Summary (upcoming in 24h, upcoming this week)
|
|
154
|
-
|
|
155
|
-
### Getting Comprehensive System Overview
|
|
156
|
-
|
|
157
|
-
**Natural Language**: "What are the latest announcements?"
|
|
158
|
-
|
|
159
|
-
**Tool Call**:
|
|
160
|
-
```typescript
|
|
161
|
-
const announcements = await get_infrastructure_news({
|
|
162
|
-
time: "all",
|
|
163
|
-
limit: 10
|
|
164
|
-
});
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
**Returns**: Comprehensive overview including:
|
|
168
|
-
- Current outages
|
|
169
|
-
- Scheduled maintenance
|
|
170
|
-
- Recent past outages (last 30 days)
|
|
171
|
-
- Category breakdown
|
|
172
|
-
- Sorted by relevance (current issues first)
|
|
173
|
-
|
|
174
|
-
### Checking Specific Resource Status
|
|
175
|
-
|
|
176
|
-
**Natural Language**: "Is Expanse available?"
|
|
177
|
-
|
|
178
|
-
**Tool Call**:
|
|
179
|
-
```typescript
|
|
180
|
-
const status = await get_infrastructure_news({
|
|
181
|
-
resource_ids: ["expanse.sdsc.access-ci.org"]
|
|
182
|
-
});
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
**Returns**: Current operational status:
|
|
186
|
-
- Overall status ("operational" or "affected")
|
|
187
|
-
- Severity level if affected
|
|
188
|
-
- Number of active outages
|
|
189
|
-
- Outage details with subjects
|
|
190
|
-
- Timestamp of status check
|
|
191
|
-
|
|
192
|
-
## API Endpoints
|
|
193
|
-
|
|
194
|
-
This server connects to the ACCESS-CI Operations API at `https://operations-api.access-ci.org`
|
|
195
|
-
|
|
196
|
-
## License
|
|
80
|
+
## Resources
|
|
197
81
|
|
|
198
|
-
|
|
82
|
+
- `accessci://system-status` - Current operational status of all ACCESS-CI resources
|
|
@@ -8,12 +8,14 @@ describe("SystemStatusServer Integration Tests", () => {
|
|
|
8
8
|
describe("Real API Integration", () => {
|
|
9
9
|
it("should fetch current outages from real API", async () => {
|
|
10
10
|
const result = await server["handleToolCall"]({
|
|
11
|
+
method: "tools/call",
|
|
11
12
|
params: {
|
|
12
13
|
name: "get_infrastructure_news",
|
|
13
|
-
arguments: { time: "current", limit: 5 }
|
|
14
|
-
}
|
|
14
|
+
arguments: { time: "current", limit: 5 },
|
|
15
|
+
},
|
|
15
16
|
});
|
|
16
|
-
const
|
|
17
|
+
const content = result.content[0];
|
|
18
|
+
const responseData = JSON.parse(content.text);
|
|
17
19
|
expect(responseData).toHaveProperty("total_outages");
|
|
18
20
|
expect(responseData).toHaveProperty("affected_resources");
|
|
19
21
|
expect(responseData).toHaveProperty("severity_counts");
|
|
@@ -32,11 +34,14 @@ describe("SystemStatusServer Integration Tests", () => {
|
|
|
32
34
|
}, 10000);
|
|
33
35
|
it("should fetch scheduled maintenance from real API", async () => {
|
|
34
36
|
const result = await server["handleToolCall"]({
|
|
37
|
+
method: "tools/call",
|
|
35
38
|
params: {
|
|
36
|
-
name: "get_infrastructure_news",
|
|
37
|
-
|
|
39
|
+
name: "get_infrastructure_news",
|
|
40
|
+
arguments: { time: "scheduled", limit: 5 },
|
|
41
|
+
},
|
|
38
42
|
});
|
|
39
|
-
const
|
|
43
|
+
const content = result.content[0];
|
|
44
|
+
const responseData = JSON.parse(content.text);
|
|
40
45
|
expect(responseData).toHaveProperty("total_scheduled");
|
|
41
46
|
expect(responseData).toHaveProperty("upcoming_24h");
|
|
42
47
|
expect(responseData).toHaveProperty("upcoming_week");
|
|
@@ -52,11 +57,14 @@ describe("SystemStatusServer Integration Tests", () => {
|
|
|
52
57
|
}, 10000);
|
|
53
58
|
it("should fetch past outages from real API", async () => {
|
|
54
59
|
const result = await server["handleToolCall"]({
|
|
60
|
+
method: "tools/call",
|
|
55
61
|
params: {
|
|
56
|
-
name: "get_infrastructure_news",
|
|
57
|
-
|
|
62
|
+
name: "get_infrastructure_news",
|
|
63
|
+
arguments: { time: "past", limit: 5 },
|
|
64
|
+
},
|
|
58
65
|
});
|
|
59
|
-
const
|
|
66
|
+
const content = result.content[0];
|
|
67
|
+
const responseData = JSON.parse(content.text);
|
|
60
68
|
expect(responseData).toHaveProperty("total_past_outages");
|
|
61
69
|
expect(responseData).toHaveProperty("recent_outages_30_days");
|
|
62
70
|
expect(responseData).toHaveProperty("affected_resources");
|
|
@@ -74,12 +82,14 @@ describe("SystemStatusServer Integration Tests", () => {
|
|
|
74
82
|
}, 10000);
|
|
75
83
|
it("should get comprehensive system announcements", async () => {
|
|
76
84
|
const result = await server["handleToolCall"]({
|
|
85
|
+
method: "tools/call",
|
|
77
86
|
params: {
|
|
78
87
|
name: "get_infrastructure_news",
|
|
79
|
-
arguments: { time: "all", limit: 20 }
|
|
80
|
-
}
|
|
88
|
+
arguments: { time: "all", limit: 20 },
|
|
89
|
+
},
|
|
81
90
|
});
|
|
82
|
-
const
|
|
91
|
+
const content = result.content[0];
|
|
92
|
+
const responseData = JSON.parse(content.text);
|
|
83
93
|
expect(responseData).toHaveProperty("total_announcements");
|
|
84
94
|
expect(responseData).toHaveProperty("current_outages");
|
|
85
95
|
expect(responseData).toHaveProperty("scheduled_maintenance");
|
|
@@ -99,15 +109,17 @@ describe("SystemStatusServer Integration Tests", () => {
|
|
|
99
109
|
it("should check resource status with direct method", async () => {
|
|
100
110
|
// Test with common resource names that might exist
|
|
101
111
|
const result = await server["handleToolCall"]({
|
|
112
|
+
method: "tools/call",
|
|
102
113
|
params: {
|
|
103
114
|
name: "get_infrastructure_news",
|
|
104
115
|
arguments: {
|
|
105
116
|
ids: ["anvil", "bridges", "jetstream"],
|
|
106
|
-
use_group_api: false
|
|
107
|
-
}
|
|
108
|
-
}
|
|
117
|
+
use_group_api: false,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
109
120
|
});
|
|
110
|
-
const
|
|
121
|
+
const content = result.content[0];
|
|
122
|
+
const responseData = JSON.parse(content.text);
|
|
111
123
|
expect(responseData).toHaveProperty("checked_at");
|
|
112
124
|
expect(responseData).toHaveProperty("resources_checked", 3);
|
|
113
125
|
expect(responseData).toHaveProperty("operational");
|
|
@@ -127,15 +139,17 @@ describe("SystemStatusServer Integration Tests", () => {
|
|
|
127
139
|
it("should test group API functionality", async () => {
|
|
128
140
|
// Test group API with a resource that might have a group ID
|
|
129
141
|
const result = await server["handleToolCall"]({
|
|
142
|
+
method: "tools/call",
|
|
130
143
|
params: {
|
|
131
144
|
name: "get_infrastructure_news",
|
|
132
145
|
arguments: {
|
|
133
146
|
ids: ["anvil"],
|
|
134
|
-
use_group_api: true
|
|
135
|
-
}
|
|
136
|
-
}
|
|
147
|
+
use_group_api: true,
|
|
148
|
+
},
|
|
149
|
+
},
|
|
137
150
|
});
|
|
138
|
-
const
|
|
151
|
+
const content = result.content[0];
|
|
152
|
+
const responseData = JSON.parse(content.text);
|
|
139
153
|
expect(responseData).toHaveProperty("api_method", "resource_group_api");
|
|
140
154
|
expect(responseData).toHaveProperty("resources_checked", 1);
|
|
141
155
|
expect(responseData.resource_status).toHaveLength(1);
|
|
@@ -156,12 +170,14 @@ describe("SystemStatusServer Integration Tests", () => {
|
|
|
156
170
|
}, 10000);
|
|
157
171
|
it("should filter outages by resource correctly", async () => {
|
|
158
172
|
const result = await server["handleToolCall"]({
|
|
173
|
+
method: "tools/call",
|
|
159
174
|
params: {
|
|
160
175
|
name: "get_infrastructure_news",
|
|
161
|
-
arguments: { time: "current", query: "anvil" }
|
|
162
|
-
}
|
|
176
|
+
arguments: { time: "current", query: "anvil" },
|
|
177
|
+
},
|
|
163
178
|
});
|
|
164
|
-
const
|
|
179
|
+
const content = result.content[0];
|
|
180
|
+
const responseData = JSON.parse(content.text);
|
|
165
181
|
expect(responseData).toHaveProperty("total_outages");
|
|
166
182
|
// If there are any results, they should match the filter
|
|
167
183
|
if (responseData.outages.length > 0) {
|
|
@@ -178,11 +194,12 @@ describe("SystemStatusServer Integration Tests", () => {
|
|
|
178
194
|
"accessci://system-status",
|
|
179
195
|
"accessci://outages/current",
|
|
180
196
|
"accessci://outages/scheduled",
|
|
181
|
-
"accessci://outages/past"
|
|
197
|
+
"accessci://outages/past",
|
|
182
198
|
];
|
|
183
199
|
for (const uri of resources) {
|
|
184
200
|
const result = await server["handleResourceRead"]({
|
|
185
|
-
|
|
201
|
+
method: "resources/read",
|
|
202
|
+
params: { uri },
|
|
186
203
|
});
|
|
187
204
|
expect(result.contents).toHaveLength(1);
|
|
188
205
|
expect(result.contents[0]).toHaveProperty("uri", uri);
|
|
@@ -199,24 +216,28 @@ describe("SystemStatusServer Integration Tests", () => {
|
|
|
199
216
|
it("should handle empty API responses", async () => {
|
|
200
217
|
// This tests the robustness of our logic with potentially empty responses
|
|
201
218
|
const result = await server["handleToolCall"]({
|
|
219
|
+
method: "tools/call",
|
|
202
220
|
params: {
|
|
203
221
|
name: "get_infrastructure_news",
|
|
204
|
-
arguments: { time: "current", query: "nonexistent-resource-xyz-12345" }
|
|
205
|
-
}
|
|
222
|
+
arguments: { time: "current", query: "nonexistent-resource-xyz-12345" },
|
|
223
|
+
},
|
|
206
224
|
});
|
|
207
|
-
const
|
|
225
|
+
const content = result.content[0];
|
|
226
|
+
const responseData = JSON.parse(content.text);
|
|
208
227
|
expect(responseData).toHaveProperty("total_outages", 0);
|
|
209
228
|
expect(responseData.outages).toHaveLength(0);
|
|
210
229
|
expect(responseData.affected_resources).toHaveLength(0);
|
|
211
230
|
}, 10000);
|
|
212
231
|
it("should handle large limit values gracefully", async () => {
|
|
213
232
|
const result = await server["handleToolCall"]({
|
|
233
|
+
method: "tools/call",
|
|
214
234
|
params: {
|
|
215
235
|
name: "get_infrastructure_news",
|
|
216
|
-
arguments: { time: "past", limit: 1000 }
|
|
217
|
-
}
|
|
236
|
+
arguments: { time: "past", limit: 1000 },
|
|
237
|
+
},
|
|
218
238
|
});
|
|
219
|
-
const
|
|
239
|
+
const content = result.content[0];
|
|
240
|
+
const responseData = JSON.parse(content.text);
|
|
220
241
|
expect(responseData).toHaveProperty("total_past_outages");
|
|
221
242
|
// Should not crash or timeout
|
|
222
243
|
expect(responseData.outages.length).toBeLessThanOrEqual(1000);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
|
|
2
2
|
import { SystemStatusServer } from "../server.js";
|
|
3
|
+
import { createRequire } from "module";
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const { version } = require("../../package.json");
|
|
3
6
|
// Mock axios
|
|
4
7
|
vi.mock("axios");
|
|
5
8
|
describe("SystemStatusServer", () => {
|
|
@@ -12,9 +15,7 @@ describe("SystemStatusServer", () => {
|
|
|
12
15
|
Content: "Critical issue requiring immediate attention",
|
|
13
16
|
OutageStart: "2024-08-27T10:00:00Z",
|
|
14
17
|
OutageEnd: "2024-08-27T11:00:00Z",
|
|
15
|
-
AffectedResources: [
|
|
16
|
-
{ ResourceName: "Anvil", ResourceID: "anvil-1" }
|
|
17
|
-
]
|
|
18
|
+
AffectedResources: [{ ResourceName: "Anvil", ResourceID: "anvil-1" }],
|
|
18
19
|
},
|
|
19
20
|
{
|
|
20
21
|
id: "2",
|
|
@@ -22,10 +23,8 @@ describe("SystemStatusServer", () => {
|
|
|
22
23
|
Content: "Regular maintenance window",
|
|
23
24
|
OutageStart: "2024-08-27T08:00:00Z",
|
|
24
25
|
OutageEnd: "2024-08-27T08:30:00Z",
|
|
25
|
-
AffectedResources: [
|
|
26
|
-
|
|
27
|
-
]
|
|
28
|
-
}
|
|
26
|
+
AffectedResources: [{ ResourceName: "Bridges-2", ResourceID: "bridges2-1" }],
|
|
27
|
+
},
|
|
29
28
|
];
|
|
30
29
|
const mockFutureOutagesData = [
|
|
31
30
|
{
|
|
@@ -34,10 +33,8 @@ describe("SystemStatusServer", () => {
|
|
|
34
33
|
Content: "Planned maintenance",
|
|
35
34
|
OutageStart: "2024-08-30T10:00:00Z",
|
|
36
35
|
OutageEnd: "2024-08-30T14:00:00Z",
|
|
37
|
-
AffectedResources: [
|
|
38
|
-
|
|
39
|
-
]
|
|
40
|
-
}
|
|
36
|
+
AffectedResources: [{ ResourceName: "Jetstream", ResourceID: "jetstream-1" }],
|
|
37
|
+
},
|
|
41
38
|
];
|
|
42
39
|
const mockPastOutagesData = [
|
|
43
40
|
{
|
|
@@ -47,10 +44,8 @@ describe("SystemStatusServer", () => {
|
|
|
47
44
|
OutageStart: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), // 3 days ago
|
|
48
45
|
OutageEnd: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000 + 6 * 60 * 60 * 1000).toISOString(), // 3 days ago + 6 hours
|
|
49
46
|
OutageType: "Full",
|
|
50
|
-
AffectedResources: [
|
|
51
|
-
|
|
52
|
-
]
|
|
53
|
-
}
|
|
47
|
+
AffectedResources: [{ ResourceName: "Stampede3", ResourceID: "stampede3-1" }],
|
|
48
|
+
},
|
|
54
49
|
];
|
|
55
50
|
beforeEach(() => {
|
|
56
51
|
server = new SystemStatusServer();
|
|
@@ -71,7 +66,7 @@ describe("SystemStatusServer", () => {
|
|
|
71
66
|
it("should initialize with correct server info", () => {
|
|
72
67
|
expect(server).toBeDefined();
|
|
73
68
|
expect(server["serverName"]).toBe("access-mcp-system-status");
|
|
74
|
-
expect(server["version"]).toBe(
|
|
69
|
+
expect(server["version"]).toBe(version);
|
|
75
70
|
expect(server["baseURL"]).toBe("https://operations-api.access-ci.org");
|
|
76
71
|
});
|
|
77
72
|
it("should provide correct tools", () => {
|
|
@@ -82,11 +77,11 @@ describe("SystemStatusServer", () => {
|
|
|
82
77
|
it("should provide correct resources", () => {
|
|
83
78
|
const resources = server["getResources"]();
|
|
84
79
|
expect(resources).toHaveLength(4);
|
|
85
|
-
expect(resources.map(r => r.uri)).toEqual([
|
|
80
|
+
expect(resources.map((r) => r.uri)).toEqual([
|
|
86
81
|
"accessci://system-status",
|
|
87
82
|
"accessci://outages/current",
|
|
88
83
|
"accessci://outages/scheduled",
|
|
89
|
-
"accessci://outages/past"
|
|
84
|
+
"accessci://outages/past",
|
|
90
85
|
]);
|
|
91
86
|
});
|
|
92
87
|
});
|
|
@@ -94,13 +89,15 @@ describe("SystemStatusServer", () => {
|
|
|
94
89
|
it("should fetch and enhance current outages", async () => {
|
|
95
90
|
mockHttpClient.get.mockResolvedValue({
|
|
96
91
|
status: 200,
|
|
97
|
-
data: { results: mockCurrentOutagesData }
|
|
92
|
+
data: { results: mockCurrentOutagesData },
|
|
98
93
|
});
|
|
99
94
|
const result = await server["handleToolCall"]({
|
|
100
|
-
|
|
95
|
+
method: "tools/call",
|
|
96
|
+
params: { name: "get_infrastructure_news", arguments: { time: "current" } },
|
|
101
97
|
});
|
|
102
98
|
expect(mockHttpClient.get).toHaveBeenCalledWith("/wh2/news/v1/affiliation/access-ci.org/current_outages/");
|
|
103
|
-
const
|
|
99
|
+
const content = result.content[0];
|
|
100
|
+
const response = JSON.parse(content.text);
|
|
104
101
|
expect(response.total_outages).toBe(2);
|
|
105
102
|
expect(response.affected_resources).toEqual(["Anvil", "Bridges-2"]);
|
|
106
103
|
expect(response.severity_counts).toHaveProperty("high", 1); // Emergency
|
|
@@ -110,27 +107,31 @@ describe("SystemStatusServer", () => {
|
|
|
110
107
|
it("should filter outages by resource", async () => {
|
|
111
108
|
mockHttpClient.get.mockResolvedValue({
|
|
112
109
|
status: 200,
|
|
113
|
-
data: { results: mockCurrentOutagesData }
|
|
110
|
+
data: { results: mockCurrentOutagesData },
|
|
114
111
|
});
|
|
115
112
|
const result = await server["handleToolCall"]({
|
|
113
|
+
method: "tools/call",
|
|
116
114
|
params: {
|
|
117
115
|
name: "get_infrastructure_news",
|
|
118
|
-
arguments: { query: "Anvil", time: "current" }
|
|
119
|
-
}
|
|
116
|
+
arguments: { query: "Anvil", time: "current" },
|
|
117
|
+
},
|
|
120
118
|
});
|
|
121
|
-
const
|
|
119
|
+
const content = result.content[0];
|
|
120
|
+
const response = JSON.parse(content.text);
|
|
122
121
|
expect(response.total_outages).toBe(1);
|
|
123
122
|
expect(response.outages[0].Subject).toContain("Anvil");
|
|
124
123
|
});
|
|
125
124
|
it("should categorize severity correctly", async () => {
|
|
126
125
|
mockHttpClient.get.mockResolvedValue({
|
|
127
126
|
status: 200,
|
|
128
|
-
data: { results: mockCurrentOutagesData }
|
|
127
|
+
data: { results: mockCurrentOutagesData },
|
|
129
128
|
});
|
|
130
129
|
const result = await server["handleToolCall"]({
|
|
131
|
-
|
|
130
|
+
method: "tools/call",
|
|
131
|
+
params: { name: "get_infrastructure_news", arguments: { time: "current" } },
|
|
132
132
|
});
|
|
133
|
-
const
|
|
133
|
+
const content = result.content[0];
|
|
134
|
+
const response = JSON.parse(content.text);
|
|
134
135
|
const emergencyOutage = response.outages.find((o) => o.Subject.includes("Emergency"));
|
|
135
136
|
const maintenanceOutage = response.outages.find((o) => o.Subject.includes("Scheduled"));
|
|
136
137
|
expect(emergencyOutage.severity).toBe("high");
|
|
@@ -141,12 +142,14 @@ describe("SystemStatusServer", () => {
|
|
|
141
142
|
it("should fetch and enhance scheduled maintenance", async () => {
|
|
142
143
|
mockHttpClient.get.mockResolvedValue({
|
|
143
144
|
status: 200,
|
|
144
|
-
data: { results: mockFutureOutagesData }
|
|
145
|
+
data: { results: mockFutureOutagesData },
|
|
145
146
|
});
|
|
146
147
|
const result = await server["handleToolCall"]({
|
|
147
|
-
|
|
148
|
+
method: "tools/call",
|
|
149
|
+
params: { name: "get_infrastructure_news", arguments: { time: "scheduled" } },
|
|
148
150
|
});
|
|
149
|
-
const
|
|
151
|
+
const content = result.content[0];
|
|
152
|
+
const response = JSON.parse(content.text);
|
|
150
153
|
expect(response.total_scheduled).toBe(1);
|
|
151
154
|
expect(response.affected_resources).toEqual(["Jetstream"]);
|
|
152
155
|
expect(response.maintenance[0]).toHaveProperty("hours_until_start");
|
|
@@ -154,19 +157,23 @@ describe("SystemStatusServer", () => {
|
|
|
154
157
|
expect(response.maintenance[0]).toHaveProperty("has_scheduled_time", true);
|
|
155
158
|
});
|
|
156
159
|
it("should handle missing scheduled times", async () => {
|
|
157
|
-
const dataWithoutSchedule = [
|
|
160
|
+
const dataWithoutSchedule = [
|
|
161
|
+
{
|
|
158
162
|
...mockFutureOutagesData[0],
|
|
159
163
|
OutageStart: null,
|
|
160
|
-
OutageEnd: null
|
|
161
|
-
}
|
|
164
|
+
OutageEnd: null,
|
|
165
|
+
},
|
|
166
|
+
];
|
|
162
167
|
mockHttpClient.get.mockResolvedValue({
|
|
163
168
|
status: 200,
|
|
164
|
-
data: { results: dataWithoutSchedule }
|
|
169
|
+
data: { results: dataWithoutSchedule },
|
|
165
170
|
});
|
|
166
171
|
const result = await server["handleToolCall"]({
|
|
167
|
-
|
|
172
|
+
method: "tools/call",
|
|
173
|
+
params: { name: "get_infrastructure_news", arguments: { time: "scheduled" } },
|
|
168
174
|
});
|
|
169
|
-
const
|
|
175
|
+
const content = result.content[0];
|
|
176
|
+
const response = JSON.parse(content.text);
|
|
170
177
|
expect(response.maintenance[0].has_scheduled_time).toBe(false);
|
|
171
178
|
expect(response.maintenance[0].duration_hours).toBe(null);
|
|
172
179
|
});
|
|
@@ -175,22 +182,24 @@ describe("SystemStatusServer", () => {
|
|
|
175
182
|
{
|
|
176
183
|
...mockFutureOutagesData[0],
|
|
177
184
|
OutageStart: "2024-08-31T10:00:00Z", // Later
|
|
178
|
-
Subject: "Later maintenance"
|
|
185
|
+
Subject: "Later maintenance",
|
|
179
186
|
},
|
|
180
187
|
{
|
|
181
188
|
...mockFutureOutagesData[0],
|
|
182
189
|
OutageStart: "2024-08-30T10:00:00Z", // Earlier
|
|
183
|
-
Subject: "Earlier maintenance"
|
|
184
|
-
}
|
|
190
|
+
Subject: "Earlier maintenance",
|
|
191
|
+
},
|
|
185
192
|
];
|
|
186
193
|
mockHttpClient.get.mockResolvedValue({
|
|
187
194
|
status: 200,
|
|
188
|
-
data: { results: multipleMaintenanceData }
|
|
195
|
+
data: { results: multipleMaintenanceData },
|
|
189
196
|
});
|
|
190
197
|
const result = await server["handleToolCall"]({
|
|
191
|
-
|
|
198
|
+
method: "tools/call",
|
|
199
|
+
params: { name: "get_infrastructure_news", arguments: { time: "scheduled" } },
|
|
192
200
|
});
|
|
193
|
-
const
|
|
201
|
+
const content = result.content[0];
|
|
202
|
+
const response = JSON.parse(content.text);
|
|
194
203
|
expect(response.maintenance[0].Subject).toBe("Earlier maintenance");
|
|
195
204
|
expect(response.maintenance[1].Subject).toBe("Later maintenance");
|
|
196
205
|
});
|
|
@@ -199,12 +208,14 @@ describe("SystemStatusServer", () => {
|
|
|
199
208
|
it("should fetch and enhance past outages", async () => {
|
|
200
209
|
mockHttpClient.get.mockResolvedValue({
|
|
201
210
|
status: 200,
|
|
202
|
-
data: { results: mockPastOutagesData }
|
|
211
|
+
data: { results: mockPastOutagesData },
|
|
203
212
|
});
|
|
204
213
|
const result = await server["handleToolCall"]({
|
|
205
|
-
|
|
214
|
+
method: "tools/call",
|
|
215
|
+
params: { name: "get_infrastructure_news", arguments: { time: "past" } },
|
|
206
216
|
});
|
|
207
|
-
const
|
|
217
|
+
const content = result.content[0];
|
|
218
|
+
const response = JSON.parse(content.text);
|
|
208
219
|
expect(response.total_past_outages).toBe(1);
|
|
209
220
|
expect(response.outage_types).toEqual(["Full"]);
|
|
210
221
|
expect(response.average_duration_hours).toBe(6); // 6 hour duration
|
|
@@ -212,22 +223,26 @@ describe("SystemStatusServer", () => {
|
|
|
212
223
|
expect(response.outages[0]).toHaveProperty("days_ago");
|
|
213
224
|
});
|
|
214
225
|
it("should apply limit correctly", async () => {
|
|
215
|
-
const manyOutages = Array(50)
|
|
226
|
+
const manyOutages = Array(50)
|
|
227
|
+
.fill(0)
|
|
228
|
+
.map((_, i) => ({
|
|
216
229
|
...mockPastOutagesData[0],
|
|
217
230
|
id: `past-${i}`,
|
|
218
|
-
Subject: `Past outage ${i}
|
|
231
|
+
Subject: `Past outage ${i}`,
|
|
219
232
|
}));
|
|
220
233
|
mockHttpClient.get.mockResolvedValue({
|
|
221
234
|
status: 200,
|
|
222
|
-
data: { results: manyOutages }
|
|
235
|
+
data: { results: manyOutages },
|
|
223
236
|
});
|
|
224
237
|
const result = await server["handleToolCall"]({
|
|
238
|
+
method: "tools/call",
|
|
225
239
|
params: {
|
|
226
240
|
name: "get_infrastructure_news",
|
|
227
|
-
arguments: { time: "past", limit: 10 }
|
|
228
|
-
}
|
|
241
|
+
arguments: { time: "past", limit: 10 },
|
|
242
|
+
},
|
|
229
243
|
});
|
|
230
|
-
const
|
|
244
|
+
const content = result.content[0];
|
|
245
|
+
const response = JSON.parse(content.text);
|
|
231
246
|
expect(response.outages).toHaveLength(10);
|
|
232
247
|
});
|
|
233
248
|
});
|
|
@@ -238,10 +253,12 @@ describe("SystemStatusServer", () => {
|
|
|
238
253
|
.mockResolvedValueOnce({ status: 200, data: { results: mockFutureOutagesData } })
|
|
239
254
|
.mockResolvedValueOnce({ status: 200, data: { results: mockPastOutagesData } });
|
|
240
255
|
const result = await server["handleToolCall"]({
|
|
241
|
-
|
|
256
|
+
method: "tools/call",
|
|
257
|
+
params: { name: "get_infrastructure_news", arguments: { time: "all" } },
|
|
242
258
|
});
|
|
243
259
|
expect(mockHttpClient.get).toHaveBeenCalledTimes(3);
|
|
244
|
-
const
|
|
260
|
+
const content = result.content[0];
|
|
261
|
+
const response = JSON.parse(content.text);
|
|
245
262
|
expect(response.current_outages).toBe(2);
|
|
246
263
|
expect(response.scheduled_maintenance).toBe(1);
|
|
247
264
|
expect(response.recent_past_outages).toBe(1); // Within 30 days
|
|
@@ -255,9 +272,11 @@ describe("SystemStatusServer", () => {
|
|
|
255
272
|
.mockResolvedValueOnce({ status: 200, data: { results: mockFutureOutagesData } })
|
|
256
273
|
.mockResolvedValueOnce({ status: 200, data: { results: mockPastOutagesData } });
|
|
257
274
|
const result = await server["handleToolCall"]({
|
|
258
|
-
|
|
275
|
+
method: "tools/call",
|
|
276
|
+
params: { name: "get_infrastructure_news", arguments: { time: "all" } },
|
|
259
277
|
});
|
|
260
|
-
const
|
|
278
|
+
const content = result.content[0];
|
|
279
|
+
const response = JSON.parse(content.text);
|
|
261
280
|
const firstAnnouncement = response.announcements[0];
|
|
262
281
|
expect(firstAnnouncement.category).toBe("current");
|
|
263
282
|
});
|
|
@@ -266,15 +285,17 @@ describe("SystemStatusServer", () => {
|
|
|
266
285
|
it("should check resource status efficiently (direct method)", async () => {
|
|
267
286
|
mockHttpClient.get.mockResolvedValue({
|
|
268
287
|
status: 200,
|
|
269
|
-
data: { results: mockCurrentOutagesData }
|
|
288
|
+
data: { results: mockCurrentOutagesData },
|
|
270
289
|
});
|
|
271
290
|
const result = await server["handleToolCall"]({
|
|
291
|
+
method: "tools/call",
|
|
272
292
|
params: {
|
|
273
293
|
name: "get_infrastructure_news",
|
|
274
|
-
arguments: { ids: ["anvil-1", "unknown-resource"] }
|
|
275
|
-
}
|
|
294
|
+
arguments: { ids: ["anvil-1", "unknown-resource"] },
|
|
295
|
+
},
|
|
276
296
|
});
|
|
277
|
-
const
|
|
297
|
+
const content = result.content[0];
|
|
298
|
+
const response = JSON.parse(content.text);
|
|
278
299
|
expect(response.api_method).toBe("direct_outages_check");
|
|
279
300
|
expect(response.resources_checked).toBe(2);
|
|
280
301
|
expect(response.operational).toBe(1); // unknown-resource
|
|
@@ -286,19 +307,21 @@ describe("SystemStatusServer", () => {
|
|
|
286
307
|
it("should use group API when requested", async () => {
|
|
287
308
|
mockHttpClient.get.mockResolvedValue({
|
|
288
309
|
status: 200,
|
|
289
|
-
data: { results: [] } // No outages for this group
|
|
310
|
+
data: { results: [] }, // No outages for this group
|
|
290
311
|
});
|
|
291
312
|
const result = await server["handleToolCall"]({
|
|
313
|
+
method: "tools/call",
|
|
292
314
|
params: {
|
|
293
315
|
name: "get_infrastructure_news",
|
|
294
316
|
arguments: {
|
|
295
317
|
ids: ["anvil"],
|
|
296
|
-
use_group_api: true
|
|
297
|
-
}
|
|
298
|
-
}
|
|
318
|
+
use_group_api: true,
|
|
319
|
+
},
|
|
320
|
+
},
|
|
299
321
|
});
|
|
300
322
|
expect(mockHttpClient.get).toHaveBeenCalledWith("/wh2/news/v1/info_groupid/anvil/");
|
|
301
|
-
const
|
|
323
|
+
const content = result.content[0];
|
|
324
|
+
const response = JSON.parse(content.text);
|
|
302
325
|
expect(response.api_method).toBe("resource_group_api");
|
|
303
326
|
expect(response.resource_status[0].status).toBe("operational");
|
|
304
327
|
expect(response.resource_status[0].api_method).toBe("group_specific");
|
|
@@ -306,15 +329,17 @@ describe("SystemStatusServer", () => {
|
|
|
306
329
|
it("should handle group API failures gracefully", async () => {
|
|
307
330
|
mockHttpClient.get.mockRejectedValue(new Error("API Error"));
|
|
308
331
|
const result = await server["handleToolCall"]({
|
|
332
|
+
method: "tools/call",
|
|
309
333
|
params: {
|
|
310
334
|
name: "get_infrastructure_news",
|
|
311
335
|
arguments: {
|
|
312
336
|
ids: ["invalid-resource"],
|
|
313
|
-
use_group_api: true
|
|
314
|
-
}
|
|
315
|
-
}
|
|
337
|
+
use_group_api: true,
|
|
338
|
+
},
|
|
339
|
+
},
|
|
316
340
|
});
|
|
317
|
-
const
|
|
341
|
+
const content = result.content[0];
|
|
342
|
+
const response = JSON.parse(content.text);
|
|
318
343
|
expect(response).toHaveProperty("unknown", 1);
|
|
319
344
|
expect(response.resource_status[0].status).toBe("unknown");
|
|
320
345
|
expect(response.resource_status[0].api_method).toBe("group_specific_failed");
|
|
@@ -325,37 +350,45 @@ describe("SystemStatusServer", () => {
|
|
|
325
350
|
it("should handle API errors gracefully", async () => {
|
|
326
351
|
mockHttpClient.get.mockResolvedValue({
|
|
327
352
|
status: 500,
|
|
328
|
-
statusText: "Internal Server Error"
|
|
353
|
+
statusText: "Internal Server Error",
|
|
354
|
+
data: null,
|
|
329
355
|
});
|
|
330
356
|
const result = await server["handleToolCall"]({
|
|
331
|
-
|
|
357
|
+
method: "tools/call",
|
|
358
|
+
params: { name: "get_infrastructure_news", arguments: { time: "current" } },
|
|
332
359
|
});
|
|
333
|
-
const
|
|
360
|
+
const content = result.content[0];
|
|
361
|
+
const response = JSON.parse(content.text);
|
|
334
362
|
expect(response.error).toBeDefined();
|
|
335
363
|
});
|
|
336
364
|
it("should handle network errors", async () => {
|
|
337
365
|
mockHttpClient.get.mockRejectedValue(new Error("Network error"));
|
|
338
366
|
const result = await server["handleToolCall"]({
|
|
339
|
-
|
|
367
|
+
method: "tools/call",
|
|
368
|
+
params: { name: "get_infrastructure_news", arguments: { time: "current" } },
|
|
340
369
|
});
|
|
341
|
-
const
|
|
370
|
+
const content = result.content[0];
|
|
371
|
+
const response = JSON.parse(content.text);
|
|
342
372
|
expect(response.error).toBe("Network error");
|
|
343
373
|
});
|
|
344
374
|
it("should handle unknown tools", async () => {
|
|
345
375
|
const result = await server["handleToolCall"]({
|
|
346
|
-
|
|
376
|
+
method: "tools/call",
|
|
377
|
+
params: { name: "unknown_tool", arguments: {} },
|
|
347
378
|
});
|
|
348
|
-
|
|
379
|
+
const content = result.content[0];
|
|
380
|
+
expect(content.text).toContain("Unknown tool");
|
|
349
381
|
});
|
|
350
382
|
});
|
|
351
383
|
describe("Resource Handling", () => {
|
|
352
384
|
it("should handle resource reads correctly", async () => {
|
|
353
385
|
mockHttpClient.get.mockResolvedValue({
|
|
354
386
|
status: 200,
|
|
355
|
-
data: { results: mockCurrentOutagesData }
|
|
387
|
+
data: { results: mockCurrentOutagesData },
|
|
356
388
|
});
|
|
357
389
|
const result = await server["handleResourceRead"]({
|
|
358
|
-
|
|
390
|
+
method: "resources/read",
|
|
391
|
+
params: { uri: "accessci://outages/current" },
|
|
359
392
|
});
|
|
360
393
|
expect(result.contents[0].mimeType).toBe("application/json");
|
|
361
394
|
expect(result.contents[0].text).toBeDefined();
|
|
@@ -363,7 +396,8 @@ describe("SystemStatusServer", () => {
|
|
|
363
396
|
it("should handle unknown resources", async () => {
|
|
364
397
|
await expect(async () => {
|
|
365
398
|
await server["handleResourceRead"]({
|
|
366
|
-
|
|
399
|
+
method: "resources/read",
|
|
400
|
+
params: { uri: "accessci://unknown" },
|
|
367
401
|
});
|
|
368
402
|
}).rejects.toThrow("Unknown resource");
|
|
369
403
|
});
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ async function main() {
|
|
|
5
5
|
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
|
|
6
6
|
await server.start(port ? { httpPort: port } : undefined);
|
|
7
7
|
}
|
|
8
|
-
main().catch((
|
|
8
|
+
main().catch(() => {
|
|
9
9
|
// Log errors to a file instead of stderr to avoid interfering with JSON-RPC
|
|
10
10
|
process.exit(1);
|
|
11
11
|
});
|
package/dist/server.d.ts
CHANGED
|
@@ -1,66 +1,16 @@
|
|
|
1
|
-
import { BaseAccessServer } from "@access-mcp/shared";
|
|
1
|
+
import { BaseAccessServer, Tool, Resource, CallToolResult } from "@access-mcp/shared";
|
|
2
|
+
import { CallToolRequest, ReadResourceRequest, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
3
|
export declare class SystemStatusServer extends BaseAccessServer {
|
|
3
4
|
constructor();
|
|
4
|
-
protected getTools():
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
inputSchema: {
|
|
8
|
-
type: string;
|
|
9
|
-
properties: {
|
|
10
|
-
query: {
|
|
11
|
-
type: string;
|
|
12
|
-
description: string;
|
|
13
|
-
};
|
|
14
|
-
time: {
|
|
15
|
-
type: string;
|
|
16
|
-
enum: string[];
|
|
17
|
-
description: string;
|
|
18
|
-
default: string;
|
|
19
|
-
};
|
|
20
|
-
ids: {
|
|
21
|
-
type: string;
|
|
22
|
-
items: {
|
|
23
|
-
type: string;
|
|
24
|
-
};
|
|
25
|
-
description: string;
|
|
26
|
-
};
|
|
27
|
-
limit: {
|
|
28
|
-
type: string;
|
|
29
|
-
description: string;
|
|
30
|
-
default: number;
|
|
31
|
-
};
|
|
32
|
-
use_group_api: {
|
|
33
|
-
type: string;
|
|
34
|
-
description: string;
|
|
35
|
-
default: boolean;
|
|
36
|
-
};
|
|
37
|
-
};
|
|
38
|
-
};
|
|
39
|
-
}[];
|
|
40
|
-
protected getResources(): {
|
|
41
|
-
uri: string;
|
|
42
|
-
name: string;
|
|
43
|
-
description: string;
|
|
44
|
-
mimeType: string;
|
|
45
|
-
}[];
|
|
46
|
-
handleToolCall(request: any): Promise<{
|
|
47
|
-
content: {
|
|
48
|
-
type: string;
|
|
49
|
-
text: string;
|
|
50
|
-
}[];
|
|
51
|
-
}>;
|
|
5
|
+
protected getTools(): Tool[];
|
|
6
|
+
protected getResources(): Resource[];
|
|
7
|
+
protected handleToolCall(request: CallToolRequest): Promise<CallToolResult>;
|
|
52
8
|
/**
|
|
53
9
|
* Router for consolidated get_infrastructure_news tool
|
|
54
10
|
* Routes to appropriate handler based on parameters
|
|
55
11
|
*/
|
|
56
12
|
private getInfrastructureNewsRouter;
|
|
57
|
-
handleResourceRead(request:
|
|
58
|
-
contents: {
|
|
59
|
-
uri: any;
|
|
60
|
-
mimeType: string;
|
|
61
|
-
text: string;
|
|
62
|
-
}[];
|
|
63
|
-
}>;
|
|
13
|
+
protected handleResourceRead(request: ReadResourceRequest): Promise<ReadResourceResult>;
|
|
64
14
|
private getCurrentOutages;
|
|
65
15
|
private getScheduledMaintenance;
|
|
66
16
|
private getPastOutages;
|
package/dist/server.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { BaseAccessServer, handleApiError } from "@access-mcp/shared";
|
|
1
|
+
import { BaseAccessServer, handleApiError, } from "@access-mcp/shared";
|
|
2
|
+
import { createRequire } from "module";
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const { version } = require("../package.json");
|
|
2
5
|
export class SystemStatusServer extends BaseAccessServer {
|
|
3
6
|
constructor() {
|
|
4
|
-
super("access-mcp-system-status",
|
|
7
|
+
super("access-mcp-system-status", version, "https://operations-api.access-ci.org");
|
|
5
8
|
}
|
|
6
9
|
getTools() {
|
|
7
10
|
return [
|
|
@@ -13,30 +16,30 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
13
16
|
properties: {
|
|
14
17
|
query: {
|
|
15
18
|
type: "string",
|
|
16
|
-
description: "Filter by resource name (e.g., 'delta', 'bridges2')"
|
|
19
|
+
description: "Filter by resource name (e.g., 'delta', 'bridges2')",
|
|
17
20
|
},
|
|
18
21
|
time: {
|
|
19
22
|
type: "string",
|
|
20
23
|
enum: ["current", "scheduled", "past", "all"],
|
|
21
24
|
description: "Period: current (active), scheduled (future), past, all",
|
|
22
|
-
default: "current"
|
|
25
|
+
default: "current",
|
|
23
26
|
},
|
|
24
27
|
ids: {
|
|
25
28
|
type: "array",
|
|
26
29
|
items: { type: "string" },
|
|
27
|
-
description: "Check status for specific resource IDs"
|
|
30
|
+
description: "Check status for specific resource IDs",
|
|
28
31
|
},
|
|
29
32
|
limit: {
|
|
30
33
|
type: "number",
|
|
31
34
|
description: "Max results (default: 50)",
|
|
32
|
-
default: 50
|
|
35
|
+
default: 50,
|
|
33
36
|
},
|
|
34
37
|
use_group_api: {
|
|
35
38
|
type: "boolean",
|
|
36
39
|
description: "Use group API for status (with ids only)",
|
|
37
|
-
default: false
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
+
default: false,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
40
43
|
},
|
|
41
44
|
},
|
|
42
45
|
];
|
|
@@ -71,15 +74,16 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
71
74
|
}
|
|
72
75
|
async handleToolCall(request) {
|
|
73
76
|
const { name, arguments: args = {} } = request.params;
|
|
77
|
+
const typedArgs = args;
|
|
74
78
|
try {
|
|
75
79
|
switch (name) {
|
|
76
80
|
case "get_infrastructure_news":
|
|
77
81
|
return await this.getInfrastructureNewsRouter({
|
|
78
|
-
resource:
|
|
79
|
-
time:
|
|
80
|
-
resource_ids:
|
|
81
|
-
limit:
|
|
82
|
-
use_group_api:
|
|
82
|
+
resource: typedArgs.query,
|
|
83
|
+
time: typedArgs.time,
|
|
84
|
+
resource_ids: typedArgs.ids,
|
|
85
|
+
limit: typedArgs.limit,
|
|
86
|
+
use_group_api: typedArgs.use_group_api,
|
|
83
87
|
});
|
|
84
88
|
default:
|
|
85
89
|
return this.errorResponse(`Unknown tool: ${name}`);
|
|
@@ -116,7 +120,7 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
116
120
|
async handleResourceRead(request) {
|
|
117
121
|
const { uri } = request.params;
|
|
118
122
|
switch (uri) {
|
|
119
|
-
case "accessci://system-status":
|
|
123
|
+
case "accessci://system-status": {
|
|
120
124
|
return {
|
|
121
125
|
contents: [
|
|
122
126
|
{
|
|
@@ -126,39 +130,49 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
126
130
|
},
|
|
127
131
|
],
|
|
128
132
|
};
|
|
129
|
-
|
|
133
|
+
}
|
|
134
|
+
case "accessci://outages/current": {
|
|
130
135
|
const currentOutages = await this.getCurrentOutages();
|
|
136
|
+
const content = currentOutages.content[0];
|
|
137
|
+
const text = content.type === "text" ? content.text : "";
|
|
131
138
|
return {
|
|
132
139
|
contents: [
|
|
133
140
|
{
|
|
134
141
|
uri,
|
|
135
142
|
mimeType: "application/json",
|
|
136
|
-
text
|
|
143
|
+
text,
|
|
137
144
|
},
|
|
138
145
|
],
|
|
139
146
|
};
|
|
140
|
-
|
|
147
|
+
}
|
|
148
|
+
case "accessci://outages/scheduled": {
|
|
141
149
|
const scheduledMaintenance = await this.getScheduledMaintenance();
|
|
150
|
+
const content = scheduledMaintenance.content[0];
|
|
151
|
+
const text = content.type === "text" ? content.text : "";
|
|
142
152
|
return {
|
|
143
153
|
contents: [
|
|
144
154
|
{
|
|
145
155
|
uri,
|
|
146
156
|
mimeType: "application/json",
|
|
147
|
-
text
|
|
157
|
+
text,
|
|
148
158
|
},
|
|
149
159
|
],
|
|
150
160
|
};
|
|
151
|
-
|
|
161
|
+
}
|
|
162
|
+
case "accessci://outages/past": {
|
|
152
163
|
const pastOutages = await this.getPastOutages();
|
|
164
|
+
const content = pastOutages.content[0];
|
|
165
|
+
const text = content.type === "text" ? content.text : "";
|
|
153
166
|
return {
|
|
154
167
|
contents: [
|
|
155
168
|
{
|
|
156
169
|
uri,
|
|
157
170
|
mimeType: "application/json",
|
|
158
|
-
text
|
|
171
|
+
text,
|
|
159
172
|
},
|
|
160
173
|
],
|
|
161
174
|
};
|
|
175
|
+
}
|
|
162
176
|
default:
|
|
163
177
|
throw new Error(`Unknown resource: ${uri}`);
|
|
164
178
|
}
|
|
@@ -178,9 +192,11 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
178
192
|
const severityCounts = { high: 0, medium: 0, low: 0, unknown: 0 };
|
|
179
193
|
// Enhance outages with status summary
|
|
180
194
|
const enhancedOutages = outages.map((outage) => {
|
|
181
|
-
// Track affected resources
|
|
195
|
+
// Track affected resources (use ResourceID as fallback if ResourceName is missing)
|
|
182
196
|
outage.AffectedResources?.forEach((resource) => {
|
|
183
|
-
|
|
197
|
+
const resourceIdentifier = resource.ResourceName || resource.ResourceID;
|
|
198
|
+
if (resourceIdentifier)
|
|
199
|
+
affectedResources.add(String(resourceIdentifier));
|
|
184
200
|
});
|
|
185
201
|
// Categorize severity (basic heuristic)
|
|
186
202
|
const subject = outage.Subject?.toLowerCase() || "";
|
|
@@ -188,8 +204,7 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
188
204
|
if (subject.includes("emergency") || subject.includes("critical")) {
|
|
189
205
|
severity = "high";
|
|
190
206
|
}
|
|
191
|
-
else if (subject.includes("maintenance") ||
|
|
192
|
-
subject.includes("scheduled")) {
|
|
207
|
+
else if (subject.includes("maintenance") || subject.includes("scheduled")) {
|
|
193
208
|
severity = "low";
|
|
194
209
|
}
|
|
195
210
|
else {
|
|
@@ -228,8 +243,8 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
228
243
|
}
|
|
229
244
|
// Sort by scheduled start time
|
|
230
245
|
maintenance.sort((a, b) => {
|
|
231
|
-
const dateA = new Date(a.OutageStart);
|
|
232
|
-
const dateB = new Date(b.OutageStart);
|
|
246
|
+
const dateA = new Date(a.OutageStart || "");
|
|
247
|
+
const dateB = new Date(b.OutageStart || "");
|
|
233
248
|
return dateA.getTime() - dateB.getTime();
|
|
234
249
|
});
|
|
235
250
|
// Initialize tracking variables
|
|
@@ -239,11 +254,13 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
239
254
|
const enhancedMaintenance = maintenance.map((item) => {
|
|
240
255
|
// Track affected resources
|
|
241
256
|
item.AffectedResources?.forEach((resource) => {
|
|
242
|
-
|
|
257
|
+
const resourceIdentifier = resource.ResourceName || resource.ResourceID;
|
|
258
|
+
if (resourceIdentifier)
|
|
259
|
+
affectedResources.add(String(resourceIdentifier));
|
|
243
260
|
});
|
|
244
261
|
// Check timing - use OutageStart for scheduling
|
|
245
262
|
const hasScheduledTime = !!item.OutageStart;
|
|
246
|
-
const startTime = new Date(item.OutageStart);
|
|
263
|
+
const startTime = new Date(item.OutageStart || "");
|
|
247
264
|
const now = new Date();
|
|
248
265
|
const hoursUntil = (startTime.getTime() - now.getTime()) / (1000 * 60 * 60);
|
|
249
266
|
if (hoursUntil <= 24)
|
|
@@ -256,8 +273,7 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
256
273
|
scheduled_end: item.OutageEnd,
|
|
257
274
|
hours_until_start: Math.max(0, Math.round(hoursUntil)),
|
|
258
275
|
duration_hours: item.OutageEnd && item.OutageStart
|
|
259
|
-
? Math.round((new Date(item.OutageEnd).getTime() -
|
|
260
|
-
new Date(item.OutageStart).getTime()) /
|
|
276
|
+
? Math.round((new Date(item.OutageEnd).getTime() - new Date(item.OutageStart).getTime()) /
|
|
261
277
|
(1000 * 60 * 60))
|
|
262
278
|
: null,
|
|
263
279
|
has_scheduled_time: hasScheduledTime,
|
|
@@ -291,8 +307,8 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
291
307
|
}
|
|
292
308
|
// Sort by outage end time (most recent first)
|
|
293
309
|
pastOutages.sort((a, b) => {
|
|
294
|
-
const dateA = new Date(a.OutageEnd);
|
|
295
|
-
const dateB = new Date(b.OutageEnd);
|
|
310
|
+
const dateA = new Date(a.OutageEnd || "");
|
|
311
|
+
const dateB = new Date(b.OutageEnd || "");
|
|
296
312
|
return dateB.getTime() - dateA.getTime();
|
|
297
313
|
});
|
|
298
314
|
// Apply limit
|
|
@@ -303,23 +319,25 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
303
319
|
const affectedResources = new Set();
|
|
304
320
|
const outageTypes = new Set();
|
|
305
321
|
const recentOutages = pastOutages.filter((outage) => {
|
|
306
|
-
const endTime = new Date(outage.OutageEnd);
|
|
322
|
+
const endTime = new Date(outage.OutageEnd || "");
|
|
307
323
|
const daysAgo = (Date.now() - endTime.getTime()) / (1000 * 60 * 60 * 24);
|
|
308
324
|
return daysAgo <= 30; // Last 30 days
|
|
309
325
|
});
|
|
310
326
|
// Enhance outages with calculated fields
|
|
311
327
|
const enhancedOutages = pastOutages.map((outage) => {
|
|
312
|
-
// Track affected resources
|
|
328
|
+
// Track affected resources (use ResourceID as fallback if ResourceName is missing)
|
|
313
329
|
outage.AffectedResources?.forEach((resource) => {
|
|
314
|
-
|
|
330
|
+
const resourceIdentifier = resource.ResourceName || resource.ResourceID;
|
|
331
|
+
if (resourceIdentifier)
|
|
332
|
+
affectedResources.add(String(resourceIdentifier));
|
|
315
333
|
});
|
|
316
334
|
// Track outage types
|
|
317
335
|
if (outage.OutageType) {
|
|
318
336
|
outageTypes.add(outage.OutageType);
|
|
319
337
|
}
|
|
320
338
|
// Calculate duration
|
|
321
|
-
const startTime = new Date(outage.OutageStart);
|
|
322
|
-
const endTime = new Date(outage.OutageEnd);
|
|
339
|
+
const startTime = new Date(outage.OutageStart || "");
|
|
340
|
+
const endTime = new Date(outage.OutageEnd || "");
|
|
323
341
|
const durationHours = Math.round((endTime.getTime() - startTime.getTime()) / (1000 * 60 * 60));
|
|
324
342
|
// Calculate how long ago it ended
|
|
325
343
|
const daysAgo = Math.round((Date.now() - endTime.getTime()) / (1000 * 60 * 60 * 24));
|
|
@@ -339,9 +357,9 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
339
357
|
outage_types: Array.from(outageTypes),
|
|
340
358
|
average_duration_hours: enhancedOutages.length > 0
|
|
341
359
|
? Math.round(enhancedOutages
|
|
342
|
-
.filter((o) => o.duration_hours > 0)
|
|
343
|
-
.reduce((sum, o) => sum + o.duration_hours, 0) /
|
|
344
|
-
enhancedOutages.filter((o) => o.duration_hours > 0).length)
|
|
360
|
+
.filter((o) => o.duration_hours && o.duration_hours > 0)
|
|
361
|
+
.reduce((sum, o) => sum + (o.duration_hours || 0), 0) /
|
|
362
|
+
enhancedOutages.filter((o) => o.duration_hours && o.duration_hours > 0).length)
|
|
345
363
|
: 0,
|
|
346
364
|
outages: enhancedOutages,
|
|
347
365
|
};
|
|
@@ -363,27 +381,30 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
363
381
|
]);
|
|
364
382
|
const currentOutages = currentResponse.data.results || [];
|
|
365
383
|
const futureOutages = futureResponse.data.results || [];
|
|
366
|
-
const
|
|
384
|
+
const pastOutagesData = pastResponse.data.results || [];
|
|
367
385
|
// Filter recent past outages (last 30 days) for announcements
|
|
368
|
-
const recentPastOutages =
|
|
369
|
-
const endTime = new Date(outage.OutageEnd);
|
|
386
|
+
const recentPastOutages = pastOutagesData.filter((outage) => {
|
|
387
|
+
const endTime = new Date(outage.OutageEnd || "");
|
|
370
388
|
const daysAgo = (Date.now() - endTime.getTime()) / (1000 * 60 * 60 * 24);
|
|
371
389
|
return daysAgo <= 30;
|
|
372
390
|
});
|
|
373
391
|
// Combine all announcements and sort by most relevant date
|
|
374
392
|
const allAnnouncements = [
|
|
375
|
-
...currentOutages.map((item) => ({ ...item, category:
|
|
376
|
-
...futureOutages.map((item) => ({ ...item, category:
|
|
377
|
-
...recentPastOutages.map((item) => ({
|
|
393
|
+
...currentOutages.map((item) => ({ ...item, category: "current" })),
|
|
394
|
+
...futureOutages.map((item) => ({ ...item, category: "scheduled" })),
|
|
395
|
+
...recentPastOutages.map((item) => ({
|
|
396
|
+
...item,
|
|
397
|
+
category: "recent_past",
|
|
398
|
+
})),
|
|
378
399
|
]
|
|
379
400
|
.sort((a, b) => {
|
|
380
401
|
// Sort by most relevant date: current first, then future by start time, then past by end time
|
|
381
|
-
if (a.category ===
|
|
402
|
+
if (a.category === "current" && b.category !== "current")
|
|
382
403
|
return -1;
|
|
383
|
-
if (b.category ===
|
|
404
|
+
if (b.category === "current" && a.category !== "current")
|
|
384
405
|
return 1;
|
|
385
|
-
const dateA = new Date(a.OutageStart);
|
|
386
|
-
const dateB = new Date(b.OutageStart);
|
|
406
|
+
const dateA = new Date(a.OutageStart || "");
|
|
407
|
+
const dateB = new Date(b.OutageStart || "");
|
|
387
408
|
return dateB.getTime() - dateA.getTime(); // Most recent first
|
|
388
409
|
})
|
|
389
410
|
.slice(0, limit);
|
|
@@ -393,9 +414,9 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
393
414
|
scheduled_maintenance: futureOutages.length,
|
|
394
415
|
recent_past_outages: recentPastOutages.length,
|
|
395
416
|
categories: {
|
|
396
|
-
current: allAnnouncements.filter(a => a.category ===
|
|
397
|
-
scheduled: allAnnouncements.filter(a => a.category ===
|
|
398
|
-
recent_past: allAnnouncements.filter(a => a.category ===
|
|
417
|
+
current: allAnnouncements.filter((a) => a.category === "current").length,
|
|
418
|
+
scheduled: allAnnouncements.filter((a) => a.category === "scheduled").length,
|
|
419
|
+
recent_past: allAnnouncements.filter((a) => a.category === "recent_past").length,
|
|
399
420
|
},
|
|
400
421
|
announcements: allAnnouncements,
|
|
401
422
|
};
|
|
@@ -464,8 +485,7 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
464
485
|
checked_at: new Date().toISOString(),
|
|
465
486
|
resources_checked: resourceIds.length,
|
|
466
487
|
operational: resourceStatus.filter((r) => r.status === "operational").length,
|
|
467
|
-
affected: resourceStatus.filter((r) => r.status === "affected")
|
|
468
|
-
.length,
|
|
488
|
+
affected: resourceStatus.filter((r) => r.status === "affected").length,
|
|
469
489
|
api_method: "direct_outages_check",
|
|
470
490
|
resource_status: resourceStatus,
|
|
471
491
|
}, null, 2),
|
|
@@ -494,7 +514,7 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
494
514
|
api_method: "group_specific",
|
|
495
515
|
};
|
|
496
516
|
}
|
|
497
|
-
catch
|
|
517
|
+
catch {
|
|
498
518
|
// Fallback to general check if group API fails
|
|
499
519
|
return {
|
|
500
520
|
resource_id: resourceId,
|
|
@@ -516,10 +536,8 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
516
536
|
checked_at: new Date().toISOString(),
|
|
517
537
|
resources_checked: resourceIds.length,
|
|
518
538
|
operational: resourceStatus.filter((r) => r.status === "operational").length,
|
|
519
|
-
affected: resourceStatus.filter((r) => r.status === "affected")
|
|
520
|
-
|
|
521
|
-
unknown: resourceStatus.filter((r) => r.status === "unknown")
|
|
522
|
-
.length,
|
|
539
|
+
affected: resourceStatus.filter((r) => r.status === "affected").length,
|
|
540
|
+
unknown: resourceStatus.filter((r) => r.status === "unknown").length,
|
|
523
541
|
api_method: "resource_group_api",
|
|
524
542
|
resource_status: resourceStatus,
|
|
525
543
|
}, null, 2),
|
package/dist/web-server.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "path";
|
|
|
3
3
|
import { SystemStatusServer } from "./server.js";
|
|
4
4
|
export function startWebServer(port = 3000) {
|
|
5
5
|
const app = express();
|
|
6
|
-
|
|
6
|
+
new SystemStatusServer();
|
|
7
7
|
// Serve static files from public directory
|
|
8
8
|
const publicDir = path.join(__dirname, "../../../public");
|
|
9
9
|
app.use(express.static(publicDir));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@access-mcp/system-status",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "MCP server for ACCESS-CI System Status and Outages API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"node": ">=18.0.0"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@access-mcp/shared": "^0.
|
|
47
|
+
"@access-mcp/shared": "^0.6.0",
|
|
48
48
|
"express": "^4.18.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|