@access-mcp/announcements 0.1.0 → 0.3.0
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 +96 -200
- package/dist/index.js +2 -17
- package/dist/server.d.ts +73 -149
- package/dist/server.js +843 -161
- package/package.json +3 -3
- package/src/index.ts +2 -18
- package/src/server.integration.test.ts +119 -87
- package/src/server.test.ts +1032 -105
- package/src/server.ts +995 -178
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@access-mcp/announcements",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "MCP server for ACCESS Support Announcements API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"author": "ACCESS-CI",
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@access-mcp/shared": "
|
|
29
|
+
"@access-mcp/shared": "^0.5.0",
|
|
30
30
|
"@modelcontextprotocol/sdk": "^0.5.0",
|
|
31
31
|
"axios": "^1.7.0"
|
|
32
32
|
},
|
|
@@ -35,4 +35,4 @@
|
|
|
35
35
|
"typescript": "^5.0.0",
|
|
36
36
|
"vitest": "^1.0.0"
|
|
37
37
|
}
|
|
38
|
-
}
|
|
38
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,25 +3,9 @@
|
|
|
3
3
|
import { AnnouncementsServer } from "./server.js";
|
|
4
4
|
|
|
5
5
|
async function main() {
|
|
6
|
-
// Check if we should run as HTTP server (for deployment)
|
|
7
|
-
const port = process.env.PORT;
|
|
8
|
-
|
|
9
6
|
const server = new AnnouncementsServer();
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// Running in HTTP mode (deployment)
|
|
13
|
-
await server.start({ httpPort: parseInt(port) });
|
|
14
|
-
// Keep the process running in HTTP mode
|
|
15
|
-
process.on('SIGINT', () => {
|
|
16
|
-
console.log('Shutting down server...');
|
|
17
|
-
process.exit(0);
|
|
18
|
-
});
|
|
19
|
-
// Keep the event loop alive
|
|
20
|
-
setInterval(() => {}, 1000 * 60 * 60); // Heartbeat every hour
|
|
21
|
-
} else {
|
|
22
|
-
// Running in MCP mode (stdio)
|
|
23
|
-
await server.start();
|
|
24
|
-
}
|
|
7
|
+
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
|
|
8
|
+
await server.start(port ? { httpPort: port } : undefined);
|
|
25
9
|
}
|
|
26
10
|
|
|
27
11
|
main().catch((error) => {
|
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
2
|
import { AnnouncementsServer } from "./server.js";
|
|
3
3
|
|
|
4
|
+
interface TextContent {
|
|
5
|
+
type: "text";
|
|
6
|
+
text: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface AnnouncementItem {
|
|
10
|
+
title: string;
|
|
11
|
+
body: string;
|
|
12
|
+
published_date: string;
|
|
13
|
+
tags: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
4
16
|
/**
|
|
5
17
|
* Integration tests for AnnouncementsServer
|
|
6
18
|
* These tests make actual API calls to the ACCESS Support API
|
|
@@ -15,33 +27,31 @@ describe("AnnouncementsServer Integration Tests", () => {
|
|
|
15
27
|
describe("get_announcements", () => {
|
|
16
28
|
it("should fetch real announcements from API", async () => {
|
|
17
29
|
const result = await server["handleToolCall"]({
|
|
30
|
+
method: "tools/call",
|
|
18
31
|
params: {
|
|
19
|
-
name: "
|
|
32
|
+
name: "search_announcements",
|
|
20
33
|
arguments: {
|
|
21
34
|
limit: 5,
|
|
22
35
|
},
|
|
23
36
|
},
|
|
24
|
-
}
|
|
37
|
+
});
|
|
25
38
|
|
|
26
39
|
expect(result.isError).toBeFalsy();
|
|
27
|
-
const responseData = JSON.parse(result.content[0].text);
|
|
40
|
+
const responseData = JSON.parse((result.content[0] as TextContent).text);
|
|
28
41
|
|
|
29
42
|
// Check structure
|
|
30
|
-
expect(responseData).toHaveProperty("
|
|
31
|
-
expect(responseData).toHaveProperty("
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// Announcements should be an array
|
|
36
|
-
expect(Array.isArray(responseData.announcements)).toBe(true);
|
|
43
|
+
expect(responseData).toHaveProperty("total");
|
|
44
|
+
expect(responseData).toHaveProperty("items");
|
|
45
|
+
|
|
46
|
+
// Items should be an array
|
|
47
|
+
expect(Array.isArray(responseData.items)).toBe(true);
|
|
37
48
|
|
|
38
49
|
// If there are announcements, check their structure
|
|
39
|
-
if (responseData.
|
|
40
|
-
const firstAnnouncement = responseData.
|
|
50
|
+
if (responseData.items.length > 0) {
|
|
51
|
+
const firstAnnouncement = responseData.items[0];
|
|
41
52
|
expect(firstAnnouncement).toHaveProperty("title");
|
|
42
53
|
expect(firstAnnouncement).toHaveProperty("body");
|
|
43
|
-
expect(firstAnnouncement).toHaveProperty("
|
|
44
|
-
expect(firstAnnouncement).toHaveProperty("formatted_date");
|
|
54
|
+
expect(firstAnnouncement).toHaveProperty("published_date");
|
|
45
55
|
expect(firstAnnouncement).toHaveProperty("tags");
|
|
46
56
|
expect(Array.isArray(firstAnnouncement.tags)).toBe(true);
|
|
47
57
|
}
|
|
@@ -49,25 +59,26 @@ describe("AnnouncementsServer Integration Tests", () => {
|
|
|
49
59
|
|
|
50
60
|
it("should filter by tags", async () => {
|
|
51
61
|
const result = await server["handleToolCall"]({
|
|
62
|
+
method: "tools/call",
|
|
52
63
|
params: {
|
|
53
|
-
name: "
|
|
64
|
+
name: "search_announcements",
|
|
54
65
|
arguments: {
|
|
55
66
|
tags: "maintenance",
|
|
56
67
|
limit: 3,
|
|
57
68
|
},
|
|
58
69
|
},
|
|
59
|
-
}
|
|
70
|
+
});
|
|
60
71
|
|
|
61
72
|
expect(result.isError).toBeFalsy();
|
|
62
|
-
const responseData = JSON.parse(result.content[0].text);
|
|
63
|
-
|
|
64
|
-
expect(responseData).toHaveProperty("
|
|
65
|
-
expect(Array.isArray(responseData.
|
|
66
|
-
|
|
73
|
+
const responseData = JSON.parse((result.content[0] as TextContent).text);
|
|
74
|
+
|
|
75
|
+
expect(responseData).toHaveProperty("items");
|
|
76
|
+
expect(Array.isArray(responseData.items)).toBe(true);
|
|
77
|
+
|
|
67
78
|
// If maintenance announcements exist, they should contain the tag
|
|
68
|
-
if (responseData.
|
|
69
|
-
const hasMaintenanceTag = responseData.
|
|
70
|
-
ann.tags && ann.tags.some((tag: string) =>
|
|
79
|
+
if (responseData.items.length > 0) {
|
|
80
|
+
const hasMaintenanceTag = responseData.items.some((ann: AnnouncementItem) =>
|
|
81
|
+
ann.tags && ann.tags.some((tag: string) =>
|
|
71
82
|
tag.toLowerCase().includes("maintenance")
|
|
72
83
|
)
|
|
73
84
|
);
|
|
@@ -78,29 +89,28 @@ describe("AnnouncementsServer Integration Tests", () => {
|
|
|
78
89
|
|
|
79
90
|
it("should handle date range filters", async () => {
|
|
80
91
|
const result = await server["handleToolCall"]({
|
|
92
|
+
method: "tools/call",
|
|
81
93
|
params: {
|
|
82
|
-
name: "
|
|
94
|
+
name: "search_announcements",
|
|
83
95
|
arguments: {
|
|
84
|
-
|
|
85
|
-
relative_end_date: "today",
|
|
96
|
+
date: "this_month",
|
|
86
97
|
limit: 10,
|
|
87
98
|
},
|
|
88
99
|
},
|
|
89
|
-
}
|
|
100
|
+
});
|
|
90
101
|
|
|
91
102
|
expect(result.isError).toBeFalsy();
|
|
92
|
-
const responseData = JSON.parse(result.content[0].text);
|
|
93
|
-
|
|
94
|
-
expect(responseData).toHaveProperty("
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
const responseData = JSON.parse((result.content[0] as TextContent).text);
|
|
104
|
+
|
|
105
|
+
expect(responseData).toHaveProperty("items");
|
|
106
|
+
|
|
97
107
|
// All announcements should be within the last month
|
|
98
|
-
if (responseData.
|
|
108
|
+
if (responseData.items.length > 0) {
|
|
99
109
|
const oneMonthAgo = new Date();
|
|
100
110
|
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
|
|
101
|
-
|
|
102
|
-
responseData.
|
|
103
|
-
const annDate = new Date(ann.
|
|
111
|
+
|
|
112
|
+
responseData.items.forEach((ann: AnnouncementItem) => {
|
|
113
|
+
const annDate = new Date(ann.published_date);
|
|
104
114
|
expect(annDate.getTime()).toBeGreaterThanOrEqual(oneMonthAgo.getTime());
|
|
105
115
|
});
|
|
106
116
|
}
|
|
@@ -110,27 +120,27 @@ describe("AnnouncementsServer Integration Tests", () => {
|
|
|
110
120
|
describe("get_recent_announcements", () => {
|
|
111
121
|
it("should fetch announcements from the past week", async () => {
|
|
112
122
|
const result = await server["handleToolCall"]({
|
|
123
|
+
method: "tools/call",
|
|
113
124
|
params: {
|
|
114
|
-
name: "
|
|
125
|
+
name: "search_announcements",
|
|
115
126
|
arguments: {
|
|
116
|
-
|
|
127
|
+
date: "this_week",
|
|
117
128
|
},
|
|
118
129
|
},
|
|
119
|
-
}
|
|
130
|
+
});
|
|
120
131
|
|
|
121
132
|
expect(result.isError).toBeFalsy();
|
|
122
|
-
const responseData = JSON.parse(result.content[0].text);
|
|
123
|
-
|
|
124
|
-
expect(responseData).toHaveProperty("
|
|
125
|
-
|
|
126
|
-
|
|
133
|
+
const responseData = JSON.parse((result.content[0] as TextContent).text);
|
|
134
|
+
|
|
135
|
+
expect(responseData).toHaveProperty("items");
|
|
136
|
+
|
|
127
137
|
// Check that announcements are from the past week if any exist
|
|
128
|
-
if (responseData.
|
|
138
|
+
if (responseData.items.length > 0) {
|
|
129
139
|
const oneWeekAgo = new Date();
|
|
130
140
|
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
|
131
|
-
|
|
132
|
-
responseData.
|
|
133
|
-
const annDate = new Date(ann.
|
|
141
|
+
|
|
142
|
+
responseData.items.forEach((ann: AnnouncementItem) => {
|
|
143
|
+
const annDate = new Date(ann.published_date);
|
|
134
144
|
expect(annDate.getTime()).toBeGreaterThanOrEqual(oneWeekAgo.getTime());
|
|
135
145
|
});
|
|
136
146
|
}
|
|
@@ -138,83 +148,105 @@ describe("AnnouncementsServer Integration Tests", () => {
|
|
|
138
148
|
|
|
139
149
|
it("should handle today filter", async () => {
|
|
140
150
|
const result = await server["handleToolCall"]({
|
|
151
|
+
method: "tools/call",
|
|
141
152
|
params: {
|
|
142
|
-
name: "
|
|
153
|
+
name: "search_announcements",
|
|
143
154
|
arguments: {
|
|
144
|
-
|
|
155
|
+
date: "today",
|
|
145
156
|
},
|
|
146
157
|
},
|
|
147
|
-
}
|
|
158
|
+
});
|
|
148
159
|
|
|
149
160
|
expect(result.isError).toBeFalsy();
|
|
150
|
-
const responseData = JSON.parse(result.content[0].text);
|
|
151
|
-
|
|
152
|
-
expect(responseData).toHaveProperty("
|
|
153
|
-
|
|
154
|
-
|
|
161
|
+
const responseData = JSON.parse((result.content[0] as TextContent).text);
|
|
162
|
+
|
|
163
|
+
expect(responseData).toHaveProperty("items");
|
|
164
|
+
|
|
155
165
|
// Today's announcements might be empty, that's okay
|
|
156
|
-
expect(Array.isArray(responseData.
|
|
166
|
+
expect(Array.isArray(responseData.items)).toBe(true);
|
|
157
167
|
}, 10000);
|
|
158
168
|
});
|
|
159
169
|
|
|
160
|
-
describe("
|
|
161
|
-
it("should
|
|
162
|
-
// We don't know specific affinity group IDs, so test with a made-up one
|
|
170
|
+
describe("get_announcements with limit", () => {
|
|
171
|
+
it("should respect limit parameter", async () => {
|
|
163
172
|
const result = await server["handleToolCall"]({
|
|
173
|
+
method: "tools/call",
|
|
164
174
|
params: {
|
|
165
|
-
name: "
|
|
175
|
+
name: "search_announcements",
|
|
166
176
|
arguments: {
|
|
167
|
-
ag: "test-group-123",
|
|
168
177
|
limit: 5,
|
|
169
178
|
},
|
|
170
179
|
},
|
|
171
|
-
}
|
|
180
|
+
});
|
|
172
181
|
|
|
173
182
|
expect(result.isError).toBeFalsy();
|
|
174
|
-
const responseData = JSON.parse(result.content[0].text);
|
|
175
|
-
|
|
176
|
-
// Should return a valid response
|
|
177
|
-
expect(responseData).toHaveProperty("
|
|
178
|
-
expect(Array.isArray(responseData.
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
const responseData = JSON.parse((result.content[0] as TextContent).text);
|
|
184
|
+
|
|
185
|
+
// Should return a valid response
|
|
186
|
+
expect(responseData).toHaveProperty("items");
|
|
187
|
+
expect(Array.isArray(responseData.items)).toBe(true);
|
|
188
|
+
if (responseData.items.length > 0) {
|
|
189
|
+
expect(responseData.items.length).toBeLessThanOrEqual(5);
|
|
190
|
+
}
|
|
181
191
|
}, 10000);
|
|
182
192
|
});
|
|
183
193
|
|
|
184
|
-
describe("
|
|
185
|
-
it("should
|
|
194
|
+
describe("search with query parameter", () => {
|
|
195
|
+
it("should perform full-text search", async () => {
|
|
186
196
|
const result = await server["handleToolCall"]({
|
|
197
|
+
method: "tools/call",
|
|
187
198
|
params: {
|
|
188
|
-
name: "
|
|
199
|
+
name: "search_announcements",
|
|
189
200
|
arguments: {
|
|
190
|
-
|
|
201
|
+
query: "ACCESS",
|
|
202
|
+
limit: 5,
|
|
191
203
|
},
|
|
192
204
|
},
|
|
193
|
-
}
|
|
205
|
+
});
|
|
194
206
|
|
|
195
|
-
|
|
196
|
-
|
|
207
|
+
expect(result.isError).toBeFalsy();
|
|
208
|
+
const responseData = JSON.parse((result.content[0] as TextContent).text);
|
|
209
|
+
|
|
210
|
+
expect(responseData).toHaveProperty("items");
|
|
211
|
+
expect(Array.isArray(responseData.items)).toBe(true);
|
|
212
|
+
}, 10000);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("API Error Handling", () => {
|
|
216
|
+
it("should handle search with no parameters", async () => {
|
|
217
|
+
const result = await server["handleToolCall"]({
|
|
218
|
+
method: "tools/call",
|
|
219
|
+
params: {
|
|
220
|
+
name: "search_announcements",
|
|
221
|
+
arguments: {},
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Should return default results
|
|
197
226
|
expect(result).toBeDefined();
|
|
198
227
|
expect(result.content).toBeDefined();
|
|
228
|
+
const responseData = JSON.parse((result.content[0] as TextContent).text);
|
|
229
|
+
expect(responseData).toHaveProperty("items");
|
|
199
230
|
}, 10000);
|
|
200
231
|
|
|
201
232
|
it("should handle empty results", async () => {
|
|
202
233
|
const result = await server["handleToolCall"]({
|
|
234
|
+
method: "tools/call",
|
|
203
235
|
params: {
|
|
204
|
-
name: "
|
|
236
|
+
name: "search_announcements",
|
|
205
237
|
arguments: {
|
|
206
238
|
tags: "nonexistent-tag-xyz-123-456",
|
|
207
|
-
exact_match: true,
|
|
208
239
|
},
|
|
209
240
|
},
|
|
210
|
-
}
|
|
241
|
+
});
|
|
211
242
|
|
|
212
243
|
expect(result.isError).toBeFalsy();
|
|
213
|
-
const responseData = JSON.parse(result.content[0].text);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
expect(responseData
|
|
217
|
-
expect(responseData
|
|
244
|
+
const responseData = JSON.parse((result.content[0] as TextContent).text);
|
|
245
|
+
|
|
246
|
+
// May have 0 or few results
|
|
247
|
+
expect(responseData).toHaveProperty("total");
|
|
248
|
+
expect(responseData).toHaveProperty("items");
|
|
249
|
+
expect(Array.isArray(responseData.items)).toBe(true);
|
|
218
250
|
}, 10000);
|
|
219
251
|
});
|
|
220
252
|
});
|