@dpesch/mantisbt-mcp-server 1.9.0 → 1.10.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 +13 -1
- package/README.de.md +1 -0
- package/README.md +1 -0
- package/dist/client.js +9 -0
- package/dist/search/tools.js +11 -6
- package/dist/tools/issues.js +52 -18
- package/dist/tools/notes.js +15 -4
- package/dist/tools/projects.js +3 -2
- package/dist/tools/version.js +2 -1
- package/docs/cookbook.de.md +78 -15
- package/docs/cookbook.md +78 -15
- package/package.json +1 -1
- package/server.json +2 -2
- package/tests/client.test.ts +40 -1
- package/tests/search/tools.test.ts +34 -5
- package/tests/tools/string-coercion.test.ts +64 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,19 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
## [
|
|
10
|
+
## [1.10.0] – 2026-04-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- All issue and note responses now include a `view_url` field with the absolute MantisBT web URL for the respective item. The URL is built by the MCP server from `MANTIS_BASE_URL` — it is not provided by the MantisBT REST API. Pattern: `{baseUrl}/view.php?id={issueId}` for issues; `{baseUrl}/view.php?id={issueId}#bugnoteN` for notes. The field is present in all tools that return issues or notes: `get_issue`, `get_issues`, `list_issues`, `create_issue`, `update_issue`, `list_notes`, `add_note`, and `search_issues`. It is always included regardless of the `select` parameter.
|
|
14
|
+
- `MantisClient` gains a new public `getBaseUrl()` method (returns the resolved, normalised base URL).
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## [1.9.1] – 2026-03-30
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- 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()`.
|
|
22
|
+
- `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
23
|
|
|
12
24
|
---
|
|
13
25
|
|
package/README.de.md
CHANGED
|
@@ -87,6 +87,7 @@ npm run build
|
|
|
87
87
|
| Tool | Beschreibung |
|
|
88
88
|
|---|---|
|
|
89
89
|
| `get_issue` | Ein Issue anhand seiner ID abrufen |
|
|
90
|
+
| `get_issues` | Mehrere Issues per ID in einem Aufruf abrufen (1–50 IDs); nicht zugängliche IDs liefern `null` an ihrer Position, statt den gesamten Aufruf abzubrechen |
|
|
90
91
|
| `list_issues` | Issues nach Projekt, Status, Autor u.v.m. filtern; optionales `select` für Feldprojektion und `status` für clientseitige Statusfilterung — kanonische englische Statusnamen (z.B. `"new"`, `"resolved"`) werden per ID abgeglichen und funktionieren damit sprachunabhängig auf lokalisierten Installationen |
|
|
91
92
|
| `create_issue` | Neues Issue anlegen; `severity` und `priority` müssen kanonische englische Namen sein (z.B. `minor`, `major`, `normal`, `high`) — `get_issue_enums` aufrufen, um alle gültigen Werte und deren lokalisierte Bezeichnungen zu sehen; optionaler `handler`-Parameter akzeptiert einen Benutzernamen als Alternative zu `handler_id` (wird gegen die Projektmitglieder aufgelöst) |
|
|
92
93
|
| `update_issue` | Bestehendes Issue bearbeiten; Enum-Felder (`status`, `priority`, `severity`, `resolution`, `reproducibility`) akzeptieren kanonische englische Namen, lokalisierte Namen oder numerische IDs — der Server löst Namen automatisch zu IDs auf |
|
package/README.md
CHANGED
|
@@ -87,6 +87,7 @@ npm run build
|
|
|
87
87
|
| Tool | Description |
|
|
88
88
|
|---|---|
|
|
89
89
|
| `get_issue` | Retrieve an issue by its numeric ID |
|
|
90
|
+
| `get_issues` | Retrieve multiple issues by ID in one call (1–50 IDs); missing or inaccessible IDs return `null` at their position instead of failing the call |
|
|
90
91
|
| `list_issues` | Filter issues by project, status, author, and more; optional `select` for field projection and `status` for client-side status filtering — canonical English status names (e.g. `"new"`, `"resolved"`) are matched by ID, making the filter language-independent on localized installations |
|
|
91
92
|
| `create_issue` | Create a new issue; `severity` and `priority` must be canonical English names (e.g. `minor`, `major`, `normal`, `high`) — call `get_issue_enums` to see all valid values and their localized labels; optional `handler` parameter accepts a username as alternative to `handler_id` (resolved against project members) |
|
|
92
93
|
| `update_issue` | Update an existing issue; enum fields (`status`, `priority`, `severity`, `resolution`, `reproducibility`) accept canonical English names, localized names, or numeric IDs — the server resolves names to IDs automatically |
|
package/dist/client.js
CHANGED
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
export function normalizeBaseUrl(url) {
|
|
11
11
|
return url.replace(/\/api\/rest\/?$/, '').replace(/\/$/, '');
|
|
12
12
|
}
|
|
13
|
+
export function buildIssueViewUrl(baseUrl, issueId) {
|
|
14
|
+
return `${baseUrl}/view.php?id=${issueId}`;
|
|
15
|
+
}
|
|
16
|
+
export function buildNoteViewUrl(baseUrl, issueId, noteId) {
|
|
17
|
+
return `${baseUrl}/view.php?id=${issueId}#bugnote${noteId}`;
|
|
18
|
+
}
|
|
13
19
|
// ---------------------------------------------------------------------------
|
|
14
20
|
// MantisApiError
|
|
15
21
|
// ---------------------------------------------------------------------------
|
|
@@ -45,6 +51,9 @@ export class MantisClient {
|
|
|
45
51
|
}
|
|
46
52
|
return this.resolvedCredentials;
|
|
47
53
|
}
|
|
54
|
+
async getBaseUrl() {
|
|
55
|
+
return (await this.getCredentials()).baseUrl;
|
|
56
|
+
}
|
|
48
57
|
async buildUrl(path, params) {
|
|
49
58
|
const { baseUrl } = await this.getCredentials();
|
|
50
59
|
const url = new URL(`${baseUrl}/api/rest/${path}`);
|
package/dist/search/tools.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { buildIssueViewUrl } from '../client.js';
|
|
2
3
|
import { SearchSyncService } from './sync.js';
|
|
3
4
|
import { getVersionHint } from '../version-hint.js';
|
|
4
5
|
import { dateFilterSchema, matchesDateFilter, hasDateFilter } from '../date-filter.js';
|
|
@@ -24,6 +25,7 @@ function errorText(msg) {
|
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
25
26
|
// registerSearchTools
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
28
|
+
const coerceBool = (val) => val === 'true' ? true : val === 'false' ? false : val;
|
|
27
29
|
export function registerSearchTools(server, client, store, embedder) {
|
|
28
30
|
// ---------------------------------------------------------------------------
|
|
29
31
|
// search_issues
|
|
@@ -45,7 +47,7 @@ export function registerSearchTools(server, client, store, embedder) {
|
|
|
45
47
|
'When provided, each matching issue is fetched from MantisBT and enriched with the requested fields. ' +
|
|
46
48
|
'The relevance score is always included. Without this parameter only id and score are returned.'),
|
|
47
49
|
highlight: z
|
|
48
|
-
.boolean()
|
|
50
|
+
.preprocess(coerceBool, z.boolean())
|
|
49
51
|
.default(false)
|
|
50
52
|
.describe('If true, adds a "highlights" field per result with query terms bolded (**term**) ' +
|
|
51
53
|
'in the issue summary and a short description snippet. ' +
|
|
@@ -74,20 +76,23 @@ export function registerSearchTools(server, client, store, embedder) {
|
|
|
74
76
|
const dateFilter = { updated_after, updated_before, created_after, created_before };
|
|
75
77
|
const filterActive = hasDateFilter(dateFilter);
|
|
76
78
|
const terms = highlight ? extractTerms(query) : [];
|
|
77
|
-
const queryVector = await
|
|
79
|
+
const [queryVector, baseUrl] = await Promise.all([
|
|
80
|
+
embedder.embed(query),
|
|
81
|
+
client.getBaseUrl(),
|
|
82
|
+
]);
|
|
78
83
|
const results = await store.search(queryVector, top_n);
|
|
79
84
|
if (!select) {
|
|
80
85
|
// For filtering or highlighting we need store metadata per result
|
|
81
86
|
if (!filterActive && !terms.length) {
|
|
82
87
|
return {
|
|
83
|
-
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
|
88
|
+
content: [{ type: 'text', text: JSON.stringify(results.map(({ id, score }) => ({ id, score, view_url: buildIssueViewUrl(baseUrl, id) })), null, 2) }],
|
|
84
89
|
};
|
|
85
90
|
}
|
|
86
91
|
const filtered = await Promise.all(results.map(async ({ id, score }) => {
|
|
87
92
|
const item = await store.getItem(id);
|
|
88
93
|
if (filterActive && !matchesDateFilter(item?.metadata ?? {}, dateFilter))
|
|
89
94
|
return null;
|
|
90
|
-
const result = { id, score };
|
|
95
|
+
const result = { id, score, view_url: buildIssueViewUrl(baseUrl, id) };
|
|
91
96
|
if (terms.length > 0 && item) {
|
|
92
97
|
const h = buildHighlights(item.metadata.summary, item.metadata.description, terms);
|
|
93
98
|
if (h)
|
|
@@ -107,7 +112,7 @@ export function registerSearchTools(server, client, store, embedder) {
|
|
|
107
112
|
if (filterActive && !matchesDateFilter(issue, dateFilter)) {
|
|
108
113
|
return null;
|
|
109
114
|
}
|
|
110
|
-
const projected = { id, score };
|
|
115
|
+
const projected = { id, score, view_url: buildIssueViewUrl(baseUrl, id) };
|
|
111
116
|
for (const field of fields) {
|
|
112
117
|
if (field !== 'id' && field in issue) {
|
|
113
118
|
projected[field] = issue[field];
|
|
@@ -123,7 +128,7 @@ export function registerSearchTools(server, client, store, embedder) {
|
|
|
123
128
|
return projected;
|
|
124
129
|
}
|
|
125
130
|
catch {
|
|
126
|
-
const result = { id, score };
|
|
131
|
+
const result = { id, score, view_url: buildIssueViewUrl(baseUrl, id) };
|
|
127
132
|
if (terms.length > 0) {
|
|
128
133
|
const item = await store.getItem(id);
|
|
129
134
|
if (item) {
|
package/dist/tools/issues.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { buildIssueViewUrl, buildNoteViewUrl } from '../client.js';
|
|
2
3
|
import { getVersionHint } from '../version-hint.js';
|
|
3
4
|
import { MANTIS_CANONICAL_ENUM_NAMES, MANTIS_RESOLVED_STATUS_ID, resolveEnumId } from '../constants.js';
|
|
4
5
|
import { dateFilterSchema, matchesDateFilter, hasDateFilter } from '../date-filter.js';
|
|
@@ -9,6 +10,16 @@ function errorText(msg) {
|
|
|
9
10
|
const hint = vh?.getUpdateHint();
|
|
10
11
|
return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
|
|
11
12
|
}
|
|
13
|
+
function enrichIssue(issue, baseUrl) {
|
|
14
|
+
return {
|
|
15
|
+
...issue,
|
|
16
|
+
view_url: buildIssueViewUrl(baseUrl, issue.id),
|
|
17
|
+
notes: issue.notes?.map(note => ({
|
|
18
|
+
...note,
|
|
19
|
+
view_url: buildNoteViewUrl(baseUrl, issue.id, note.id),
|
|
20
|
+
})),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
12
23
|
const GET_ISSUES_CONCURRENCY = 5;
|
|
13
24
|
// Worker-pool: runs `fn` over all `items` with at most `concurrency` in-flight at once.
|
|
14
25
|
// nextIndex is only incremented inside microtasks, so the ++ is safe without a lock.
|
|
@@ -63,8 +74,9 @@ export function registerIssueTools(server, client, cache) {
|
|
|
63
74
|
try {
|
|
64
75
|
const result = await client.get(`issues/${id}`);
|
|
65
76
|
const issue = result.issues?.[0] ?? result;
|
|
77
|
+
const baseUrl = await client.getBaseUrl();
|
|
66
78
|
return {
|
|
67
|
-
content: [{ type: 'text', text: JSON.stringify(issue, null, 2) }],
|
|
79
|
+
content: [{ type: 'text', text: JSON.stringify(enrichIssue(issue, baseUrl), null, 2) }],
|
|
68
80
|
};
|
|
69
81
|
}
|
|
70
82
|
catch (error) {
|
|
@@ -95,15 +107,19 @@ export function registerIssueTools(server, client, cache) {
|
|
|
95
107
|
idempotentHint: true,
|
|
96
108
|
},
|
|
97
109
|
}, async ({ ids }) => {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
110
|
+
const [rawResults, baseUrl] = await Promise.all([
|
|
111
|
+
runWithConcurrency(ids, GET_ISSUES_CONCURRENCY, async (id) => {
|
|
112
|
+
try {
|
|
113
|
+
const result = await client.get(`issues/${id}`);
|
|
114
|
+
return result.issues?.[0] ?? result;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}),
|
|
120
|
+
client.getBaseUrl(),
|
|
121
|
+
]);
|
|
122
|
+
const results = rawResults.map(issue => issue !== null ? enrichIssue(issue, baseUrl) : null);
|
|
107
123
|
const found = results.filter((r) => r !== null).length;
|
|
108
124
|
return {
|
|
109
125
|
content: [{
|
|
@@ -149,11 +165,13 @@ export function registerIssueTools(server, client, cache) {
|
|
|
149
165
|
};
|
|
150
166
|
const dateFilter = { updated_after, updated_before, created_after, created_before };
|
|
151
167
|
const needsClientFilter = status !== undefined || assigned_to !== undefined || reporter_id !== undefined || hasDateFilter(dateFilter);
|
|
168
|
+
const baseUrl = await client.getBaseUrl();
|
|
152
169
|
if (!needsClientFilter) {
|
|
153
170
|
// No client-side filtering — single API call, pass pagination as-is
|
|
154
171
|
const result = await client.get('issues', { ...baseParams, page, page_size });
|
|
172
|
+
const enriched = { ...result, issues: result.issues?.map(i => enrichIssue(i, baseUrl)) };
|
|
155
173
|
return {
|
|
156
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
174
|
+
content: [{ type: 'text', text: JSON.stringify(enriched, null, 2) }],
|
|
157
175
|
};
|
|
158
176
|
}
|
|
159
177
|
// Client-side filtering active: scan multiple API pages until we have
|
|
@@ -214,10 +232,11 @@ export function registerIssueTools(server, client, cache) {
|
|
|
214
232
|
serverPage++;
|
|
215
233
|
}
|
|
216
234
|
const start = (page - 1) * page_size;
|
|
235
|
+
const pageIssues = matching.slice(start, start + page_size).map(i => enrichIssue(i, baseUrl));
|
|
217
236
|
return {
|
|
218
237
|
content: [{
|
|
219
238
|
type: 'text',
|
|
220
|
-
text: JSON.stringify({ issues:
|
|
239
|
+
text: JSON.stringify({ issues: pageIssues }, null, 2),
|
|
221
240
|
}],
|
|
222
241
|
};
|
|
223
242
|
}
|
|
@@ -330,8 +349,9 @@ export function registerIssueTools(server, client, cache) {
|
|
|
330
349
|
// unable to fetch details — return minimal object
|
|
331
350
|
}
|
|
332
351
|
}
|
|
352
|
+
const baseUrl = await client.getBaseUrl();
|
|
333
353
|
return {
|
|
334
|
-
content: [{ type: 'text', text: JSON.stringify(issue, null, 2) }],
|
|
354
|
+
content: [{ type: 'text', text: JSON.stringify(enrichIssue(issue, baseUrl), null, 2) }],
|
|
335
355
|
};
|
|
336
356
|
}
|
|
337
357
|
catch (error) {
|
|
@@ -342,6 +362,7 @@ export function registerIssueTools(server, client, cache) {
|
|
|
342
362
|
// ---------------------------------------------------------------------------
|
|
343
363
|
// update_issue
|
|
344
364
|
// ---------------------------------------------------------------------------
|
|
365
|
+
const coerceBool = (val) => val === 'true' ? true : val === 'false' ? false : val;
|
|
345
366
|
// MantisBT reference shape: at least one of id or name must be provided
|
|
346
367
|
const ref = z.object({ id: z.number().optional(), name: z.string().optional() })
|
|
347
368
|
.refine(o => o.id !== undefined || o.name !== undefined, { message: "At least one of 'id' or 'name' must be provided" });
|
|
@@ -369,8 +390,17 @@ The "fields" object accepts any combination of:
|
|
|
369
390
|
Important: when resolving an issue, always set BOTH status and resolution to avoid leaving resolution as "open".`,
|
|
370
391
|
inputSchema: z.object({
|
|
371
392
|
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.
|
|
393
|
+
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.'),
|
|
394
|
+
fields: z.preprocess((v) => {
|
|
395
|
+
if (typeof v !== 'string')
|
|
396
|
+
return v;
|
|
397
|
+
try {
|
|
398
|
+
return JSON.parse(v);
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
return v;
|
|
402
|
+
}
|
|
403
|
+
}, z.object({
|
|
374
404
|
summary: z.string().optional(),
|
|
375
405
|
description: z.string().optional(),
|
|
376
406
|
steps_to_reproduce: z.string().optional(),
|
|
@@ -386,7 +416,7 @@ Important: when resolving an issue, always set BOTH status and resolution to avo
|
|
|
386
416
|
target_version: ref.optional(),
|
|
387
417
|
fixed_in_version: ref.optional(),
|
|
388
418
|
view_state: ref.optional(),
|
|
389
|
-
}).strict().describe('Fields to update (partial update — only provided fields are changed; unknown keys are rejected)'),
|
|
419
|
+
}).strict().describe('Fields to update (partial update — only provided fields are changed; unknown keys are rejected)')),
|
|
390
420
|
}),
|
|
391
421
|
annotations: {
|
|
392
422
|
readOnlyHint: false,
|
|
@@ -409,9 +439,13 @@ Important: when resolving an issue, always set BOTH status and resolution to avo
|
|
|
409
439
|
patch[field] = resolved;
|
|
410
440
|
}
|
|
411
441
|
}
|
|
412
|
-
const result = await
|
|
442
|
+
const [result, baseUrl] = await Promise.all([
|
|
443
|
+
client.patch(`issues/${id}`, patch),
|
|
444
|
+
client.getBaseUrl(),
|
|
445
|
+
]);
|
|
446
|
+
const issue = result.issue ?? result;
|
|
413
447
|
return {
|
|
414
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
448
|
+
content: [{ type: 'text', text: JSON.stringify(enrichIssue(issue, baseUrl), null, 2) }],
|
|
415
449
|
};
|
|
416
450
|
}
|
|
417
451
|
catch (error) {
|
package/dist/tools/notes.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { buildNoteViewUrl } from '../client.js';
|
|
2
3
|
import { getVersionHint } from '../version-hint.js';
|
|
3
4
|
function errorText(msg) {
|
|
4
5
|
const vh = getVersionHint();
|
|
@@ -23,8 +24,14 @@ export function registerNoteTools(server, client) {
|
|
|
23
24
|
},
|
|
24
25
|
}, async ({ issue_id }) => {
|
|
25
26
|
try {
|
|
26
|
-
const result = await
|
|
27
|
-
|
|
27
|
+
const [result, baseUrl] = await Promise.all([
|
|
28
|
+
client.get(`issues/${issue_id}`),
|
|
29
|
+
client.getBaseUrl(),
|
|
30
|
+
]);
|
|
31
|
+
const notes = (result.issues?.[0]?.notes ?? []).map(note => ({
|
|
32
|
+
...note,
|
|
33
|
+
view_url: buildNoteViewUrl(baseUrl, issue_id, note.id),
|
|
34
|
+
}));
|
|
28
35
|
return {
|
|
29
36
|
content: [{ type: 'text', text: JSON.stringify(notes, null, 2) }],
|
|
30
37
|
};
|
|
@@ -56,9 +63,13 @@ export function registerNoteTools(server, client) {
|
|
|
56
63
|
text,
|
|
57
64
|
view_state: { name: view_state },
|
|
58
65
|
};
|
|
59
|
-
const result = await
|
|
66
|
+
const [result, baseUrl] = await Promise.all([
|
|
67
|
+
client.post(`issues/${issue_id}/notes`, body),
|
|
68
|
+
client.getBaseUrl(),
|
|
69
|
+
]);
|
|
70
|
+
const note = result.note ?? result;
|
|
60
71
|
return {
|
|
61
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
72
|
+
content: [{ type: 'text', text: JSON.stringify({ ...note, view_url: buildNoteViewUrl(baseUrl, issue_id, note.id) }, null, 2) }],
|
|
62
73
|
};
|
|
63
74
|
}
|
|
64
75
|
catch (error) {
|
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/docs/cookbook.de.md
CHANGED
|
@@ -10,6 +10,7 @@ Tool-orientierte Rezepte für den MantisBT MCP Server — jedes Rezept zeigt gen
|
|
|
10
10
|
- [Gültige Feldnamen für `select` ermitteln](#gültige-feldnamen-für-select-ermitteln)
|
|
11
11
|
- [Issues](#issues)
|
|
12
12
|
- [Einzelnes Issue abrufen](#einzelnes-issue-abrufen)
|
|
13
|
+
- [Mehrere Issues in einem Aufruf abrufen](#mehrere-issues-in-einem-aufruf-abrufen)
|
|
13
14
|
- [Issues auflisten (paginiert)](#issues-auflisten-paginiert)
|
|
14
15
|
- [Antwortgröße mit `select` reduzieren](#antwortgröße-mit-select-reduzieren)
|
|
15
16
|
- [Nach Status filtern](#nach-status-filtern)
|
|
@@ -222,12 +223,59 @@ Ruft ein einzelnes Issue anhand seiner numerischen ID ab, inklusive Notizen, Anh
|
|
|
222
223
|
"tags": [],
|
|
223
224
|
"notes": [],
|
|
224
225
|
"attachments": [],
|
|
225
|
-
"relationships": []
|
|
226
|
+
"relationships": [],
|
|
227
|
+
"view_url": "https://mantis.example.com/view.php?id=1042"
|
|
226
228
|
}
|
|
227
229
|
```
|
|
228
230
|
|
|
229
231
|
---
|
|
230
232
|
|
|
233
|
+
### Mehrere Issues in einem Aufruf abrufen
|
|
234
|
+
|
|
235
|
+
Ruft bis zu 50 Issues in einem einzigen MCP-Aufruf ab. Die Anfragen laufen parallel (max. 5 gleichzeitig). Nicht zugängliche IDs liefern `null` an ihrer Position — der Aufruf schlägt nie wegen einzelner fehlender IDs fehl.
|
|
236
|
+
|
|
237
|
+
**Tool:** `get_issues`
|
|
238
|
+
|
|
239
|
+
**Parameter:**
|
|
240
|
+
- `ids` — Array numerischer Issue-IDs (1–50)
|
|
241
|
+
|
|
242
|
+
**Request:**
|
|
243
|
+
|
|
244
|
+
```json
|
|
245
|
+
{
|
|
246
|
+
"ids": [1042, 1041, 9999]
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Response:**
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
{
|
|
254
|
+
"issues": [
|
|
255
|
+
{
|
|
256
|
+
"id": 1042,
|
|
257
|
+
"summary": "Login-Button auf mobilem Safari reagiert nicht",
|
|
258
|
+
"status": { "id": 50, "name": "assigned" },
|
|
259
|
+
"view_url": "https://mantis.example.com/view.php?id=1042"
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"id": 1041,
|
|
263
|
+
"summary": "Checkout-Gesamtbetrag wird falsch gerundet",
|
|
264
|
+
"status": { "id": 40, "name": "confirmed" },
|
|
265
|
+
"view_url": "https://mantis.example.com/view.php?id=1041"
|
|
266
|
+
},
|
|
267
|
+
null
|
|
268
|
+
],
|
|
269
|
+
"requested": 3,
|
|
270
|
+
"found": 2,
|
|
271
|
+
"failed": 1
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
> **Hinweis:** `null`-Einträge zeigen IDs an, die nicht gefunden oder nicht zugänglich waren. `failed` gibt an, wie viele IDs nicht abgerufen werden konnten.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
231
279
|
### Issues auflisten (paginiert)
|
|
232
280
|
|
|
233
281
|
Gibt eine paginierte Liste von Issues zurück, optional auf ein Projekt beschränkt.
|
|
@@ -258,13 +306,15 @@ Gibt eine paginierte Liste von Issues zurück, optional auf ein Projekt beschrä
|
|
|
258
306
|
"id": 1042,
|
|
259
307
|
"summary": "Login button unresponsive on mobile Safari",
|
|
260
308
|
"status": { "id": 50, "name": "assigned" },
|
|
261
|
-
"handler": { "id": 7, "name": "jdoe" }
|
|
309
|
+
"handler": { "id": 7, "name": "jdoe" },
|
|
310
|
+
"view_url": "https://mantis.example.com/view.php?id=1042"
|
|
262
311
|
},
|
|
263
312
|
{
|
|
264
313
|
"id": 1041,
|
|
265
314
|
"summary": "Checkout total rounds incorrectly",
|
|
266
315
|
"status": { "id": 40, "name": "confirmed" },
|
|
267
|
-
"handler": { "id": 4, "name": "jsmith" }
|
|
316
|
+
"handler": { "id": 4, "name": "jsmith" },
|
|
317
|
+
"view_url": "https://mantis.example.com/view.php?id=1041"
|
|
268
318
|
}
|
|
269
319
|
// ...
|
|
270
320
|
]
|
|
@@ -301,7 +351,8 @@ Eine kommagetrennte Liste von Feldnamen übergeben, um nur die benötigten Felde
|
|
|
301
351
|
"id": 1042,
|
|
302
352
|
"summary": "Login button unresponsive on mobile Safari",
|
|
303
353
|
"status": { "id": 50, "name": "assigned" },
|
|
304
|
-
"handler": { "id": 7, "name": "jdoe" }
|
|
354
|
+
"handler": { "id": 7, "name": "jdoe" },
|
|
355
|
+
"view_url": "https://mantis.example.com/view.php?id=1042"
|
|
305
356
|
}
|
|
306
357
|
// ...
|
|
307
358
|
]
|
|
@@ -310,6 +361,8 @@ Eine kommagetrennte Liste von Feldnamen übergeben, um nur die benötigten Felde
|
|
|
310
361
|
|
|
311
362
|
> **Hinweis:** Mit `get_issue_fields()` lassen sich alle verfügbaren Feldnamen anzeigen.
|
|
312
363
|
|
|
364
|
+
> **Hinweis:** `view_url` ist in allen Issue-Responses immer vorhanden — es wird vom MCP-Server injiziert und wird durch den `select`-Parameter nicht beeinflusst.
|
|
365
|
+
|
|
313
366
|
---
|
|
314
367
|
|
|
315
368
|
### Nach Status filtern
|
|
@@ -496,7 +549,8 @@ Legt ein neues Issue in MantisBT an.
|
|
|
496
549
|
"tags": [],
|
|
497
550
|
"notes": [],
|
|
498
551
|
"attachments": [],
|
|
499
|
-
"relationships": []
|
|
552
|
+
"relationships": [],
|
|
553
|
+
"view_url": "https://mantis.example.com/view.php?id=1042"
|
|
500
554
|
}
|
|
501
555
|
```
|
|
502
556
|
|
|
@@ -543,7 +597,8 @@ Löst ein Issue auf und schließt es. **Immer beide Felder** `status` und `resol
|
|
|
543
597
|
"summary": "Login-Button auf mobilem Safari reagiert nicht",
|
|
544
598
|
"status": { "id": 80, "name": "resolved" },
|
|
545
599
|
"resolution": { "id": 20, "name": "fixed" },
|
|
546
|
-
"updated_at": "2024-11-06T10:30:00+00:00"
|
|
600
|
+
"updated_at": "2024-11-06T10:30:00+00:00",
|
|
601
|
+
"view_url": "https://mantis.example.com/view.php?id=1042"
|
|
547
602
|
}
|
|
548
603
|
```
|
|
549
604
|
|
|
@@ -580,7 +635,8 @@ Löst ein Issue auf und schließt es. **Immer beide Felder** `status` und `resol
|
|
|
580
635
|
"summary": "Login-Button auf mobilem Safari reagiert nicht",
|
|
581
636
|
"status": { "id": 50, "name": "assigned" },
|
|
582
637
|
"handler": { "id": 7, "name": "jdoe" },
|
|
583
|
-
"updated_at": "2024-11-06T11:00:00+00:00"
|
|
638
|
+
"updated_at": "2024-11-06T11:00:00+00:00",
|
|
639
|
+
"view_url": "https://mantis.example.com/view.php?id=1042"
|
|
584
640
|
}
|
|
585
641
|
```
|
|
586
642
|
|
|
@@ -614,7 +670,8 @@ Setzt das Feld `fixed_in_version` eines Issues.
|
|
|
614
670
|
"id": 1042,
|
|
615
671
|
"summary": "Login-Button auf mobilem Safari reagiert nicht",
|
|
616
672
|
"fixed_in_version": { "name": "2.1.0" },
|
|
617
|
-
"updated_at": "2024-11-06T11:15:00+00:00"
|
|
673
|
+
"updated_at": "2024-11-06T11:15:00+00:00",
|
|
674
|
+
"view_url": "https://mantis.example.com/view.php?id=1042"
|
|
618
675
|
}
|
|
619
676
|
```
|
|
620
677
|
|
|
@@ -652,7 +709,8 @@ Fügt eine öffentlich sichtbare Notiz zu einem Issue hinzu.
|
|
|
652
709
|
"reporter": { "id": 7, "name": "jdoe" },
|
|
653
710
|
"text": "In Version 2.0.3 reproduziert. Ursache in der Auth-Middleware identifiziert.",
|
|
654
711
|
"view_state": { "id": 10, "name": "public" },
|
|
655
|
-
"created_at": "2024-11-05T14:02:11+00:00"
|
|
712
|
+
"created_at": "2024-11-05T14:02:11+00:00",
|
|
713
|
+
"view_url": "https://mantis.example.com/view.php?id=1042#bugnote88"
|
|
656
714
|
}
|
|
657
715
|
```
|
|
658
716
|
|
|
@@ -687,7 +745,8 @@ Fügt eine Notiz hinzu, die nur für Entwickler und Manager sichtbar ist.
|
|
|
687
745
|
"reporter": { "id": 7, "name": "jdoe" },
|
|
688
746
|
"text": "Intern: Ursache ist das nicht erneuerte Session-Token.",
|
|
689
747
|
"view_state": { "id": 50, "name": "private" },
|
|
690
|
-
"created_at": "2024-11-05T14:05:00+00:00"
|
|
748
|
+
"created_at": "2024-11-05T14:05:00+00:00",
|
|
749
|
+
"view_url": "https://mantis.example.com/view.php?id=1042#bugnote89"
|
|
691
750
|
}
|
|
692
751
|
```
|
|
693
752
|
|
|
@@ -1265,9 +1324,9 @@ Findet Issues, die einem natürlichsprachigen Suchbegriff semantisch ähnlich si
|
|
|
1265
1324
|
|
|
1266
1325
|
```json
|
|
1267
1326
|
[
|
|
1268
|
-
{ "id": 1042, "score": 0.91 },
|
|
1269
|
-
{ "id": 987, "score": 0.84 },
|
|
1270
|
-
{ "id": 1015, "score": 0.79 }
|
|
1327
|
+
{ "id": 1042, "score": 0.91, "view_url": "https://mantis.example.com/view.php?id=1042" },
|
|
1328
|
+
{ "id": 987, "score": 0.84, "view_url": "https://mantis.example.com/view.php?id=987" },
|
|
1329
|
+
{ "id": 1015, "score": 0.79, "view_url": "https://mantis.example.com/view.php?id=1015" }
|
|
1271
1330
|
]
|
|
1272
1331
|
```
|
|
1273
1332
|
|
|
@@ -1306,7 +1365,8 @@ Reichert Suchergebnisse mit bestimmten Feldern aus MantisBT an. Ohne `select` we
|
|
|
1306
1365
|
"score": 0.91,
|
|
1307
1366
|
"summary": "Login button unresponsive on mobile Safari",
|
|
1308
1367
|
"status": { "id": 50, "name": "assigned" },
|
|
1309
|
-
"handler": { "id": 7, "name": "jdoe" }
|
|
1368
|
+
"handler": { "id": 7, "name": "jdoe" },
|
|
1369
|
+
"view_url": "https://mantis.example.com/view.php?id=1042"
|
|
1310
1370
|
}
|
|
1311
1371
|
// ...
|
|
1312
1372
|
]
|
|
@@ -1344,6 +1404,7 @@ Zeigt, welcher Teil eines Issues mit der Suchanfrage übereinstimmt. Jedes Ergeb
|
|
|
1344
1404
|
{
|
|
1345
1405
|
"id": 1042,
|
|
1346
1406
|
"score": 0.91,
|
|
1407
|
+
"view_url": "https://mantis.example.com/view.php?id=1042",
|
|
1347
1408
|
"highlights": {
|
|
1348
1409
|
"summary": "**Login**-Button reagiert nach **Passwort**-**Reset** auf Mobile Safari nicht",
|
|
1349
1410
|
"description": "…Benutzer tippt auf **Login** und es passiert nichts. Reproduzierbar nach einem **Passwort**-**Reset**-Vorgang…"
|
|
@@ -1352,13 +1413,15 @@ Zeigt, welcher Teil eines Issues mit der Suchanfrage übereinstimmt. Jedes Ergeb
|
|
|
1352
1413
|
{
|
|
1353
1414
|
"id": 987,
|
|
1354
1415
|
"score": 0.84,
|
|
1416
|
+
"view_url": "https://mantis.example.com/view.php?id=987",
|
|
1355
1417
|
"highlights": {
|
|
1356
1418
|
"summary": "**Login** schlägt mit 401 fehl — Token ungültig"
|
|
1357
1419
|
}
|
|
1358
1420
|
},
|
|
1359
1421
|
{
|
|
1360
1422
|
"id": 1015,
|
|
1361
|
-
"score": 0.79
|
|
1423
|
+
"score": 0.79,
|
|
1424
|
+
"view_url": "https://mantis.example.com/view.php?id=1015"
|
|
1362
1425
|
}
|
|
1363
1426
|
]
|
|
1364
1427
|
```
|
package/docs/cookbook.md
CHANGED
|
@@ -10,6 +10,7 @@ Tool-oriented recipes for the MantisBT MCP server — each recipe shows exactly
|
|
|
10
10
|
- [Discover valid field names for `select`](#discover-valid-field-names-for-select)
|
|
11
11
|
- [Issues](#issues)
|
|
12
12
|
- [Fetch a single issue](#fetch-a-single-issue)
|
|
13
|
+
- [Fetch multiple issues in one call](#fetch-multiple-issues-in-one-call)
|
|
13
14
|
- [List issues (paginated)](#list-issues-paginated)
|
|
14
15
|
- [Reduce response size with `select`](#reduce-response-size-with-select)
|
|
15
16
|
- [Filter by status](#filter-by-status)
|
|
@@ -222,12 +223,59 @@ Retrieves a single issue by its numeric ID including notes, attachments, tags, a
|
|
|
222
223
|
"tags": [],
|
|
223
224
|
"notes": [],
|
|
224
225
|
"attachments": [],
|
|
225
|
-
"relationships": []
|
|
226
|
+
"relationships": [],
|
|
227
|
+
"view_url": "https://mantis.example.com/view.php?id=1042"
|
|
226
228
|
}
|
|
227
229
|
```
|
|
228
230
|
|
|
229
231
|
---
|
|
230
232
|
|
|
233
|
+
### Fetch multiple issues in one call
|
|
234
|
+
|
|
235
|
+
Fetches up to 50 issues in a single MCP call. Requests run in parallel (max 5 concurrent). Missing or inaccessible IDs return `null` at their position — the call never fails due to individual missing IDs.
|
|
236
|
+
|
|
237
|
+
**Tool:** `get_issues`
|
|
238
|
+
|
|
239
|
+
**Parameters:**
|
|
240
|
+
- `ids` — array of numeric issue IDs (1–50)
|
|
241
|
+
|
|
242
|
+
**Request:**
|
|
243
|
+
|
|
244
|
+
```json
|
|
245
|
+
{
|
|
246
|
+
"ids": [1042, 1041, 9999]
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Response:**
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
{
|
|
254
|
+
"issues": [
|
|
255
|
+
{
|
|
256
|
+
"id": 1042,
|
|
257
|
+
"summary": "Login button unresponsive on mobile Safari",
|
|
258
|
+
"status": { "id": 50, "name": "assigned" },
|
|
259
|
+
"view_url": "https://mantis.example.com/view.php?id=1042"
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"id": 1041,
|
|
263
|
+
"summary": "Checkout total rounds incorrectly",
|
|
264
|
+
"status": { "id": 40, "name": "confirmed" },
|
|
265
|
+
"view_url": "https://mantis.example.com/view.php?id=1041"
|
|
266
|
+
},
|
|
267
|
+
null
|
|
268
|
+
],
|
|
269
|
+
"requested": 3,
|
|
270
|
+
"found": 2,
|
|
271
|
+
"failed": 1
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
> **Note:** `null` entries indicate IDs that were not found or could not be accessed. Check `failed` to see how many IDs could not be retrieved.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
231
279
|
### List issues (paginated)
|
|
232
280
|
|
|
233
281
|
Returns a paginated list of issues, optionally scoped to a project.
|
|
@@ -258,13 +306,15 @@ Returns a paginated list of issues, optionally scoped to a project.
|
|
|
258
306
|
"id": 1042,
|
|
259
307
|
"summary": "Login button unresponsive on mobile Safari",
|
|
260
308
|
"status": { "id": 50, "name": "assigned" },
|
|
261
|
-
"handler": { "id": 7, "name": "jdoe" }
|
|
309
|
+
"handler": { "id": 7, "name": "jdoe" },
|
|
310
|
+
"view_url": "https://mantis.example.com/view.php?id=1042"
|
|
262
311
|
},
|
|
263
312
|
{
|
|
264
313
|
"id": 1041,
|
|
265
314
|
"summary": "Checkout total rounds incorrectly",
|
|
266
315
|
"status": { "id": 40, "name": "confirmed" },
|
|
267
|
-
"handler": { "id": 4, "name": "jsmith" }
|
|
316
|
+
"handler": { "id": 4, "name": "jsmith" },
|
|
317
|
+
"view_url": "https://mantis.example.com/view.php?id=1041"
|
|
268
318
|
}
|
|
269
319
|
// ...
|
|
270
320
|
]
|
|
@@ -301,7 +351,8 @@ Pass a comma-separated list of field names to receive only the fields you need.
|
|
|
301
351
|
"id": 1042,
|
|
302
352
|
"summary": "Login button unresponsive on mobile Safari",
|
|
303
353
|
"status": { "id": 50, "name": "assigned" },
|
|
304
|
-
"handler": { "id": 7, "name": "jdoe" }
|
|
354
|
+
"handler": { "id": 7, "name": "jdoe" },
|
|
355
|
+
"view_url": "https://mantis.example.com/view.php?id=1042"
|
|
305
356
|
}
|
|
306
357
|
// ...
|
|
307
358
|
]
|
|
@@ -310,6 +361,8 @@ Pass a comma-separated list of field names to receive only the fields you need.
|
|
|
310
361
|
|
|
311
362
|
> **Note:** Use `get_issue_fields()` to see all available field names.
|
|
312
363
|
|
|
364
|
+
> **Note:** `view_url` is always present in all issue responses — it is injected by the MCP server and is not affected by the `select` parameter.
|
|
365
|
+
|
|
313
366
|
---
|
|
314
367
|
|
|
315
368
|
### Filter by status
|
|
@@ -496,7 +549,8 @@ Creates a new issue in MantisBT.
|
|
|
496
549
|
"tags": [],
|
|
497
550
|
"notes": [],
|
|
498
551
|
"attachments": [],
|
|
499
|
-
"relationships": []
|
|
552
|
+
"relationships": [],
|
|
553
|
+
"view_url": "https://mantis.example.com/view.php?id=1042"
|
|
500
554
|
}
|
|
501
555
|
```
|
|
502
556
|
|
|
@@ -543,7 +597,8 @@ Resolves and closes an issue. Always set **both** `status` and `resolution` —
|
|
|
543
597
|
"summary": "Login button unresponsive on mobile Safari",
|
|
544
598
|
"status": { "id": 80, "name": "resolved" },
|
|
545
599
|
"resolution": { "id": 20, "name": "fixed" },
|
|
546
|
-
"updated_at": "2024-11-06T10:30:00+00:00"
|
|
600
|
+
"updated_at": "2024-11-06T10:30:00+00:00",
|
|
601
|
+
"view_url": "https://mantis.example.com/view.php?id=1042"
|
|
547
602
|
}
|
|
548
603
|
```
|
|
549
604
|
|
|
@@ -580,7 +635,8 @@ Changes the handler (assignee) of an existing issue.
|
|
|
580
635
|
"summary": "Login button unresponsive on mobile Safari",
|
|
581
636
|
"status": { "id": 50, "name": "assigned" },
|
|
582
637
|
"handler": { "id": 7, "name": "jdoe" },
|
|
583
|
-
"updated_at": "2024-11-06T11:00:00+00:00"
|
|
638
|
+
"updated_at": "2024-11-06T11:00:00+00:00",
|
|
639
|
+
"view_url": "https://mantis.example.com/view.php?id=1042"
|
|
584
640
|
}
|
|
585
641
|
```
|
|
586
642
|
|
|
@@ -614,7 +670,8 @@ Sets the `fixed_in_version` field on an issue.
|
|
|
614
670
|
"id": 1042,
|
|
615
671
|
"summary": "Login button unresponsive on mobile Safari",
|
|
616
672
|
"fixed_in_version": { "name": "2.1.0" },
|
|
617
|
-
"updated_at": "2024-11-06T11:15:00+00:00"
|
|
673
|
+
"updated_at": "2024-11-06T11:15:00+00:00",
|
|
674
|
+
"view_url": "https://mantis.example.com/view.php?id=1042"
|
|
618
675
|
}
|
|
619
676
|
```
|
|
620
677
|
|
|
@@ -652,7 +709,8 @@ Adds a publicly visible note to an issue.
|
|
|
652
709
|
"reporter": { "id": 7, "name": "jdoe" },
|
|
653
710
|
"text": "Reproduced on version 2.0.3. Root cause identified in the auth middleware.",
|
|
654
711
|
"view_state": { "id": 10, "name": "public" },
|
|
655
|
-
"created_at": "2024-11-05T14:02:11+00:00"
|
|
712
|
+
"created_at": "2024-11-05T14:02:11+00:00",
|
|
713
|
+
"view_url": "https://mantis.example.com/view.php?id=1042#bugnote88"
|
|
656
714
|
}
|
|
657
715
|
```
|
|
658
716
|
|
|
@@ -687,7 +745,8 @@ Adds a note visible only to developers and managers.
|
|
|
687
745
|
"reporter": { "id": 7, "name": "jdoe" },
|
|
688
746
|
"text": "Internal: this is caused by the session token not being refreshed.",
|
|
689
747
|
"view_state": { "id": 50, "name": "private" },
|
|
690
|
-
"created_at": "2024-11-05T14:05:00+00:00"
|
|
748
|
+
"created_at": "2024-11-05T14:05:00+00:00",
|
|
749
|
+
"view_url": "https://mantis.example.com/view.php?id=1042#bugnote89"
|
|
691
750
|
}
|
|
692
751
|
```
|
|
693
752
|
|
|
@@ -1265,9 +1324,9 @@ Finds issues semantically similar to a natural language query. Returns issue IDs
|
|
|
1265
1324
|
|
|
1266
1325
|
```json
|
|
1267
1326
|
[
|
|
1268
|
-
{ "id": 1042, "score": 0.91 },
|
|
1269
|
-
{ "id": 987, "score": 0.84 },
|
|
1270
|
-
{ "id": 1015, "score": 0.79 }
|
|
1327
|
+
{ "id": 1042, "score": 0.91, "view_url": "https://mantis.example.com/view.php?id=1042" },
|
|
1328
|
+
{ "id": 987, "score": 0.84, "view_url": "https://mantis.example.com/view.php?id=987" },
|
|
1329
|
+
{ "id": 1015, "score": 0.79, "view_url": "https://mantis.example.com/view.php?id=1015" }
|
|
1271
1330
|
]
|
|
1272
1331
|
```
|
|
1273
1332
|
|
|
@@ -1306,7 +1365,8 @@ Enriches search results with specific fields fetched from MantisBT. Without `sel
|
|
|
1306
1365
|
"score": 0.91,
|
|
1307
1366
|
"summary": "Login button unresponsive on mobile Safari",
|
|
1308
1367
|
"status": { "id": 50, "name": "assigned" },
|
|
1309
|
-
"handler": { "id": 7, "name": "jdoe" }
|
|
1368
|
+
"handler": { "id": 7, "name": "jdoe" },
|
|
1369
|
+
"view_url": "https://mantis.example.com/view.php?id=1042"
|
|
1310
1370
|
}
|
|
1311
1371
|
// ...
|
|
1312
1372
|
]
|
|
@@ -1344,6 +1404,7 @@ Shows which part of an issue matched the search query. Each result that has keyw
|
|
|
1344
1404
|
{
|
|
1345
1405
|
"id": 1042,
|
|
1346
1406
|
"score": 0.91,
|
|
1407
|
+
"view_url": "https://mantis.example.com/view.php?id=1042",
|
|
1347
1408
|
"highlights": {
|
|
1348
1409
|
"summary": "**Login** button unresponsive after **password** **reset** on mobile Safari",
|
|
1349
1410
|
"description": "…user taps **login** and nothing happens. Reproducible after a **password** **reset** flow…"
|
|
@@ -1352,13 +1413,15 @@ Shows which part of an issue matched the search query. Each result that has keyw
|
|
|
1352
1413
|
{
|
|
1353
1414
|
"id": 987,
|
|
1354
1415
|
"score": 0.84,
|
|
1416
|
+
"view_url": "https://mantis.example.com/view.php?id=987",
|
|
1355
1417
|
"highlights": {
|
|
1356
1418
|
"summary": "**Login** fails with 401 — token invalidated"
|
|
1357
1419
|
}
|
|
1358
1420
|
},
|
|
1359
1421
|
{
|
|
1360
1422
|
"id": 1015,
|
|
1361
|
-
"score": 0.79
|
|
1423
|
+
"score": 0.79,
|
|
1424
|
+
"view_url": "https://mantis.example.com/view.php?id=1015"
|
|
1362
1425
|
}
|
|
1363
1426
|
]
|
|
1364
1427
|
```
|
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.10.0",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"registryType": "npm",
|
|
10
10
|
"identifier": "@dpesch/mantisbt-mcp-server",
|
|
11
|
-
"version": "1.
|
|
11
|
+
"version": "1.10.0",
|
|
12
12
|
"runtimeHint": "npx",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|
package/tests/client.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { MantisClient, MantisApiError } from '../src/client.js';
|
|
2
|
+
import { MantisClient, MantisApiError, buildIssueViewUrl, buildNoteViewUrl } from '../src/client.js';
|
|
3
3
|
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
// Helpers
|
|
@@ -331,3 +331,42 @@ describe('MantisClient – responseObserver', () => {
|
|
|
331
331
|
expect(observer).not.toHaveBeenCalled();
|
|
332
332
|
});
|
|
333
333
|
});
|
|
334
|
+
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// URL helpers
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
describe('buildIssueViewUrl', () => {
|
|
340
|
+
it('builds the correct MantisBT issue view URL', () => {
|
|
341
|
+
expect(buildIssueViewUrl('https://mantis.example.com', 42))
|
|
342
|
+
.toBe('https://mantis.example.com/view.php?id=42');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('works with base URLs that have a path prefix', () => {
|
|
346
|
+
expect(buildIssueViewUrl('https://example.com/mantis', 1))
|
|
347
|
+
.toBe('https://example.com/mantis/view.php?id=1');
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe('buildNoteViewUrl', () => {
|
|
352
|
+
it('builds the correct MantisBT note anchor URL', () => {
|
|
353
|
+
expect(buildNoteViewUrl('https://mantis.example.com', 42, 99))
|
|
354
|
+
.toBe('https://mantis.example.com/view.php?id=42#bugnote99');
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe('MantisClient – getBaseUrl', () => {
|
|
359
|
+
it('returns the normalized base URL (direct constructor)', async () => {
|
|
360
|
+
const client = new MantisClient('https://mantis.example.com', 'token');
|
|
361
|
+
expect(await client.getBaseUrl()).toBe('https://mantis.example.com');
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('returns the base URL from the credential factory', async () => {
|
|
365
|
+
const factory = vi.fn().mockResolvedValue({
|
|
366
|
+
baseUrl: 'https://lazy.example.com',
|
|
367
|
+
apiKey: 'key',
|
|
368
|
+
});
|
|
369
|
+
const client = new MantisClient(factory);
|
|
370
|
+
expect(await client.getBaseUrl()).toBe('https://lazy.example.com');
|
|
371
|
+
});
|
|
372
|
+
});
|
|
@@ -164,16 +164,17 @@ describe('rebuild_search_index – full: false', () => {
|
|
|
164
164
|
// ---------------------------------------------------------------------------
|
|
165
165
|
|
|
166
166
|
describe('search_issues – select parameter', () => {
|
|
167
|
-
it('returns plain {id, score} array when select is not provided', async () => {
|
|
167
|
+
it('returns plain {id, score, view_url} array when select is not provided', async () => {
|
|
168
168
|
const store = makeMockStore({ itemCount: 2 });
|
|
169
169
|
registerSearchTools(mockServer as never, client, store, embedder);
|
|
170
170
|
|
|
171
171
|
const result = await mockServer.callTool('search_issues', { query: 'test', top_n: 2 });
|
|
172
172
|
|
|
173
173
|
expect(result.isError).toBeUndefined();
|
|
174
|
-
const parsed = JSON.parse(result.content[0]!.text) as Array<{ id: number; score: number }>;
|
|
174
|
+
const parsed = JSON.parse(result.content[0]!.text) as Array<{ id: number; score: number; view_url: string }>;
|
|
175
175
|
expect(parsed[0]).toEqual(expect.objectContaining({ id: expect.any(Number), score: expect.any(Number) }));
|
|
176
|
-
expect(Object.keys(parsed[0]!)).toEqual(['id', 'score']);
|
|
176
|
+
expect(Object.keys(parsed[0]!)).toEqual(['id', 'score', 'view_url']);
|
|
177
|
+
expect(parsed[0]!.view_url).toBe(`https://mantis.example.com/view.php?id=${parsed[0]!.id}`);
|
|
177
178
|
});
|
|
178
179
|
|
|
179
180
|
it('fetches issues and projects requested fields when select is provided', async () => {
|
|
@@ -234,8 +235,8 @@ describe('search_issues – select parameter', () => {
|
|
|
234
235
|
expect(parsed).toHaveLength(2);
|
|
235
236
|
// First item enriched
|
|
236
237
|
expect(parsed[0]).toHaveProperty('summary');
|
|
237
|
-
// Second item fallback —
|
|
238
|
-
expect(Object.keys(parsed[1]!).sort()).toEqual(['id', 'score']);
|
|
238
|
+
// Second item fallback — id, score, and view_url
|
|
239
|
+
expect(Object.keys(parsed[1]!).sort()).toEqual(['id', 'score', 'view_url']);
|
|
239
240
|
});
|
|
240
241
|
|
|
241
242
|
it('omits non-existent fields silently', async () => {
|
|
@@ -630,3 +631,31 @@ describe('search_issues – highlight: true combined with date filter', () => {
|
|
|
630
631
|
expect(parsed[0]!.id).toBe(1);
|
|
631
632
|
});
|
|
632
633
|
});
|
|
634
|
+
|
|
635
|
+
// ---------------------------------------------------------------------------
|
|
636
|
+
// string-coercion – highlight as string
|
|
637
|
+
// ---------------------------------------------------------------------------
|
|
638
|
+
|
|
639
|
+
describe('string-coercion – search_issues highlight as string', () => {
|
|
640
|
+
it('accepts highlight "true" as boolean true', async () => {
|
|
641
|
+
const store = makeMockStore({
|
|
642
|
+
items: [{ id: 1, score: 0.9 }],
|
|
643
|
+
});
|
|
644
|
+
vi.mocked(store.getItem).mockResolvedValue({
|
|
645
|
+
id: 1,
|
|
646
|
+
vector: [],
|
|
647
|
+
metadata: { summary: 'Login error occurred', description: 'The login fails.' },
|
|
648
|
+
});
|
|
649
|
+
registerSearchTools(mockServer as never, client, store, embedder);
|
|
650
|
+
|
|
651
|
+
const result = await mockServer.callTool(
|
|
652
|
+
'search_issues',
|
|
653
|
+
{ query: 'login error', top_n: 1, highlight: 'true' },
|
|
654
|
+
{ validate: true },
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
expect(result.isError).toBeUndefined();
|
|
658
|
+
const parsed = JSON.parse(result.content[0]!.text) as Array<Record<string, unknown>>;
|
|
659
|
+
expect(parsed[0]).toHaveProperty('highlights');
|
|
660
|
+
});
|
|
661
|
+
});
|
|
@@ -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
|
+
});
|