@dpesch/mantisbt-mcp-server 1.10.3 → 1.10.6

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/package.json +1 -1
  3. package/server.json +2 -2
  4. package/.gitea/PULL_REQUEST_TEMPLATE.md +0 -17
  5. package/.gitea/workflows/ci.yml +0 -95
  6. package/.github/workflows/ci.yml +0 -32
  7. package/scripts/hooks/pre-push.mjs +0 -220
  8. package/scripts/init.mjs +0 -95
  9. package/scripts/record-fixtures.ts +0 -160
  10. package/tests/cache.test.ts +0 -265
  11. package/tests/client.test.ts +0 -372
  12. package/tests/config.test.ts +0 -263
  13. package/tests/fixtures/get_current_user.json +0 -40
  14. package/tests/fixtures/get_issue.json +0 -133
  15. package/tests/fixtures/get_issue_enums.json +0 -67
  16. package/tests/fixtures/get_issue_fields_sample.json +0 -76
  17. package/tests/fixtures/get_project_categories.json +0 -157
  18. package/tests/fixtures/get_project_versions.json +0 -3
  19. package/tests/fixtures/get_project_versions_with_data.json +0 -52
  20. package/tests/fixtures/list_issues.json +0 -95
  21. package/tests/fixtures/list_projects.json +0 -65
  22. package/tests/helpers/mock-server.ts +0 -166
  23. package/tests/helpers/search-mocks.ts +0 -84
  24. package/tests/prompts/prompts.test.ts +0 -242
  25. package/tests/resources/resources.test.ts +0 -309
  26. package/tests/search/embedder.test.ts +0 -81
  27. package/tests/search/highlight.test.ts +0 -129
  28. package/tests/search/store.test.ts +0 -193
  29. package/tests/search/sync.test.ts +0 -249
  30. package/tests/search/tools.test.ts +0 -661
  31. package/tests/tools/config.test.ts +0 -212
  32. package/tests/tools/files.test.ts +0 -343
  33. package/tests/tools/issues.test.ts +0 -1180
  34. package/tests/tools/metadata.test.ts +0 -509
  35. package/tests/tools/monitors.test.ts +0 -101
  36. package/tests/tools/projects.test.ts +0 -338
  37. package/tests/tools/relationships.test.ts +0 -177
  38. package/tests/tools/string-coercion.test.ts +0 -317
  39. package/tests/tools/users.test.ts +0 -62
  40. package/tests/utils/date-filter.test.ts +0 -169
  41. package/tests/version-hint.test.ts +0 -230
  42. package/tsconfig.build.json +0 -8
  43. package/vitest.config.ts +0 -8
@@ -1,317 +0,0 @@
1
- /**
2
- * Regression tests for string-to-number coercion in MCP tool schemas.
3
- *
4
- * The MCP protocol allows clients to pass numeric IDs as strings (e.g. "1940"
5
- * instead of 1940). Without z.coerce.number(), the Zod schema would reject
6
- * these inputs with error -32602 (Invalid params).
7
- *
8
- * These tests use { validate: true } to run args through the Zod schema —
9
- * exactly as the real MCP server does — before passing them to the handler.
10
- */
11
-
12
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
13
- import { MantisClient } from '../../src/client.js';
14
- import { registerIssueTools } from '../../src/tools/issues.js';
15
- import { MetadataCache } from '../../src/cache.js';
16
- import { registerNoteTools } from '../../src/tools/notes.js';
17
- import { registerFileTools } from '../../src/tools/files.js';
18
- import { registerMonitorTools } from '../../src/tools/monitors.js';
19
- import { registerRelationshipTools } from '../../src/tools/relationships.js';
20
- import { registerTagTools } from '../../src/tools/tags.js';
21
- import { registerProjectTools } from '../../src/tools/projects.js';
22
- import { registerVersionTools } from '../../src/tools/version.js';
23
- import { VersionHintService } from '../../src/version-hint.js';
24
- import { MockMcpServer, makeResponse } from '../helpers/mock-server.js';
25
-
26
- // ---------------------------------------------------------------------------
27
- // Setup
28
- // ---------------------------------------------------------------------------
29
-
30
- let mockServer: MockMcpServer;
31
- let client: MantisClient;
32
-
33
- beforeEach(() => {
34
- mockServer = new MockMcpServer();
35
- client = new MantisClient('https://mantis.example.com', 'test-token');
36
- const stubCache = { loadIfValid: vi.fn(async () => null) } as unknown as MetadataCache;
37
- registerIssueTools(mockServer as never, client, stubCache);
38
- registerNoteTools(mockServer as never, client);
39
- registerFileTools(mockServer as never, client);
40
- registerMonitorTools(mockServer as never, client);
41
- registerRelationshipTools(mockServer as never, client);
42
- registerTagTools(mockServer as never, client);
43
- registerProjectTools(mockServer as never, client);
44
- registerVersionTools(mockServer as never, client, new VersionHintService(), '0.0.0-test');
45
- vi.stubGlobal('fetch', vi.fn());
46
- });
47
-
48
- afterEach(() => {
49
- vi.unstubAllGlobals();
50
- vi.clearAllMocks();
51
- });
52
-
53
- // ---------------------------------------------------------------------------
54
- // Issues
55
- // ---------------------------------------------------------------------------
56
-
57
- describe('string-coercion – get_issue', () => {
58
- it('accepts issue_id as string "1940"', async () => {
59
- vi.mocked(fetch).mockResolvedValue(
60
- makeResponse(200, JSON.stringify({ issues: [{ id: 1940, summary: 'Test' }] }))
61
- );
62
-
63
- const result = await mockServer.callTool('get_issue', { id: '1940' }, { validate: true });
64
-
65
- expect(result.isError).toBeUndefined();
66
- const calledUrl = vi.mocked(fetch).mock.calls[0]![0] as string;
67
- expect(calledUrl).toContain('1940');
68
- });
69
-
70
- it('rejects a non-numeric string', async () => {
71
- const result = await mockServer.callTool('get_issue', { id: 'abc' }, { validate: true });
72
- expect(result.isError).toBe(true);
73
- });
74
- });
75
-
76
- describe('string-coercion – list_issues', () => {
77
- it('accepts project_id as string', async () => {
78
- vi.mocked(fetch).mockResolvedValue(
79
- makeResponse(200, JSON.stringify({ issues: [], total_count: 0 }))
80
- );
81
-
82
- const result = await mockServer.callTool(
83
- 'list_issues',
84
- { project_id: '5', page: '1', page_size: '10' },
85
- { validate: true },
86
- );
87
-
88
- expect(result.isError).toBeUndefined();
89
- });
90
- });
91
-
92
- describe('string-coercion – update_issue', () => {
93
- it('accepts id as string', async () => {
94
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ issue: { id: 99, summary: 'Updated' } })));
95
-
96
- const result = await mockServer.callTool(
97
- 'update_issue',
98
- { id: '99', fields: { summary: 'Updated' } },
99
- { validate: true },
100
- );
101
-
102
- expect(result.isError).toBeUndefined();
103
- });
104
- });
105
-
106
- describe('string-coercion – delete_issue', () => {
107
- it('accepts id as string', async () => {
108
- vi.mocked(fetch).mockResolvedValue(makeResponse(204, ''));
109
-
110
- const result = await mockServer.callTool('delete_issue', { id: '42' }, { validate: true });
111
-
112
- expect(result.isError).toBeUndefined();
113
- });
114
- });
115
-
116
- // ---------------------------------------------------------------------------
117
- // Notes
118
- // ---------------------------------------------------------------------------
119
-
120
- describe('string-coercion – list_notes', () => {
121
- it('accepts issue_id as string', async () => {
122
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ issues: [{ notes: [] }] })));
123
-
124
- const result = await mockServer.callTool('list_notes', { issue_id: '1940' }, { validate: true });
125
-
126
- expect(result.isError).toBeUndefined();
127
- });
128
- });
129
-
130
- describe('string-coercion – delete_note', () => {
131
- it('accepts issue_id and note_id as strings', async () => {
132
- vi.mocked(fetch).mockResolvedValue(makeResponse(204, ''));
133
-
134
- const result = await mockServer.callTool(
135
- 'delete_note',
136
- { issue_id: '1940', note_id: '77' },
137
- { validate: true },
138
- );
139
-
140
- expect(result.isError).toBeUndefined();
141
- });
142
- });
143
-
144
- // ---------------------------------------------------------------------------
145
- // Files
146
- // ---------------------------------------------------------------------------
147
-
148
- describe('string-coercion – list_issue_files', () => {
149
- it('accepts issue_id as string', async () => {
150
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ issues: [{ attachments: [] }] })));
151
-
152
- const result = await mockServer.callTool('list_issue_files', { issue_id: '1940' }, { validate: true });
153
-
154
- expect(result.isError).toBeUndefined();
155
- });
156
- });
157
-
158
- // ---------------------------------------------------------------------------
159
- // Monitors
160
- // ---------------------------------------------------------------------------
161
-
162
- describe('string-coercion – add_monitor', () => {
163
- it('accepts issue_id as string', async () => {
164
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, ''));
165
-
166
- const result = await mockServer.callTool(
167
- 'add_monitor',
168
- { issue_id: '1940', username: 'jdoe' },
169
- { validate: true },
170
- );
171
-
172
- expect(result.isError).toBeUndefined();
173
- });
174
- });
175
-
176
- describe('string-coercion – remove_monitor', () => {
177
- it('accepts issue_id as string', async () => {
178
- vi.mocked(fetch).mockResolvedValue(makeResponse(204, ''));
179
-
180
- const result = await mockServer.callTool(
181
- 'remove_monitor',
182
- { issue_id: '1940', username: 'jdoe' },
183
- { validate: true },
184
- );
185
-
186
- expect(result.isError).toBeUndefined();
187
- });
188
- });
189
-
190
- // ---------------------------------------------------------------------------
191
- // Relationships
192
- // ---------------------------------------------------------------------------
193
-
194
- describe('string-coercion – add_relationship', () => {
195
- it('accepts issue_id, target_id and type_id as strings', async () => {
196
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ id: 1 })));
197
-
198
- const result = await mockServer.callTool(
199
- 'add_relationship',
200
- { issue_id: '1940', target_id: '1941', type_id: '1' },
201
- { validate: true },
202
- );
203
-
204
- expect(result.isError).toBeUndefined();
205
- });
206
- });
207
-
208
- describe('string-coercion – remove_relationship', () => {
209
- it('accepts issue_id and relationship_id as strings', async () => {
210
- vi.mocked(fetch).mockResolvedValue(makeResponse(204, ''));
211
-
212
- const result = await mockServer.callTool(
213
- 'remove_relationship',
214
- { issue_id: '1940', relationship_id: '55' },
215
- { validate: true },
216
- );
217
-
218
- expect(result.isError).toBeUndefined();
219
- });
220
- });
221
-
222
- // ---------------------------------------------------------------------------
223
- // Tags
224
- // ---------------------------------------------------------------------------
225
-
226
- describe('string-coercion – detach_tag', () => {
227
- it('accepts issue_id and tag_id as strings', async () => {
228
- vi.mocked(fetch).mockResolvedValue(makeResponse(204, ''));
229
-
230
- const result = await mockServer.callTool(
231
- 'detach_tag',
232
- { issue_id: '1940', tag_id: '7' },
233
- { validate: true },
234
- );
235
-
236
- expect(result.isError).toBeUndefined();
237
- });
238
- });
239
-
240
- // ---------------------------------------------------------------------------
241
- // Projects
242
- // ---------------------------------------------------------------------------
243
-
244
- describe('string-coercion – get_project_versions', () => {
245
- it('accepts project_id as string', async () => {
246
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ projects: [{ versions: [] }] })));
247
-
248
- const result = await mockServer.callTool(
249
- 'get_project_versions',
250
- { project_id: '3' },
251
- { validate: true },
252
- );
253
-
254
- expect(result.isError).toBeUndefined();
255
- });
256
- });
257
-
258
- // ---------------------------------------------------------------------------
259
- // Boolean-Parameter als String
260
- // ---------------------------------------------------------------------------
261
-
262
- describe('string-coercion – update_issue fields as JSON string', () => {
263
- it('accepts fields as JSON string', async () => {
264
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ issue: { id: 99, summary: 'Updated' } })));
265
- const result = await mockServer.callTool(
266
- 'update_issue',
267
- { id: 99, fields: '{"summary":"Updated"}' },
268
- { validate: true },
269
- );
270
- expect(result.isError).toBeUndefined();
271
- });
272
-
273
- it('rejects invalid JSON in fields with a validation error (not an uncaught exception)', async () => {
274
- const result = await mockServer.callTool(
275
- 'update_issue',
276
- { id: 99, fields: '{invalid json' },
277
- { validate: true },
278
- );
279
- expect(result.isError).toBe(true);
280
- });
281
- });
282
-
283
- describe('string-coercion – update_issue dry_run as string', () => {
284
- it('accepts dry_run "true" as boolean true', async () => {
285
- const result = await mockServer.callTool(
286
- 'update_issue',
287
- { id: 99, fields: { summary: 'x' }, dry_run: 'true' },
288
- { validate: true },
289
- );
290
- expect(result.isError).toBeUndefined();
291
- expect(result.content[0]?.text).toContain('"dry_run": true');
292
- });
293
- });
294
-
295
- describe('string-coercion – get_project_versions boolean flags', () => {
296
- it('accepts obsolete "true" and inherit "false" as booleans', async () => {
297
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ projects: [{ versions: [] }] })));
298
- const result = await mockServer.callTool(
299
- 'get_project_versions',
300
- { project_id: 3, obsolete: 'true', inherit: 'false' },
301
- { validate: true },
302
- );
303
- expect(result.isError).toBeUndefined();
304
- });
305
- });
306
-
307
- describe('string-coercion – get_mantis_version check_latest as string', () => {
308
- it('accepts check_latest "false" as boolean false', async () => {
309
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ version: '2.26.0' })));
310
- const result = await mockServer.callTool(
311
- 'get_mantis_version',
312
- { check_latest: 'false' },
313
- { validate: true },
314
- );
315
- expect(result.isError).toBeUndefined();
316
- });
317
- });
@@ -1,62 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { readFileSync, existsSync } from 'node:fs';
3
- import { join, dirname } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { MantisClient } from '../../src/client.js';
6
- import { registerUserTools } from '../../src/tools/users.js';
7
- import { MockMcpServer, makeResponse } from '../helpers/mock-server.js';
8
-
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = dirname(__filename);
11
- const fixturesDir = join(__dirname, '..', 'fixtures');
12
-
13
- // ---------------------------------------------------------------------------
14
- // Fixtures laden (mit Inline-Fallback)
15
- // ---------------------------------------------------------------------------
16
-
17
- const getCurrentUserFixturePath = join(fixturesDir, 'get_current_user.json');
18
-
19
- const getCurrentUserFixture = existsSync(getCurrentUserFixturePath)
20
- ? (JSON.parse(readFileSync(getCurrentUserFixturePath, 'utf-8')) as { id: number; name: string; real_name?: string; email?: string })
21
- : { id: 5, name: 'testuser', real_name: 'Test User', email: 'test@example.com' };
22
-
23
- // ---------------------------------------------------------------------------
24
- // Setup
25
- // ---------------------------------------------------------------------------
26
-
27
- let mockServer: MockMcpServer;
28
- let client: MantisClient;
29
-
30
- beforeEach(() => {
31
- mockServer = new MockMcpServer();
32
- client = new MantisClient('https://mantis.example.com', 'test-token');
33
- registerUserTools(mockServer as never, client);
34
- vi.stubGlobal('fetch', vi.fn());
35
- });
36
-
37
- afterEach(() => {
38
- vi.unstubAllGlobals();
39
- });
40
-
41
- // ---------------------------------------------------------------------------
42
- // get_current_user
43
- // ---------------------------------------------------------------------------
44
-
45
- describe('get_current_user', () => {
46
- it('ist registriert', () => {
47
- expect(mockServer.hasToolRegistered('get_current_user')).toBe(true);
48
- });
49
-
50
- it('gibt User-Daten zurück mit id und name', async () => {
51
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(getCurrentUserFixture)));
52
-
53
- const result = await mockServer.callTool('get_current_user', {});
54
-
55
- expect(result.isError).toBeUndefined();
56
- const parsed = JSON.parse(result.content[0]!.text) as { id: number; name: string };
57
- expect(typeof parsed.id).toBe('number');
58
- expect(typeof parsed.name).toBe('string');
59
- expect(parsed.id).toBe(getCurrentUserFixture.id);
60
- expect(parsed.name).toBe(getCurrentUserFixture.name);
61
- });
62
- });
@@ -1,169 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { matchesDateFilter } from '../../src/date-filter.js';
3
-
4
- describe('matchesDateFilter', () => {
5
-
6
- // ---------------------------------------------------------------------------
7
- // No filter → always pass
8
- // ---------------------------------------------------------------------------
9
-
10
- it('returns true when no filter is set', () => {
11
- expect(matchesDateFilter(
12
- { updated_at: '2026-03-20T10:00:00Z', created_at: '2026-03-01T00:00:00Z' },
13
- {}
14
- )).toBe(true);
15
- });
16
-
17
- it('returns true for an item with no dates when no filter is set', () => {
18
- expect(matchesDateFilter({}, {})).toBe(true);
19
- });
20
-
21
- // ---------------------------------------------------------------------------
22
- // updated_after
23
- // ---------------------------------------------------------------------------
24
-
25
- it('updated_after: passes when updated_at is after the threshold', () => {
26
- expect(matchesDateFilter(
27
- { updated_at: '2026-03-25T12:00:00Z' },
28
- { updated_after: '2026-03-24T00:00:00Z' }
29
- )).toBe(true);
30
- });
31
-
32
- it('updated_after: fails when updated_at is before the threshold', () => {
33
- expect(matchesDateFilter(
34
- { updated_at: '2026-03-23T12:00:00Z' },
35
- { updated_after: '2026-03-24T00:00:00Z' }
36
- )).toBe(false);
37
- });
38
-
39
- it('updated_after: fails when updated_at equals the threshold (exclusive)', () => {
40
- expect(matchesDateFilter(
41
- { updated_at: '2026-03-24T00:00:00Z' },
42
- { updated_after: '2026-03-24T00:00:00Z' }
43
- )).toBe(false);
44
- });
45
-
46
- it('updated_after: fails when updated_at is missing', () => {
47
- expect(matchesDateFilter(
48
- { created_at: '2026-03-25T00:00:00Z' },
49
- { updated_after: '2026-03-24T00:00:00Z' }
50
- )).toBe(false);
51
- });
52
-
53
- // ---------------------------------------------------------------------------
54
- // updated_before
55
- // ---------------------------------------------------------------------------
56
-
57
- it('updated_before: passes when updated_at is before the threshold', () => {
58
- expect(matchesDateFilter(
59
- { updated_at: '2026-03-20T00:00:00Z' },
60
- { updated_before: '2026-03-25T00:00:00Z' }
61
- )).toBe(true);
62
- });
63
-
64
- it('updated_before: fails when updated_at is after the threshold', () => {
65
- expect(matchesDateFilter(
66
- { updated_at: '2026-03-26T00:00:00Z' },
67
- { updated_before: '2026-03-25T00:00:00Z' }
68
- )).toBe(false);
69
- });
70
-
71
- it('updated_before: fails when updated_at equals the threshold (exclusive)', () => {
72
- expect(matchesDateFilter(
73
- { updated_at: '2026-03-25T00:00:00Z' },
74
- { updated_before: '2026-03-25T00:00:00Z' }
75
- )).toBe(false);
76
- });
77
-
78
- it('updated_before: fails when updated_at is missing', () => {
79
- expect(matchesDateFilter(
80
- {},
81
- { updated_before: '2026-03-25T00:00:00Z' }
82
- )).toBe(false);
83
- });
84
-
85
- // ---------------------------------------------------------------------------
86
- // created_after
87
- // ---------------------------------------------------------------------------
88
-
89
- it('created_after: passes when created_at is after the threshold', () => {
90
- expect(matchesDateFilter(
91
- { created_at: '2026-03-25T12:00:00Z' },
92
- { created_after: '2026-03-24T00:00:00Z' }
93
- )).toBe(true);
94
- });
95
-
96
- it('created_after: fails when created_at is before the threshold', () => {
97
- expect(matchesDateFilter(
98
- { created_at: '2026-03-23T00:00:00Z' },
99
- { created_after: '2026-03-24T00:00:00Z' }
100
- )).toBe(false);
101
- });
102
-
103
- it('created_after: fails when created_at is missing', () => {
104
- expect(matchesDateFilter(
105
- { updated_at: '2026-03-25T00:00:00Z' },
106
- { created_after: '2026-03-24T00:00:00Z' }
107
- )).toBe(false);
108
- });
109
-
110
- // ---------------------------------------------------------------------------
111
- // created_before
112
- // ---------------------------------------------------------------------------
113
-
114
- it('created_before: passes when created_at is before the threshold', () => {
115
- expect(matchesDateFilter(
116
- { created_at: '2026-03-20T00:00:00Z' },
117
- { created_before: '2026-03-25T00:00:00Z' }
118
- )).toBe(true);
119
- });
120
-
121
- it('created_before: fails when created_at is after the threshold', () => {
122
- expect(matchesDateFilter(
123
- { created_at: '2026-03-26T00:00:00Z' },
124
- { created_before: '2026-03-25T00:00:00Z' }
125
- )).toBe(false);
126
- });
127
-
128
- // ---------------------------------------------------------------------------
129
- // Combined filters (time window)
130
- // ---------------------------------------------------------------------------
131
-
132
- it('updated_after + updated_before: passes when updated_at is within window', () => {
133
- expect(matchesDateFilter(
134
- { updated_at: '2026-03-22T00:00:00Z' },
135
- { updated_after: '2026-03-20T00:00:00Z', updated_before: '2026-03-25T00:00:00Z' }
136
- )).toBe(true);
137
- });
138
-
139
- it('updated_after + updated_before: fails when updated_at is outside window', () => {
140
- expect(matchesDateFilter(
141
- { updated_at: '2026-03-26T00:00:00Z' },
142
- { updated_after: '2026-03-20T00:00:00Z', updated_before: '2026-03-25T00:00:00Z' }
143
- )).toBe(false);
144
- });
145
-
146
- it('all four filters: passes only when both dates are within their windows', () => {
147
- expect(matchesDateFilter(
148
- { updated_at: '2026-03-22T00:00:00Z', created_at: '2026-03-10T00:00:00Z' },
149
- {
150
- updated_after: '2026-03-20T00:00:00Z',
151
- updated_before: '2026-03-25T00:00:00Z',
152
- created_after: '2026-03-05T00:00:00Z',
153
- created_before: '2026-03-15T00:00:00Z',
154
- }
155
- )).toBe(true);
156
- });
157
-
158
- it('all four filters: fails when one date is out of range', () => {
159
- expect(matchesDateFilter(
160
- { updated_at: '2026-03-22T00:00:00Z', created_at: '2026-03-16T00:00:00Z' }, // created_at too late
161
- {
162
- updated_after: '2026-03-20T00:00:00Z',
163
- updated_before: '2026-03-25T00:00:00Z',
164
- created_after: '2026-03-05T00:00:00Z',
165
- created_before: '2026-03-15T00:00:00Z',
166
- }
167
- )).toBe(false);
168
- });
169
- });