@dpesch/mantisbt-mcp-server 1.2.0 → 1.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.de.md +25 -4
  3. package/README.md +25 -4
  4. package/dist/index.js +2 -2
  5. package/dist/search/index.js +6 -0
  6. package/dist/search/store.js +16 -0
  7. package/dist/search/sync.js +18 -6
  8. package/dist/search/tools.js +53 -6
  9. package/dist/tools/config.js +106 -10
  10. package/dist/tools/files.js +2 -2
  11. package/dist/tools/issues.js +13 -14
  12. package/dist/tools/metadata.js +56 -17
  13. package/dist/tools/monitors.js +2 -2
  14. package/dist/tools/notes.js +4 -4
  15. package/dist/tools/projects.js +13 -6
  16. package/dist/tools/relationships.js +5 -5
  17. package/dist/tools/tags.js +4 -4
  18. package/dist/tools/version.js +16 -1
  19. package/package.json +1 -1
  20. package/scripts/record-fixtures.ts +1 -1
  21. package/tests/cache.test.ts +1 -0
  22. package/tests/fixtures/get_current_user.json +6 -5
  23. package/tests/fixtures/get_issue.json +43 -83
  24. package/tests/fixtures/get_issue_fields_sample.json +17 -51
  25. package/tests/fixtures/get_project_categories.json +99 -2
  26. package/tests/fixtures/get_project_versions_with_data.json +35 -11
  27. package/tests/fixtures/list_issues.json +51 -57
  28. package/tests/fixtures/list_projects.json +45 -45
  29. package/tests/helpers/mock-server.ts +38 -4
  30. package/tests/helpers/search-mocks.ts +3 -1
  31. package/tests/search/sync.test.ts +50 -0
  32. package/tests/search/tools.test.ts +97 -0
  33. package/tests/tools/config.test.ts +97 -0
  34. package/tests/tools/issues.test.ts +51 -4
  35. package/tests/tools/metadata.test.ts +122 -0
  36. package/tests/tools/projects.test.ts +31 -0
  37. package/tests/tools/string-coercion.test.ts +251 -0
@@ -0,0 +1,251 @@
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 { registerNoteTools } from '../../src/tools/notes.js';
16
+ import { registerFileTools } from '../../src/tools/files.js';
17
+ import { registerMonitorTools } from '../../src/tools/monitors.js';
18
+ import { registerRelationshipTools } from '../../src/tools/relationships.js';
19
+ import { registerTagTools } from '../../src/tools/tags.js';
20
+ import { registerProjectTools } from '../../src/tools/projects.js';
21
+ import { MockMcpServer, makeResponse } from '../helpers/mock-server.js';
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
+ registerIssueTools(mockServer as never, client);
34
+ registerNoteTools(mockServer as never, client);
35
+ registerFileTools(mockServer as never, client);
36
+ registerMonitorTools(mockServer as never, client);
37
+ registerRelationshipTools(mockServer as never, client);
38
+ registerTagTools(mockServer as never, client);
39
+ registerProjectTools(mockServer as never, client);
40
+ vi.stubGlobal('fetch', vi.fn());
41
+ });
42
+
43
+ afterEach(() => {
44
+ vi.unstubAllGlobals();
45
+ vi.clearAllMocks();
46
+ });
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Issues
50
+ // ---------------------------------------------------------------------------
51
+
52
+ describe('string-coercion – get_issue', () => {
53
+ it('accepts issue_id as string "1940"', async () => {
54
+ vi.mocked(fetch).mockResolvedValue(
55
+ makeResponse(200, JSON.stringify({ issues: [{ id: 1940, summary: 'Test' }] }))
56
+ );
57
+
58
+ const result = await mockServer.callTool('get_issue', { id: '1940' }, { validate: true });
59
+
60
+ expect(result.isError).toBeUndefined();
61
+ const calledUrl = vi.mocked(fetch).mock.calls[0]![0] as string;
62
+ expect(calledUrl).toContain('1940');
63
+ });
64
+
65
+ it('rejects a non-numeric string', async () => {
66
+ const result = await mockServer.callTool('get_issue', { id: 'abc' }, { validate: true });
67
+ expect(result.isError).toBe(true);
68
+ });
69
+ });
70
+
71
+ describe('string-coercion – list_issues', () => {
72
+ it('accepts project_id as string', async () => {
73
+ vi.mocked(fetch).mockResolvedValue(
74
+ makeResponse(200, JSON.stringify({ issues: [], total_count: 0 }))
75
+ );
76
+
77
+ const result = await mockServer.callTool(
78
+ 'list_issues',
79
+ { project_id: '5', page: '1', page_size: '10' },
80
+ { validate: true },
81
+ );
82
+
83
+ expect(result.isError).toBeUndefined();
84
+ });
85
+ });
86
+
87
+ describe('string-coercion – update_issue', () => {
88
+ it('accepts id as string', async () => {
89
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ issue: { id: 99, summary: 'Updated' } })));
90
+
91
+ const result = await mockServer.callTool(
92
+ 'update_issue',
93
+ { id: '99', fields: { summary: 'Updated' } },
94
+ { validate: true },
95
+ );
96
+
97
+ expect(result.isError).toBeUndefined();
98
+ });
99
+ });
100
+
101
+ describe('string-coercion – delete_issue', () => {
102
+ it('accepts id as string', async () => {
103
+ vi.mocked(fetch).mockResolvedValue(makeResponse(204, ''));
104
+
105
+ const result = await mockServer.callTool('delete_issue', { id: '42' }, { validate: true });
106
+
107
+ expect(result.isError).toBeUndefined();
108
+ });
109
+ });
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Notes
113
+ // ---------------------------------------------------------------------------
114
+
115
+ describe('string-coercion – list_notes', () => {
116
+ it('accepts issue_id as string', async () => {
117
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ issues: [{ notes: [] }] })));
118
+
119
+ const result = await mockServer.callTool('list_notes', { issue_id: '1940' }, { validate: true });
120
+
121
+ expect(result.isError).toBeUndefined();
122
+ });
123
+ });
124
+
125
+ describe('string-coercion – delete_note', () => {
126
+ it('accepts issue_id and note_id as strings', async () => {
127
+ vi.mocked(fetch).mockResolvedValue(makeResponse(204, ''));
128
+
129
+ const result = await mockServer.callTool(
130
+ 'delete_note',
131
+ { issue_id: '1940', note_id: '77' },
132
+ { validate: true },
133
+ );
134
+
135
+ expect(result.isError).toBeUndefined();
136
+ });
137
+ });
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Files
141
+ // ---------------------------------------------------------------------------
142
+
143
+ describe('string-coercion – list_issue_files', () => {
144
+ it('accepts issue_id as string', async () => {
145
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ issues: [{ attachments: [] }] })));
146
+
147
+ const result = await mockServer.callTool('list_issue_files', { issue_id: '1940' }, { validate: true });
148
+
149
+ expect(result.isError).toBeUndefined();
150
+ });
151
+ });
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Monitors
155
+ // ---------------------------------------------------------------------------
156
+
157
+ describe('string-coercion – add_monitor', () => {
158
+ it('accepts issue_id as string', async () => {
159
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, ''));
160
+
161
+ const result = await mockServer.callTool(
162
+ 'add_monitor',
163
+ { issue_id: '1940', username: 'jdoe' },
164
+ { validate: true },
165
+ );
166
+
167
+ expect(result.isError).toBeUndefined();
168
+ });
169
+ });
170
+
171
+ describe('string-coercion – remove_monitor', () => {
172
+ it('accepts issue_id as string', async () => {
173
+ vi.mocked(fetch).mockResolvedValue(makeResponse(204, ''));
174
+
175
+ const result = await mockServer.callTool(
176
+ 'remove_monitor',
177
+ { issue_id: '1940', username: 'jdoe' },
178
+ { validate: true },
179
+ );
180
+
181
+ expect(result.isError).toBeUndefined();
182
+ });
183
+ });
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // Relationships
187
+ // ---------------------------------------------------------------------------
188
+
189
+ describe('string-coercion – add_relationship', () => {
190
+ it('accepts issue_id, target_id and type_id as strings', async () => {
191
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ id: 1 })));
192
+
193
+ const result = await mockServer.callTool(
194
+ 'add_relationship',
195
+ { issue_id: '1940', target_id: '1941', type_id: '1' },
196
+ { validate: true },
197
+ );
198
+
199
+ expect(result.isError).toBeUndefined();
200
+ });
201
+ });
202
+
203
+ describe('string-coercion – remove_relationship', () => {
204
+ it('accepts issue_id and relationship_id as strings', async () => {
205
+ vi.mocked(fetch).mockResolvedValue(makeResponse(204, ''));
206
+
207
+ const result = await mockServer.callTool(
208
+ 'remove_relationship',
209
+ { issue_id: '1940', relationship_id: '55' },
210
+ { validate: true },
211
+ );
212
+
213
+ expect(result.isError).toBeUndefined();
214
+ });
215
+ });
216
+
217
+ // ---------------------------------------------------------------------------
218
+ // Tags
219
+ // ---------------------------------------------------------------------------
220
+
221
+ describe('string-coercion – detach_tag', () => {
222
+ it('accepts issue_id and tag_id as strings', async () => {
223
+ vi.mocked(fetch).mockResolvedValue(makeResponse(204, ''));
224
+
225
+ const result = await mockServer.callTool(
226
+ 'detach_tag',
227
+ { issue_id: '1940', tag_id: '7' },
228
+ { validate: true },
229
+ );
230
+
231
+ expect(result.isError).toBeUndefined();
232
+ });
233
+ });
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Projects
237
+ // ---------------------------------------------------------------------------
238
+
239
+ describe('string-coercion – get_project_versions', () => {
240
+ it('accepts project_id as string', async () => {
241
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ projects: [{ versions: [] }] })));
242
+
243
+ const result = await mockServer.callTool(
244
+ 'get_project_versions',
245
+ { project_id: '3' },
246
+ { validate: true },
247
+ );
248
+
249
+ expect(result.isError).toBeUndefined();
250
+ });
251
+ });