@dpesch/mantisbt-mcp-server 1.9.0 → 1.9.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/CHANGELOG.md +5 -1
- package/dist/search/tools.js +2 -1
- package/dist/tools/issues.js +13 -3
- package/dist/tools/projects.js +3 -2
- package/dist/tools/version.js +2 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/tests/search/tools.test.ts +28 -0
- package/tests/tools/string-coercion.test.ts +64 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,11 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
## [
|
|
10
|
+
## [1.9.1] – 2026-03-30
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Boolean parameters (`dry_run`, `highlight`, `check_latest`, `obsolete`, `inherit`) now accept the strings `"true"` and `"false"` in addition to native booleans. MCP clients that serialize all parameters as JSON strings no longer receive error -32602. Note: `z.coerce.boolean()` was intentionally not used — it would silently convert the string `"false"` to `true` via JavaScript's `Boolean()`.
|
|
14
|
+
- `update_issue`: the `fields` parameter now accepts a JSON-encoded string in addition to a plain object. Invalid JSON is caught and surfaced as a Zod validation error instead of an uncaught `SyntaxError`.
|
|
11
15
|
|
|
12
16
|
---
|
|
13
17
|
|
package/dist/search/tools.js
CHANGED
|
@@ -24,6 +24,7 @@ function errorText(msg) {
|
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
25
|
// registerSearchTools
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
27
|
+
const coerceBool = (val) => val === 'true' ? true : val === 'false' ? false : val;
|
|
27
28
|
export function registerSearchTools(server, client, store, embedder) {
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
29
30
|
// search_issues
|
|
@@ -45,7 +46,7 @@ export function registerSearchTools(server, client, store, embedder) {
|
|
|
45
46
|
'When provided, each matching issue is fetched from MantisBT and enriched with the requested fields. ' +
|
|
46
47
|
'The relevance score is always included. Without this parameter only id and score are returned.'),
|
|
47
48
|
highlight: z
|
|
48
|
-
.boolean()
|
|
49
|
+
.preprocess(coerceBool, z.boolean())
|
|
49
50
|
.default(false)
|
|
50
51
|
.describe('If true, adds a "highlights" field per result with query terms bolded (**term**) ' +
|
|
51
52
|
'in the issue summary and a short description snippet. ' +
|
package/dist/tools/issues.js
CHANGED
|
@@ -342,6 +342,7 @@ export function registerIssueTools(server, client, cache) {
|
|
|
342
342
|
// ---------------------------------------------------------------------------
|
|
343
343
|
// update_issue
|
|
344
344
|
// ---------------------------------------------------------------------------
|
|
345
|
+
const coerceBool = (val) => val === 'true' ? true : val === 'false' ? false : val;
|
|
345
346
|
// MantisBT reference shape: at least one of id or name must be provided
|
|
346
347
|
const ref = z.object({ id: z.number().optional(), name: z.string().optional() })
|
|
347
348
|
.refine(o => o.id !== undefined || o.name !== undefined, { message: "At least one of 'id' or 'name' must be provided" });
|
|
@@ -369,8 +370,17 @@ The "fields" object accepts any combination of:
|
|
|
369
370
|
Important: when resolving an issue, always set BOTH status and resolution to avoid leaving resolution as "open".`,
|
|
370
371
|
inputSchema: z.object({
|
|
371
372
|
id: z.coerce.number().int().positive().describe('Numeric issue ID to update'),
|
|
372
|
-
dry_run: z.boolean().optional().describe('If true, return the patch payload that would be sent without actually updating the issue. Useful for previewing changes before committing them.'),
|
|
373
|
-
fields: z.
|
|
373
|
+
dry_run: z.preprocess(coerceBool, z.boolean().optional()).describe('If true, return the patch payload that would be sent without actually updating the issue. Useful for previewing changes before committing them.'),
|
|
374
|
+
fields: z.preprocess((v) => {
|
|
375
|
+
if (typeof v !== 'string')
|
|
376
|
+
return v;
|
|
377
|
+
try {
|
|
378
|
+
return JSON.parse(v);
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return v;
|
|
382
|
+
}
|
|
383
|
+
}, z.object({
|
|
374
384
|
summary: z.string().optional(),
|
|
375
385
|
description: z.string().optional(),
|
|
376
386
|
steps_to_reproduce: z.string().optional(),
|
|
@@ -386,7 +396,7 @@ Important: when resolving an issue, always set BOTH status and resolution to avo
|
|
|
386
396
|
target_version: ref.optional(),
|
|
387
397
|
fixed_in_version: ref.optional(),
|
|
388
398
|
view_state: ref.optional(),
|
|
389
|
-
}).strict().describe('Fields to update (partial update — only provided fields are changed; unknown keys are rejected)'),
|
|
399
|
+
}).strict().describe('Fields to update (partial update — only provided fields are changed; unknown keys are rejected)')),
|
|
390
400
|
}),
|
|
391
401
|
annotations: {
|
|
392
402
|
readOnlyHint: false,
|
package/dist/tools/projects.js
CHANGED
|
@@ -8,6 +8,7 @@ function errorText(msg) {
|
|
|
8
8
|
const hint = vh?.getUpdateHint();
|
|
9
9
|
return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
|
|
10
10
|
}
|
|
11
|
+
const coerceBool = (val) => val === 'true' ? true : val === 'false' ? false : val;
|
|
11
12
|
export function registerProjectTools(server, client, cache) {
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
13
14
|
// list_projects
|
|
@@ -72,8 +73,8 @@ export function registerProjectTools(server, client, cache) {
|
|
|
72
73
|
description: 'List all versions defined for a MantisBT project.',
|
|
73
74
|
inputSchema: z.object({
|
|
74
75
|
project_id: z.coerce.number().int().positive().describe('Numeric project ID'),
|
|
75
|
-
obsolete: z.boolean().default(false).describe('Include obsolete (deprecated) versions (default: false)'),
|
|
76
|
-
inherit: z.boolean().default(false).describe('Include versions inherited from parent projects (default: false)'),
|
|
76
|
+
obsolete: z.preprocess(coerceBool, z.boolean()).default(false).describe('Include obsolete (deprecated) versions (default: false)'),
|
|
77
|
+
inherit: z.preprocess(coerceBool, z.boolean()).default(false).describe('Include versions inherited from parent projects (default: false)'),
|
|
77
78
|
}),
|
|
78
79
|
annotations: {
|
|
79
80
|
readOnlyHint: true,
|
package/dist/tools/version.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { parseVersion, compareVersions } from '../version-hint.js';
|
|
3
|
+
const coerceBool = (val) => val === 'true' ? true : val === 'false' ? false : val;
|
|
3
4
|
export function registerVersionTools(server, client, versionHint, mcpVersion) {
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
5
6
|
// get_mcp_version
|
|
@@ -26,7 +27,7 @@ export function registerVersionTools(server, client, versionHint, mcpVersion) {
|
|
|
26
27
|
The version is read from the X-Mantis-Version response header sent by every API call.
|
|
27
28
|
The GitHub comparison requires an outbound HTTPS request to the GitHub API.`,
|
|
28
29
|
inputSchema: z.object({
|
|
29
|
-
check_latest: z.boolean().default(true).describe('Whether to fetch the latest release from GitHub and compare (default: true)'),
|
|
30
|
+
check_latest: z.preprocess(coerceBool, z.boolean()).default(true).describe('Whether to fetch the latest release from GitHub and compare (default: true)'),
|
|
30
31
|
}),
|
|
31
32
|
annotations: {
|
|
32
33
|
readOnlyHint: true,
|
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.9.
|
|
6
|
+
"version": "1.9.1",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"registryType": "npm",
|
|
10
10
|
"identifier": "@dpesch/mantisbt-mcp-server",
|
|
11
|
-
"version": "1.9.
|
|
11
|
+
"version": "1.9.1",
|
|
12
12
|
"runtimeHint": "npx",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|
|
@@ -630,3 +630,31 @@ describe('search_issues – highlight: true combined with date filter', () => {
|
|
|
630
630
|
expect(parsed[0]!.id).toBe(1);
|
|
631
631
|
});
|
|
632
632
|
});
|
|
633
|
+
|
|
634
|
+
// ---------------------------------------------------------------------------
|
|
635
|
+
// string-coercion – highlight as string
|
|
636
|
+
// ---------------------------------------------------------------------------
|
|
637
|
+
|
|
638
|
+
describe('string-coercion – search_issues highlight as string', () => {
|
|
639
|
+
it('accepts highlight "true" as boolean true', async () => {
|
|
640
|
+
const store = makeMockStore({
|
|
641
|
+
items: [{ id: 1, score: 0.9 }],
|
|
642
|
+
});
|
|
643
|
+
vi.mocked(store.getItem).mockResolvedValue({
|
|
644
|
+
id: 1,
|
|
645
|
+
vector: [],
|
|
646
|
+
metadata: { summary: 'Login error occurred', description: 'The login fails.' },
|
|
647
|
+
});
|
|
648
|
+
registerSearchTools(mockServer as never, client, store, embedder);
|
|
649
|
+
|
|
650
|
+
const result = await mockServer.callTool(
|
|
651
|
+
'search_issues',
|
|
652
|
+
{ query: 'login error', top_n: 1, highlight: 'true' },
|
|
653
|
+
{ validate: true },
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
expect(result.isError).toBeUndefined();
|
|
657
|
+
const parsed = JSON.parse(result.content[0]!.text) as Array<Record<string, unknown>>;
|
|
658
|
+
expect(parsed[0]).toHaveProperty('highlights');
|
|
659
|
+
});
|
|
660
|
+
});
|
|
@@ -19,6 +19,8 @@ import { registerMonitorTools } from '../../src/tools/monitors.js';
|
|
|
19
19
|
import { registerRelationshipTools } from '../../src/tools/relationships.js';
|
|
20
20
|
import { registerTagTools } from '../../src/tools/tags.js';
|
|
21
21
|
import { registerProjectTools } from '../../src/tools/projects.js';
|
|
22
|
+
import { registerVersionTools } from '../../src/tools/version.js';
|
|
23
|
+
import { VersionHintService } from '../../src/version-hint.js';
|
|
22
24
|
import { MockMcpServer, makeResponse } from '../helpers/mock-server.js';
|
|
23
25
|
|
|
24
26
|
// ---------------------------------------------------------------------------
|
|
@@ -39,6 +41,7 @@ beforeEach(() => {
|
|
|
39
41
|
registerRelationshipTools(mockServer as never, client);
|
|
40
42
|
registerTagTools(mockServer as never, client);
|
|
41
43
|
registerProjectTools(mockServer as never, client);
|
|
44
|
+
registerVersionTools(mockServer as never, client, new VersionHintService(), '0.0.0-test');
|
|
42
45
|
vi.stubGlobal('fetch', vi.fn());
|
|
43
46
|
});
|
|
44
47
|
|
|
@@ -251,3 +254,64 @@ describe('string-coercion – get_project_versions', () => {
|
|
|
251
254
|
expect(result.isError).toBeUndefined();
|
|
252
255
|
});
|
|
253
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
|
+
});
|