@dpesch/mantisbt-mcp-server 1.5.9 → 1.6.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/CHANGELOG.md +22 -0
- package/README.de.md +22 -11
- package/README.md +22 -11
- package/dist/client.js +37 -23
- package/dist/config.js +41 -50
- package/dist/index.js +23 -13
- package/dist/prompts/index.js +112 -0
- package/dist/resources/index.js +29 -0
- package/dist/tools/config.js +44 -40
- package/docs/cookbook.de.md +218 -0
- package/docs/cookbook.md +218 -0
- package/docs/examples.de.md +42 -0
- package/docs/examples.md +42 -0
- package/package.json +2 -2
- package/server.json +2 -2
- package/tests/client.test.ts +70 -0
- package/tests/config.test.ts +47 -37
- package/tests/helpers/mock-server.ts +61 -0
- package/tests/prompts/prompts.test.ts +242 -0
- package/tests/resources/resources.test.ts +192 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { MantisClient } from '../../src/client.js';
|
|
3
|
+
import { MetadataCache } from '../../src/cache.js';
|
|
4
|
+
import { registerResources } from '../../src/resources/index.js';
|
|
5
|
+
import { MockMcpServer, makeResponse } from '../helpers/mock-server.js';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Inline fixtures
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
const USER_FIXTURE = { id: 1, name: 'jsmith', real_name: 'John Smith', email: 'jsmith@example.com' };
|
|
12
|
+
|
|
13
|
+
const PROJECTS_FIXTURE = [
|
|
14
|
+
{ id: 10, name: 'Alpha', enabled: true },
|
|
15
|
+
{ id: 11, name: 'Beta', enabled: true },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const ENUM_FIXTURE = {
|
|
19
|
+
configs: [
|
|
20
|
+
{ option: 'severity_enum_string', value: '10:feature,50:minor,80:block' },
|
|
21
|
+
{ option: 'status_enum_string', value: '10:new,80:resolved,90:closed' },
|
|
22
|
+
{ option: 'priority_enum_string', value: '10:none,30:normal,60:immediate' },
|
|
23
|
+
{ option: 'resolution_enum_string', value: '10:open,20:fixed' },
|
|
24
|
+
{ option: 'reproducibility_enum_string', value: '10:always,70:have not tried' },
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Setup
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
let mockServer: MockMcpServer;
|
|
33
|
+
let client: MantisClient;
|
|
34
|
+
let cache: MetadataCache;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
mockServer = new MockMcpServer();
|
|
38
|
+
client = new MantisClient('https://mantis.example.com', 'test-token');
|
|
39
|
+
cache = new MetadataCache('/tmp/cache-resources-test', 86400);
|
|
40
|
+
registerResources(mockServer as never, client, cache);
|
|
41
|
+
vi.stubGlobal('fetch', vi.fn());
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(async () => {
|
|
45
|
+
vi.unstubAllGlobals();
|
|
46
|
+
await cache.invalidate();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Registration
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
describe('resource registration', () => {
|
|
54
|
+
it('registers mantis://me', () => {
|
|
55
|
+
expect(mockServer.hasResourceRegistered('mantis://me')).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('registers mantis://projects', () => {
|
|
59
|
+
expect(mockServer.hasResourceRegistered('mantis://projects')).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('registers mantis://enums', () => {
|
|
63
|
+
expect(mockServer.hasResourceRegistered('mantis://enums')).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// mantis://me
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
describe('mantis://me', () => {
|
|
72
|
+
it('returns a single content item with application/json', async () => {
|
|
73
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(USER_FIXTURE)));
|
|
74
|
+
|
|
75
|
+
const result = await mockServer.callResource('mantis://me');
|
|
76
|
+
|
|
77
|
+
expect(result.contents).toHaveLength(1);
|
|
78
|
+
expect(result.contents[0]!.mimeType).toBe('application/json');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('sets uri to mantis://me', async () => {
|
|
82
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(USER_FIXTURE)));
|
|
83
|
+
|
|
84
|
+
const result = await mockServer.callResource('mantis://me');
|
|
85
|
+
|
|
86
|
+
expect(result.contents[0]!.uri).toBe('mantis://me');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('returns valid JSON with id and name', async () => {
|
|
90
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(USER_FIXTURE)));
|
|
91
|
+
|
|
92
|
+
const result = await mockServer.callResource('mantis://me');
|
|
93
|
+
|
|
94
|
+
const parsed = JSON.parse(result.contents[0]!.text) as { id: number; name: string };
|
|
95
|
+
expect(parsed.id).toBe(USER_FIXTURE.id);
|
|
96
|
+
expect(parsed.name).toBe(USER_FIXTURE.name);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// mantis://projects
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
describe('mantis://projects', () => {
|
|
105
|
+
it('returns a single content item with application/json', async () => {
|
|
106
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ projects: PROJECTS_FIXTURE })));
|
|
107
|
+
|
|
108
|
+
const result = await mockServer.callResource('mantis://projects');
|
|
109
|
+
|
|
110
|
+
expect(result.contents).toHaveLength(1);
|
|
111
|
+
expect(result.contents[0]!.mimeType).toBe('application/json');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('sets uri to mantis://projects', async () => {
|
|
115
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ projects: PROJECTS_FIXTURE })));
|
|
116
|
+
|
|
117
|
+
const result = await mockServer.callResource('mantis://projects');
|
|
118
|
+
|
|
119
|
+
expect(result.contents[0]!.uri).toBe('mantis://projects');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('returns a JSON array of projects from live API when cache is empty', async () => {
|
|
123
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ projects: PROJECTS_FIXTURE })));
|
|
124
|
+
|
|
125
|
+
const result = await mockServer.callResource('mantis://projects');
|
|
126
|
+
|
|
127
|
+
const parsed = JSON.parse(result.contents[0]!.text) as unknown[];
|
|
128
|
+
expect(Array.isArray(parsed)).toBe(true);
|
|
129
|
+
expect(parsed).toHaveLength(2);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('serves from cache without calling the API when cache is valid', async () => {
|
|
133
|
+
await cache.save({
|
|
134
|
+
timestamp: Date.now(),
|
|
135
|
+
projects: PROJECTS_FIXTURE,
|
|
136
|
+
byProject: {},
|
|
137
|
+
tags: [],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const result = await mockServer.callResource('mantis://projects');
|
|
141
|
+
|
|
142
|
+
expect(vi.mocked(fetch)).not.toHaveBeenCalled();
|
|
143
|
+
const parsed = JSON.parse(result.contents[0]!.text) as unknown[];
|
|
144
|
+
expect(parsed).toHaveLength(2);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// mantis://enums
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
describe('mantis://enums', () => {
|
|
153
|
+
it('returns a single content item with application/json', async () => {
|
|
154
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(ENUM_FIXTURE)));
|
|
155
|
+
|
|
156
|
+
const result = await mockServer.callResource('mantis://enums');
|
|
157
|
+
|
|
158
|
+
expect(result.contents).toHaveLength(1);
|
|
159
|
+
expect(result.contents[0]!.mimeType).toBe('application/json');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('sets uri to mantis://enums', async () => {
|
|
163
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(ENUM_FIXTURE)));
|
|
164
|
+
|
|
165
|
+
const result = await mockServer.callResource('mantis://enums');
|
|
166
|
+
|
|
167
|
+
expect(result.contents[0]!.uri).toBe('mantis://enums');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('returns parsed enum groups with id and name entries', async () => {
|
|
171
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(ENUM_FIXTURE)));
|
|
172
|
+
|
|
173
|
+
const result = await mockServer.callResource('mantis://enums');
|
|
174
|
+
|
|
175
|
+
const parsed = JSON.parse(result.contents[0]!.text) as Record<string, Array<{ id: number; name: string }>>;
|
|
176
|
+
for (const key of ['severity', 'priority', 'status', 'resolution', 'reproducibility']) {
|
|
177
|
+
expect(Array.isArray(parsed[key])).toBe(true);
|
|
178
|
+
expect(parsed[key]!.length).toBeGreaterThan(0);
|
|
179
|
+
expect(parsed[key]![0]).toMatchObject({ id: expect.any(Number), name: expect.any(String) });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('parses severity values correctly from fixture', async () => {
|
|
184
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(ENUM_FIXTURE)));
|
|
185
|
+
|
|
186
|
+
const result = await mockServer.callResource('mantis://enums');
|
|
187
|
+
|
|
188
|
+
const parsed = JSON.parse(result.contents[0]!.text) as Record<string, Array<{ id: number; name: string }>>;
|
|
189
|
+
expect(parsed['severity']).toContainEqual({ id: 10, name: 'feature' });
|
|
190
|
+
expect(parsed['severity']).toContainEqual({ id: 50, name: 'minor' });
|
|
191
|
+
});
|
|
192
|
+
});
|