@dpesch/mantisbt-mcp-server 1.6.2 → 1.7.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 +7 -0
- package/dist/tools/issues.js +26 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/tests/tools/issues.test.ts +168 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.7.0] – 2026-03-26
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `create_issue`: new optional parameters `version`, `target_version`, `fixed_in_version`, `steps_to_reproduce`, `additional_information`, `reproducibility`, and `view_state` — these fields were already supported by the MantisBT REST API on issue creation but were missing from the tool, requiring a second `update_issue` call to set them.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
10
17
|
## [1.6.2] – 2026-03-24
|
|
11
18
|
|
|
12
19
|
### Fixed
|
package/dist/tools/issues.js
CHANGED
|
@@ -151,13 +151,20 @@ export function registerIssueTools(server, client, cache) {
|
|
|
151
151
|
severity: z.string().default('minor').describe('Severity name — must be a canonical English name: feature, trivial, text, tweak, minor, major, crash, block. Default: "minor". Call get_issue_enums to see localized labels.'),
|
|
152
152
|
handler_id: z.coerce.number().int().positive().optional().describe('User ID of the person to assign the issue to'),
|
|
153
153
|
handler: z.string().optional().describe('Username (login name) of the person to assign the issue to. Alternative to handler_id — the server resolves the name to a user ID from the project members. Use get_project_users to see available users.'),
|
|
154
|
+
version: z.string().optional().describe('Affected product version name (use get_project_versions to list available versions)'),
|
|
155
|
+
target_version: z.string().optional().describe('Target version name — version in which the issue is planned to be fixed (use get_project_versions to list available versions)'),
|
|
156
|
+
fixed_in_version: z.string().optional().describe('Version name in which the issue was fixed (use get_project_versions to list available versions)'),
|
|
157
|
+
steps_to_reproduce: z.string().optional().describe('Steps to reproduce the issue. Plain text or Markdown.'),
|
|
158
|
+
additional_information: z.string().optional().describe('Additional information about the issue. Plain text or Markdown.'),
|
|
159
|
+
reproducibility: z.string().optional().describe('Reproducibility — must be a canonical English name: always, sometimes, random, have not tried, unable to reproduce, N/A. Call get_issue_enums to see localized labels.'),
|
|
160
|
+
view_state: z.enum(['public', 'private']).optional().describe('Visibility of the issue: "public" (default) or "private"'),
|
|
154
161
|
}),
|
|
155
162
|
annotations: {
|
|
156
163
|
readOnlyHint: false,
|
|
157
164
|
destructiveHint: false,
|
|
158
165
|
idempotentHint: false,
|
|
159
166
|
},
|
|
160
|
-
}, async ({ summary, description, project_id, category, priority, severity, handler_id, handler }) => {
|
|
167
|
+
}, async ({ summary, description, project_id, category, priority, severity, handler_id, handler, version, target_version, fixed_in_version, steps_to_reproduce, additional_information, reproducibility, view_state }) => {
|
|
161
168
|
// Resolve handler username to handler_id when only a name is given
|
|
162
169
|
let resolvedHandlerId = handler_id;
|
|
163
170
|
if (resolvedHandlerId === undefined && handler !== undefined) {
|
|
@@ -199,6 +206,24 @@ export function registerIssueTools(server, client, cache) {
|
|
|
199
206
|
body.severity = severityResolved;
|
|
200
207
|
if (resolvedHandlerId)
|
|
201
208
|
body.handler = { id: resolvedHandlerId };
|
|
209
|
+
if (version !== undefined)
|
|
210
|
+
body.version = { name: version };
|
|
211
|
+
if (target_version !== undefined)
|
|
212
|
+
body.target_version = { name: target_version };
|
|
213
|
+
if (fixed_in_version !== undefined)
|
|
214
|
+
body.fixed_in_version = { name: fixed_in_version };
|
|
215
|
+
if (steps_to_reproduce !== undefined)
|
|
216
|
+
body.steps_to_reproduce = steps_to_reproduce;
|
|
217
|
+
if (additional_information !== undefined)
|
|
218
|
+
body.additional_information = additional_information;
|
|
219
|
+
if (reproducibility !== undefined) {
|
|
220
|
+
const reproducibilityResolved = resolveEnum('reproducibility', reproducibility);
|
|
221
|
+
if (typeof reproducibilityResolved === 'string')
|
|
222
|
+
return { content: [{ type: 'text', text: errorText(reproducibilityResolved) }], isError: true };
|
|
223
|
+
body.reproducibility = reproducibilityResolved;
|
|
224
|
+
}
|
|
225
|
+
if (view_state !== undefined)
|
|
226
|
+
body.view_state = { name: view_state };
|
|
202
227
|
const raw = await client.post('issues', body);
|
|
203
228
|
const partial = ('issue' in raw && typeof raw['issue'] === 'object' && raw['issue'] !== null)
|
|
204
229
|
? raw['issue']
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"name": "io.github.dpesch/mantisbt-mcp-server",
|
|
4
4
|
"title": "MantisBT MCP Server",
|
|
5
5
|
"description": "MantisBT MCP server – manage issues, notes, files, tags, and relationships. With semantic search.",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.7.0",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"registryType": "npm",
|
|
10
10
|
"identifier": "@dpesch/mantisbt-mcp-server",
|
|
11
|
-
"version": "1.
|
|
11
|
+
"version": "1.7.0",
|
|
12
12
|
"runtimeHint": "npx",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|
|
@@ -237,6 +237,174 @@ describe('create_issue', () => {
|
|
|
237
237
|
expect(result.content[0]!.text).toContain('minor');
|
|
238
238
|
expect(fetch).not.toHaveBeenCalled();
|
|
239
239
|
});
|
|
240
|
+
|
|
241
|
+
it('creates issue without any optional fields (backward compatibility)', async () => {
|
|
242
|
+
vi.mocked(fetch).mockResolvedValue(
|
|
243
|
+
makeResponse(201, JSON.stringify({ issue: { id: 300, summary: 'Minimal' } }))
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const result = await mockServer.callTool('create_issue', {
|
|
247
|
+
summary: 'Minimal issue',
|
|
248
|
+
description: 'Minimal description.',
|
|
249
|
+
project_id: 1,
|
|
250
|
+
category: 'General',
|
|
251
|
+
}, { validate: true });
|
|
252
|
+
|
|
253
|
+
expect(result.isError).toBeUndefined();
|
|
254
|
+
const body = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]!.body as string) as Record<string, unknown>;
|
|
255
|
+
expect(body.version).toBeUndefined();
|
|
256
|
+
expect(body.target_version).toBeUndefined();
|
|
257
|
+
expect(body.fixed_in_version).toBeUndefined();
|
|
258
|
+
expect(body.steps_to_reproduce).toBeUndefined();
|
|
259
|
+
expect(body.additional_information).toBeUndefined();
|
|
260
|
+
expect(body.reproducibility).toBeUndefined();
|
|
261
|
+
expect(body.view_state).toBeUndefined();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('sends version fields as { name } objects', async () => {
|
|
265
|
+
vi.mocked(fetch).mockResolvedValue(
|
|
266
|
+
makeResponse(201, JSON.stringify({ issue: { id: 301, summary: 'With versions' } }))
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
await mockServer.callTool('create_issue', {
|
|
270
|
+
summary: 'Version test',
|
|
271
|
+
description: 'Version test description.',
|
|
272
|
+
project_id: 1,
|
|
273
|
+
category: 'General',
|
|
274
|
+
version: '1.0.0',
|
|
275
|
+
target_version: '1.1.0',
|
|
276
|
+
fixed_in_version: '1.0.1',
|
|
277
|
+
}, { validate: true });
|
|
278
|
+
|
|
279
|
+
const body = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]!.body as string) as Record<string, unknown>;
|
|
280
|
+
expect(body.version).toEqual({ name: '1.0.0' });
|
|
281
|
+
expect(body.target_version).toEqual({ name: '1.1.0' });
|
|
282
|
+
expect(body.fixed_in_version).toEqual({ name: '1.0.1' });
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('sends steps_to_reproduce and additional_information as plain strings', async () => {
|
|
286
|
+
vi.mocked(fetch).mockResolvedValue(
|
|
287
|
+
makeResponse(201, JSON.stringify({ issue: { id: 302, summary: 'With text fields' } }))
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
await mockServer.callTool('create_issue', {
|
|
291
|
+
summary: 'Text fields test',
|
|
292
|
+
description: 'Description.',
|
|
293
|
+
project_id: 1,
|
|
294
|
+
category: 'General',
|
|
295
|
+
steps_to_reproduce: '1. Open app\n2. Click button',
|
|
296
|
+
additional_information: 'Happens only on Windows',
|
|
297
|
+
}, { validate: true });
|
|
298
|
+
|
|
299
|
+
const body = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]!.body as string) as Record<string, unknown>;
|
|
300
|
+
expect(body.steps_to_reproduce).toBe('1. Open app\n2. Click button');
|
|
301
|
+
expect(body.additional_information).toBe('Happens only on Windows');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('resolves reproducibility to { id } via enum lookup', async () => {
|
|
305
|
+
vi.mocked(fetch).mockResolvedValue(
|
|
306
|
+
makeResponse(201, JSON.stringify({ issue: { id: 303, summary: 'Reproducibility test' } }))
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
await mockServer.callTool('create_issue', {
|
|
310
|
+
summary: 'Repro test',
|
|
311
|
+
description: 'Description.',
|
|
312
|
+
project_id: 1,
|
|
313
|
+
category: 'General',
|
|
314
|
+
reproducibility: 'always',
|
|
315
|
+
}, { validate: true });
|
|
316
|
+
|
|
317
|
+
const body = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]!.body as string) as Record<string, unknown>;
|
|
318
|
+
expect(body.reproducibility).toEqual({ id: 10 });
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('returns error for unknown reproducibility name', async () => {
|
|
322
|
+
const result = await mockServer.callTool('create_issue', {
|
|
323
|
+
summary: 'Repro test',
|
|
324
|
+
description: 'Description.',
|
|
325
|
+
project_id: 1,
|
|
326
|
+
category: 'General',
|
|
327
|
+
reproducibility: 'immer',
|
|
328
|
+
}, { validate: true });
|
|
329
|
+
|
|
330
|
+
expect(result.isError).toBe(true);
|
|
331
|
+
expect(result.content[0]!.text).toContain('immer');
|
|
332
|
+
expect(result.content[0]!.text).toContain('always');
|
|
333
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('returns error for empty reproducibility string', async () => {
|
|
337
|
+
const result = await mockServer.callTool('create_issue', {
|
|
338
|
+
summary: 'Repro test',
|
|
339
|
+
description: 'Description.',
|
|
340
|
+
project_id: 1,
|
|
341
|
+
category: 'General',
|
|
342
|
+
reproducibility: '',
|
|
343
|
+
}, { validate: true });
|
|
344
|
+
|
|
345
|
+
expect(result.isError).toBe(true);
|
|
346
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('sends view_state as { name } object', async () => {
|
|
350
|
+
vi.mocked(fetch).mockResolvedValue(
|
|
351
|
+
makeResponse(201, JSON.stringify({ issue: { id: 304, summary: 'Private issue' } }))
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
await mockServer.callTool('create_issue', {
|
|
355
|
+
summary: 'Private issue',
|
|
356
|
+
description: 'Description.',
|
|
357
|
+
project_id: 1,
|
|
358
|
+
category: 'General',
|
|
359
|
+
view_state: 'private',
|
|
360
|
+
}, { validate: true });
|
|
361
|
+
|
|
362
|
+
const body = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]!.body as string) as Record<string, unknown>;
|
|
363
|
+
expect(body.view_state).toEqual({ name: 'private' });
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('rejects invalid view_state values', async () => {
|
|
367
|
+
const result = await mockServer.callTool('create_issue', {
|
|
368
|
+
summary: 'Test',
|
|
369
|
+
description: 'Description.',
|
|
370
|
+
project_id: 1,
|
|
371
|
+
category: 'General',
|
|
372
|
+
view_state: 'restricted',
|
|
373
|
+
}, { validate: true });
|
|
374
|
+
|
|
375
|
+
expect(result.isError).toBe(true);
|
|
376
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('sends all new optional fields together in a single request', async () => {
|
|
380
|
+
vi.mocked(fetch).mockResolvedValue(
|
|
381
|
+
makeResponse(201, JSON.stringify({ issue: { id: 305, summary: 'Full issue' } }))
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
await mockServer.callTool('create_issue', {
|
|
385
|
+
summary: 'Full issue',
|
|
386
|
+
description: 'Full description.',
|
|
387
|
+
project_id: 1,
|
|
388
|
+
category: 'General',
|
|
389
|
+
version: '2.0.0',
|
|
390
|
+
target_version: '2.1.0',
|
|
391
|
+
fixed_in_version: '2.0.1',
|
|
392
|
+
steps_to_reproduce: 'Step 1',
|
|
393
|
+
additional_information: 'Extra info',
|
|
394
|
+
reproducibility: 'sometimes',
|
|
395
|
+
view_state: 'public',
|
|
396
|
+
}, { validate: true });
|
|
397
|
+
|
|
398
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
399
|
+
const body = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]!.body as string) as Record<string, unknown>;
|
|
400
|
+
expect(body.version).toEqual({ name: '2.0.0' });
|
|
401
|
+
expect(body.target_version).toEqual({ name: '2.1.0' });
|
|
402
|
+
expect(body.fixed_in_version).toEqual({ name: '2.0.1' });
|
|
403
|
+
expect(body.steps_to_reproduce).toBe('Step 1');
|
|
404
|
+
expect(body.additional_information).toBe('Extra info');
|
|
405
|
+
expect(body.reproducibility).toEqual({ id: 30 });
|
|
406
|
+
expect(body.view_state).toEqual({ name: 'public' });
|
|
407
|
+
});
|
|
240
408
|
});
|
|
241
409
|
|
|
242
410
|
// ---------------------------------------------------------------------------
|