@esaio/esa-mcp-server 0.1.0 → 0.2.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/.github/dependabot.yml +5 -0
- package/.github/workflows/docker-publish.yml +4 -4
- package/.github/workflows/main.yml +4 -4
- package/README.en.md +17 -5
- package/README.md +20 -8
- package/bin/index.js +6 -6
- package/biome.json +1 -1
- package/package.json +9 -7
- package/src/__tests__/index.test.ts +9 -2
- package/src/api_client/__tests__/middleware.test.ts +2 -1
- package/src/generated/api-types.ts +298 -21
- package/src/prompts/__tests__/index.test.ts +2 -1
- package/src/prompts/index.ts +1 -1
- package/src/resources/__tests__/index.test.ts +2 -1
- package/src/resources/__tests__/recent-posts-list.test.ts +2 -1
- package/src/tools/__tests__/attachments.test.ts +460 -0
- package/src/tools/__tests__/categories.test.ts +177 -1
- package/src/tools/__tests__/index.test.ts +5 -4
- package/src/tools/attachments.ts +167 -0
- package/src/tools/categories.ts +60 -0
- package/src/tools/index.ts +27 -0
- package/.claude/settings.local.json +0 -23
- package/.envrc +0 -2
- package/.node-version +0 -1
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { createEsaClient } from "../../api_client/index.js";
|
|
3
|
+
import { getAttachment } from "../attachments.js";
|
|
4
|
+
|
|
5
|
+
// Mock global fetch
|
|
6
|
+
const mockFetch = vi.fn();
|
|
7
|
+
global.fetch = mockFetch;
|
|
8
|
+
|
|
9
|
+
describe("getAttachment", () => {
|
|
10
|
+
const mockClient = {
|
|
11
|
+
GET: vi.fn(),
|
|
12
|
+
} as unknown as ReturnType<typeof createEsaClient> & {
|
|
13
|
+
GET: ReturnType<typeof vi.fn>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should return base64-encoded image for small images", async () => {
|
|
21
|
+
const signedUrl = "https://s3.amazonaws.com/bucket/image.png?signature=123";
|
|
22
|
+
const imageData = "fake-image-data";
|
|
23
|
+
const imageBuffer = Buffer.from(imageData);
|
|
24
|
+
|
|
25
|
+
// Mock API response with signed URL
|
|
26
|
+
mockClient.GET.mockResolvedValue({
|
|
27
|
+
data: {
|
|
28
|
+
signed_urls: [["/uploads/example/image.png", signedUrl]],
|
|
29
|
+
},
|
|
30
|
+
error: undefined,
|
|
31
|
+
response: {
|
|
32
|
+
ok: true,
|
|
33
|
+
status: 200,
|
|
34
|
+
} as Response,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Mock fetch response for image
|
|
38
|
+
mockFetch.mockResolvedValue({
|
|
39
|
+
ok: true,
|
|
40
|
+
headers: {
|
|
41
|
+
get: (name: string) => {
|
|
42
|
+
if (name === "content-type") return "image/png";
|
|
43
|
+
if (name === "content-length") return String(imageBuffer.length);
|
|
44
|
+
return null;
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
arrayBuffer: async () => {
|
|
48
|
+
const buffer = Buffer.from(imageData);
|
|
49
|
+
return buffer.buffer.slice(
|
|
50
|
+
buffer.byteOffset,
|
|
51
|
+
buffer.byteOffset + buffer.byteLength,
|
|
52
|
+
);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const result = await getAttachment(mockClient, {
|
|
57
|
+
teamName: "test-team",
|
|
58
|
+
url: "https://dl.esa.io/uploads/example/image.png",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(mockClient.GET).toHaveBeenCalledWith(
|
|
62
|
+
"/v1/teams/{team_name}/signed_urls",
|
|
63
|
+
{
|
|
64
|
+
params: {
|
|
65
|
+
path: { team_name: "test-team" },
|
|
66
|
+
query: {
|
|
67
|
+
urls: "/uploads/example/image.png",
|
|
68
|
+
v: 2,
|
|
69
|
+
expires_in: 300,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(mockFetch).toHaveBeenCalledWith(signedUrl);
|
|
76
|
+
|
|
77
|
+
expect(result).toEqual({
|
|
78
|
+
content: [
|
|
79
|
+
{
|
|
80
|
+
type: "image",
|
|
81
|
+
data: imageBuffer.toString("base64"),
|
|
82
|
+
mimeType: "image/png",
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should return signed URL for large images (over 30MB)", async () => {
|
|
89
|
+
const signedUrl = "https://s3.amazonaws.com/bucket/large.png?signature=123";
|
|
90
|
+
const largeSize = 35 * 1024 * 1024; // 35MB
|
|
91
|
+
|
|
92
|
+
mockClient.GET.mockResolvedValue({
|
|
93
|
+
data: {
|
|
94
|
+
signed_urls: [["/uploads/example/large.png", signedUrl]],
|
|
95
|
+
},
|
|
96
|
+
error: undefined,
|
|
97
|
+
response: {
|
|
98
|
+
ok: true,
|
|
99
|
+
status: 200,
|
|
100
|
+
} as Response,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
mockFetch.mockResolvedValue({
|
|
104
|
+
ok: true,
|
|
105
|
+
headers: {
|
|
106
|
+
get: (name: string) => {
|
|
107
|
+
if (name === "content-type") return "image/png";
|
|
108
|
+
if (name === "content-length") return String(largeSize);
|
|
109
|
+
return null;
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const result = await getAttachment(mockClient, {
|
|
115
|
+
teamName: "test-team",
|
|
116
|
+
url: "/uploads/example/large.png",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(result).toEqual({
|
|
120
|
+
content: [
|
|
121
|
+
{
|
|
122
|
+
type: "text",
|
|
123
|
+
text: signedUrl,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should return signed URL for non-image files", async () => {
|
|
130
|
+
const signedUrl = "https://s3.amazonaws.com/bucket/doc.pdf?signature=123";
|
|
131
|
+
|
|
132
|
+
mockClient.GET.mockResolvedValue({
|
|
133
|
+
data: {
|
|
134
|
+
signed_urls: [["/uploads/example/doc.pdf", signedUrl]],
|
|
135
|
+
},
|
|
136
|
+
error: undefined,
|
|
137
|
+
response: {
|
|
138
|
+
ok: true,
|
|
139
|
+
status: 200,
|
|
140
|
+
} as Response,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
mockFetch.mockResolvedValue({
|
|
144
|
+
ok: true,
|
|
145
|
+
headers: {
|
|
146
|
+
get: (name: string) => {
|
|
147
|
+
if (name === "content-type") return "application/pdf";
|
|
148
|
+
if (name === "content-length") return "1024";
|
|
149
|
+
return null;
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const result = await getAttachment(mockClient, {
|
|
155
|
+
teamName: "test-team",
|
|
156
|
+
url: "https://files.esa.io/uploads/example/doc.pdf",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(result).toEqual({
|
|
160
|
+
content: [
|
|
161
|
+
{
|
|
162
|
+
type: "text",
|
|
163
|
+
text: signedUrl,
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should handle file not found (null signed URL)", async () => {
|
|
170
|
+
mockClient.GET.mockResolvedValue({
|
|
171
|
+
data: {
|
|
172
|
+
signed_urls: [["/uploads/example/missing.png", null]],
|
|
173
|
+
},
|
|
174
|
+
error: undefined,
|
|
175
|
+
response: {
|
|
176
|
+
ok: true,
|
|
177
|
+
status: 200,
|
|
178
|
+
} as Response,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const result = await getAttachment(mockClient, {
|
|
182
|
+
teamName: "test-team",
|
|
183
|
+
url: "/uploads/example/missing.png",
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(result).toEqual({
|
|
187
|
+
content: [
|
|
188
|
+
{
|
|
189
|
+
type: "text",
|
|
190
|
+
text: "Error: File not found: /uploads/example/missing.png",
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should handle fetch failure", async () => {
|
|
197
|
+
const signedUrl = "https://s3.amazonaws.com/bucket/image.png?signature=123";
|
|
198
|
+
|
|
199
|
+
mockClient.GET.mockResolvedValue({
|
|
200
|
+
data: {
|
|
201
|
+
signed_urls: [["/uploads/example/image.png", signedUrl]],
|
|
202
|
+
},
|
|
203
|
+
error: undefined,
|
|
204
|
+
response: {
|
|
205
|
+
ok: true,
|
|
206
|
+
status: 200,
|
|
207
|
+
} as Response,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
mockFetch.mockResolvedValue({
|
|
211
|
+
ok: false,
|
|
212
|
+
status: 403,
|
|
213
|
+
statusText: "Forbidden",
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const result = await getAttachment(mockClient, {
|
|
217
|
+
teamName: "test-team",
|
|
218
|
+
url: "/uploads/example/image.png",
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
expect(result).toEqual({
|
|
222
|
+
content: [
|
|
223
|
+
{
|
|
224
|
+
type: "text",
|
|
225
|
+
text: "Error: Failed to fetch attachment for /uploads/example/image.png: Failed to fetch attachment: 403 Forbidden",
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should handle API errors", async () => {
|
|
232
|
+
const mockError = { error: "not_found", message: "Team not found" };
|
|
233
|
+
|
|
234
|
+
mockClient.GET.mockResolvedValue({
|
|
235
|
+
data: undefined,
|
|
236
|
+
error: mockError,
|
|
237
|
+
response: {
|
|
238
|
+
ok: false,
|
|
239
|
+
status: 404,
|
|
240
|
+
} as Response,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const result = await getAttachment(mockClient, {
|
|
244
|
+
teamName: "test-team",
|
|
245
|
+
url: "/uploads/example/image.png",
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
expect(result).toEqual({
|
|
249
|
+
content: [
|
|
250
|
+
{
|
|
251
|
+
type: "text",
|
|
252
|
+
text: `Error: ${JSON.stringify(mockError, null, 2)}`,
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("should handle missing teamName", async () => {
|
|
259
|
+
const result = await getAttachment(mockClient, {
|
|
260
|
+
teamName: "",
|
|
261
|
+
url: "/uploads/example/image.png",
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
expect(result).toEqual({
|
|
265
|
+
content: [
|
|
266
|
+
{
|
|
267
|
+
type: "text",
|
|
268
|
+
text: "Error: Missing required parameter 'teamName'. Use esa_get_teams to list available teams, then retry with teamName specified.",
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(mockClient.GET).not.toHaveBeenCalled();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("should support various image formats (jpeg, png, gif, webp)", async () => {
|
|
277
|
+
const testCases = [
|
|
278
|
+
{ mimeType: "image/jpeg", url: "/image.jpg" },
|
|
279
|
+
{ mimeType: "image/png", url: "/image.png" },
|
|
280
|
+
{ mimeType: "image/gif", url: "/image.gif" },
|
|
281
|
+
{ mimeType: "image/webp", url: "/image.webp" },
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
for (const { mimeType, url } of testCases) {
|
|
285
|
+
vi.clearAllMocks();
|
|
286
|
+
|
|
287
|
+
const signedUrl = `https://s3.amazonaws.com/bucket${url}?signature=123`;
|
|
288
|
+
const imageData = "test-image";
|
|
289
|
+
const imageBuffer = Buffer.from(imageData);
|
|
290
|
+
|
|
291
|
+
mockClient.GET.mockResolvedValue({
|
|
292
|
+
data: {
|
|
293
|
+
signed_urls: [[url, signedUrl]],
|
|
294
|
+
},
|
|
295
|
+
error: undefined,
|
|
296
|
+
response: {
|
|
297
|
+
ok: true,
|
|
298
|
+
status: 200,
|
|
299
|
+
} as Response,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
mockFetch.mockResolvedValue({
|
|
303
|
+
ok: true,
|
|
304
|
+
headers: {
|
|
305
|
+
get: (name: string) => {
|
|
306
|
+
if (name === "content-type") return mimeType;
|
|
307
|
+
if (name === "content-length") return String(imageBuffer.length);
|
|
308
|
+
return null;
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
arrayBuffer: async () => {
|
|
312
|
+
const buffer = Buffer.from(imageData);
|
|
313
|
+
return buffer.buffer.slice(
|
|
314
|
+
buffer.byteOffset,
|
|
315
|
+
buffer.byteOffset + buffer.byteLength,
|
|
316
|
+
);
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const result = await getAttachment(mockClient, {
|
|
321
|
+
teamName: "test-team",
|
|
322
|
+
url,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
expect(result).toEqual({
|
|
326
|
+
content: [
|
|
327
|
+
{
|
|
328
|
+
type: "image",
|
|
329
|
+
data: imageBuffer.toString("base64"),
|
|
330
|
+
mimeType,
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("should return signed URL when forceSignedUrl is true", async () => {
|
|
338
|
+
const signedUrl = "https://s3.amazonaws.com/bucket/image.png?signature=123";
|
|
339
|
+
|
|
340
|
+
mockClient.GET.mockResolvedValue({
|
|
341
|
+
data: {
|
|
342
|
+
signed_urls: [["/uploads/example/image.png", signedUrl]],
|
|
343
|
+
},
|
|
344
|
+
error: undefined,
|
|
345
|
+
response: {
|
|
346
|
+
ok: true,
|
|
347
|
+
status: 200,
|
|
348
|
+
} as Response,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const result = await getAttachment(mockClient, {
|
|
352
|
+
teamName: "test-team",
|
|
353
|
+
url: "/uploads/example/image.png",
|
|
354
|
+
forceSignedUrl: true,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Should not call fetch when forceSignedUrl is true
|
|
358
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
359
|
+
|
|
360
|
+
expect(result).toEqual({
|
|
361
|
+
content: [
|
|
362
|
+
{
|
|
363
|
+
type: "text",
|
|
364
|
+
text: signedUrl,
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("should return base64 for images under 30MB", async () => {
|
|
371
|
+
const signedUrl = "https://s3.amazonaws.com/bucket/image.png?signature=123";
|
|
372
|
+
const imageData = "a".repeat(25 * 1024 * 1024); // 25MB
|
|
373
|
+
const imageBuffer = Buffer.from(imageData);
|
|
374
|
+
|
|
375
|
+
mockClient.GET.mockResolvedValue({
|
|
376
|
+
data: {
|
|
377
|
+
signed_urls: [["/uploads/example/image.png", signedUrl]],
|
|
378
|
+
},
|
|
379
|
+
error: undefined,
|
|
380
|
+
response: {
|
|
381
|
+
ok: true,
|
|
382
|
+
status: 200,
|
|
383
|
+
} as Response,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
mockFetch.mockResolvedValue({
|
|
387
|
+
ok: true,
|
|
388
|
+
headers: {
|
|
389
|
+
get: (name: string) => {
|
|
390
|
+
if (name === "content-type") return "image/png";
|
|
391
|
+
if (name === "content-length") return String(imageBuffer.length);
|
|
392
|
+
return null;
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
arrayBuffer: async () => {
|
|
396
|
+
const buffer = Buffer.from(imageData);
|
|
397
|
+
return buffer.buffer.slice(
|
|
398
|
+
buffer.byteOffset,
|
|
399
|
+
buffer.byteOffset + buffer.byteLength,
|
|
400
|
+
);
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const result = await getAttachment(mockClient, {
|
|
405
|
+
teamName: "test-team",
|
|
406
|
+
url: "/uploads/example/image.png",
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
expect(result).toEqual({
|
|
410
|
+
content: [
|
|
411
|
+
{
|
|
412
|
+
type: "image",
|
|
413
|
+
data: imageBuffer.toString("base64"),
|
|
414
|
+
mimeType: "image/png",
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("should return signed URL for unsupported image formats", async () => {
|
|
421
|
+
const signedUrl = "https://s3.amazonaws.com/bucket/image.svg?signature=123";
|
|
422
|
+
const imageBuffer = Buffer.from("svg-data");
|
|
423
|
+
|
|
424
|
+
mockClient.GET.mockResolvedValue({
|
|
425
|
+
data: {
|
|
426
|
+
signed_urls: [["/uploads/example/image.svg", signedUrl]],
|
|
427
|
+
},
|
|
428
|
+
error: undefined,
|
|
429
|
+
response: {
|
|
430
|
+
ok: true,
|
|
431
|
+
status: 200,
|
|
432
|
+
} as Response,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
mockFetch.mockResolvedValue({
|
|
436
|
+
ok: true,
|
|
437
|
+
headers: {
|
|
438
|
+
get: (name: string) => {
|
|
439
|
+
if (name === "content-type") return "image/svg+xml";
|
|
440
|
+
if (name === "content-length") return String(imageBuffer.length);
|
|
441
|
+
return null;
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const result = await getAttachment(mockClient, {
|
|
447
|
+
teamName: "test-team",
|
|
448
|
+
url: "/uploads/example/image.svg",
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
expect(result).toEqual({
|
|
452
|
+
content: [
|
|
453
|
+
{
|
|
454
|
+
type: "text",
|
|
455
|
+
text: signedUrl,
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
});
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import type { createEsaClient } from "../../api_client/index.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getAllCategoryPaths,
|
|
5
|
+
getCategories,
|
|
6
|
+
getTopCategories,
|
|
7
|
+
} from "../categories.js";
|
|
4
8
|
|
|
5
9
|
describe("getCategories", () => {
|
|
6
10
|
const mockClient = {
|
|
@@ -224,3 +228,175 @@ describe("getTopCategories", () => {
|
|
|
224
228
|
expect(mockClient.GET).not.toHaveBeenCalled();
|
|
225
229
|
});
|
|
226
230
|
});
|
|
231
|
+
|
|
232
|
+
describe("getAllCategoryPaths", () => {
|
|
233
|
+
const mockClient = {
|
|
234
|
+
GET: vi.fn(),
|
|
235
|
+
} as unknown as ReturnType<typeof createEsaClient> & {
|
|
236
|
+
GET: ReturnType<typeof vi.fn>;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
beforeEach(() => {
|
|
240
|
+
vi.clearAllMocks();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should fetch all category paths", async () => {
|
|
244
|
+
const mockResponse = [
|
|
245
|
+
{ path: null, posts: 5 },
|
|
246
|
+
{ path: "dev", posts: 10 },
|
|
247
|
+
{ path: "dev/docs", posts: 3 },
|
|
248
|
+
{ path: "design", posts: 7 },
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
mockClient.GET.mockResolvedValue({
|
|
252
|
+
data: mockResponse,
|
|
253
|
+
error: undefined,
|
|
254
|
+
response: { ok: true, status: 200 } as Response,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const result = await getAllCategoryPaths(mockClient, {
|
|
258
|
+
teamName: "test-team",
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
expect(mockClient.GET).toHaveBeenCalledWith(
|
|
262
|
+
"/v1/teams/{team_name}/categories/paths",
|
|
263
|
+
{
|
|
264
|
+
params: {
|
|
265
|
+
path: { team_name: "test-team" },
|
|
266
|
+
query: {
|
|
267
|
+
prefix: undefined,
|
|
268
|
+
suffix: undefined,
|
|
269
|
+
match: undefined,
|
|
270
|
+
exact_match: undefined,
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
expect(result.content[0].text).toContain("dev");
|
|
277
|
+
expect(result.content[0].text).toContain("design");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("should support prefix filter", async () => {
|
|
281
|
+
const mockResponse = [
|
|
282
|
+
{ path: "dev", posts: 10 },
|
|
283
|
+
{ path: "dev/docs", posts: 3 },
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
mockClient.GET.mockResolvedValue({
|
|
287
|
+
data: mockResponse,
|
|
288
|
+
error: undefined,
|
|
289
|
+
response: { ok: true, status: 200 } as Response,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const result = await getAllCategoryPaths(mockClient, {
|
|
293
|
+
teamName: "test-team",
|
|
294
|
+
prefix: "dev",
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
expect(mockClient.GET).toHaveBeenCalledWith(
|
|
298
|
+
"/v1/teams/{team_name}/categories/paths",
|
|
299
|
+
{
|
|
300
|
+
params: {
|
|
301
|
+
path: { team_name: "test-team" },
|
|
302
|
+
query: {
|
|
303
|
+
prefix: "dev",
|
|
304
|
+
suffix: undefined,
|
|
305
|
+
match: undefined,
|
|
306
|
+
exact_match: undefined,
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
expect(result.content[0].text).toContain("dev");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("should support multiple filters", async () => {
|
|
316
|
+
const mockResponse = [{ path: "dev/docs", posts: 3 }];
|
|
317
|
+
|
|
318
|
+
mockClient.GET.mockResolvedValue({
|
|
319
|
+
data: mockResponse,
|
|
320
|
+
error: undefined,
|
|
321
|
+
response: { ok: true, status: 200 } as Response,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const result = await getAllCategoryPaths(mockClient, {
|
|
325
|
+
teamName: "test-team",
|
|
326
|
+
prefix: "dev",
|
|
327
|
+
suffix: "docs",
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
expect(mockClient.GET).toHaveBeenCalledWith(
|
|
331
|
+
"/v1/teams/{team_name}/categories/paths",
|
|
332
|
+
{
|
|
333
|
+
params: {
|
|
334
|
+
path: { team_name: "test-team" },
|
|
335
|
+
query: {
|
|
336
|
+
prefix: "dev",
|
|
337
|
+
suffix: "docs",
|
|
338
|
+
match: undefined,
|
|
339
|
+
exact_match: undefined,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
expect(result.content[0].text).toContain("dev/docs");
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("should handle API errors", async () => {
|
|
349
|
+
const mockError = { error: "forbidden", message: "Access denied" };
|
|
350
|
+
|
|
351
|
+
mockClient.GET.mockResolvedValue({
|
|
352
|
+
data: undefined,
|
|
353
|
+
error: mockError,
|
|
354
|
+
response: { ok: false, status: 403 } as Response,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const result = await getAllCategoryPaths(mockClient, {
|
|
358
|
+
teamName: "test-team",
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
expect(result.content[0].text).toContain("forbidden");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("should handle network errors", async () => {
|
|
365
|
+
const networkError = new Error("Network connection failed");
|
|
366
|
+
|
|
367
|
+
mockClient.GET.mockRejectedValue(networkError);
|
|
368
|
+
|
|
369
|
+
const result = await getAllCategoryPaths(mockClient, {
|
|
370
|
+
teamName: "test-team",
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
expect(result.content[0].text).toContain("Network connection failed");
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("should handle non-Error exceptions", async () => {
|
|
377
|
+
mockClient.GET.mockRejectedValue("Unexpected error");
|
|
378
|
+
|
|
379
|
+
const result = await getAllCategoryPaths(mockClient, {
|
|
380
|
+
teamName: "test-team",
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
expect(result.content[0].text).toContain("Unexpected error");
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("should throw MissingTeamNameError when teamName is empty", async () => {
|
|
387
|
+
const result = await getAllCategoryPaths(mockClient, {
|
|
388
|
+
teamName: "",
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
expect(result).toEqual({
|
|
392
|
+
content: [
|
|
393
|
+
{
|
|
394
|
+
type: "text",
|
|
395
|
+
text: "Error: Missing required parameter 'teamName'. Use esa_get_teams to list available teams, then retry with teamName specified.",
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
expect(mockClient.GET).not.toHaveBeenCalled();
|
|
401
|
+
});
|
|
402
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { MockInstance } from "vitest";
|
|
2
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
4
|
import type { MCPContext } from "../../context/mcp-context.js";
|
|
4
5
|
import { setupTools } from "../index.js";
|
|
@@ -6,7 +7,7 @@ import { setupTools } from "../index.js";
|
|
|
6
7
|
describe("setupTools", () => {
|
|
7
8
|
let server: McpServer;
|
|
8
9
|
let context: MCPContext;
|
|
9
|
-
let consoleErrorSpy:
|
|
10
|
+
let consoleErrorSpy: MockInstance<typeof console.error>;
|
|
10
11
|
|
|
11
12
|
beforeEach(() => {
|
|
12
13
|
server = new McpServer({
|
|
@@ -23,14 +24,14 @@ describe("setupTools", () => {
|
|
|
23
24
|
consoleErrorSpy.mockRestore();
|
|
24
25
|
});
|
|
25
26
|
|
|
26
|
-
it("should register all
|
|
27
|
+
it("should register all 24 tools with correct handlers", () => {
|
|
27
28
|
const registerToolSpy = vi.spyOn(server, "registerTool");
|
|
28
29
|
|
|
29
30
|
setupTools(server, context);
|
|
30
31
|
|
|
31
|
-
expect(registerToolSpy).toHaveBeenCalledTimes(
|
|
32
|
+
expect(registerToolSpy).toHaveBeenCalledTimes(24);
|
|
32
33
|
|
|
33
|
-
for (let i = 0; i <
|
|
34
|
+
for (let i = 0; i < 24; i++) {
|
|
34
35
|
const [toolName, schema, handler] = registerToolSpy.mock.calls[i];
|
|
35
36
|
expect(typeof toolName).toBe("string");
|
|
36
37
|
expect(toolName).toMatch(/^esa_/); // All tools should start with 'esa_'
|