@dpesch/mantisbt-mcp-server 1.9.1 → 1.10.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/.github/workflows/ci.yml +32 -0
- package/CHANGELOG.md +21 -0
- package/README.de.md +1 -0
- package/README.md +1 -0
- package/dist/client.js +9 -0
- package/dist/search/tools.js +9 -5
- package/dist/tools/files.js +10 -12
- package/dist/tools/issues.js +39 -15
- package/dist/tools/notes.js +15 -4
- 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 +6 -5
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Set up Node.js
|
|
17
|
+
uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: '20'
|
|
20
|
+
cache: 'npm'
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: npm ci
|
|
24
|
+
|
|
25
|
+
- name: Type check
|
|
26
|
+
run: npm run typecheck
|
|
27
|
+
|
|
28
|
+
- name: Build
|
|
29
|
+
run: npm run build
|
|
30
|
+
|
|
31
|
+
- name: Test
|
|
32
|
+
run: npm test
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.10.1] – 2026-05-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- GitHub Actions CI workflow (`.github/workflows/ci.yml`): runs on push and pull requests to `main`; steps are typecheck, build, and test on Node.js 20.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Improved descriptions for `upload_file` and `list_issue_files`: both now include return shape, usage guidelines, and cross-references to related tools.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- `upload_file`: removed Zod `.refine()` calls from `inputSchema`. MCP SDK 1.27.x serialized `ZodEffects` (the type produced by `.refine()`) to an empty `properties: {}` object, making all parameters invisible to clients. Imperative validation in the handler is unchanged — the same constraints are still enforced at runtime.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## [1.10.0] – 2026-04-11
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- 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.
|
|
27
|
+
- `MantisClient` gains a new public `getBaseUrl()` method (returns the resolved, normalised base URL).
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
10
31
|
## [1.9.1] – 2026-03-30
|
|
11
32
|
|
|
12
33
|
### Fixed
|
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';
|
|
@@ -75,20 +76,23 @@ export function registerSearchTools(server, client, store, embedder) {
|
|
|
75
76
|
const dateFilter = { updated_after, updated_before, created_after, created_before };
|
|
76
77
|
const filterActive = hasDateFilter(dateFilter);
|
|
77
78
|
const terms = highlight ? extractTerms(query) : [];
|
|
78
|
-
const queryVector = await
|
|
79
|
+
const [queryVector, baseUrl] = await Promise.all([
|
|
80
|
+
embedder.embed(query),
|
|
81
|
+
client.getBaseUrl(),
|
|
82
|
+
]);
|
|
79
83
|
const results = await store.search(queryVector, top_n);
|
|
80
84
|
if (!select) {
|
|
81
85
|
// For filtering or highlighting we need store metadata per result
|
|
82
86
|
if (!filterActive && !terms.length) {
|
|
83
87
|
return {
|
|
84
|
-
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) }],
|
|
85
89
|
};
|
|
86
90
|
}
|
|
87
91
|
const filtered = await Promise.all(results.map(async ({ id, score }) => {
|
|
88
92
|
const item = await store.getItem(id);
|
|
89
93
|
if (filterActive && !matchesDateFilter(item?.metadata ?? {}, dateFilter))
|
|
90
94
|
return null;
|
|
91
|
-
const result = { id, score };
|
|
95
|
+
const result = { id, score, view_url: buildIssueViewUrl(baseUrl, id) };
|
|
92
96
|
if (terms.length > 0 && item) {
|
|
93
97
|
const h = buildHighlights(item.metadata.summary, item.metadata.description, terms);
|
|
94
98
|
if (h)
|
|
@@ -108,7 +112,7 @@ export function registerSearchTools(server, client, store, embedder) {
|
|
|
108
112
|
if (filterActive && !matchesDateFilter(issue, dateFilter)) {
|
|
109
113
|
return null;
|
|
110
114
|
}
|
|
111
|
-
const projected = { id, score };
|
|
115
|
+
const projected = { id, score, view_url: buildIssueViewUrl(baseUrl, id) };
|
|
112
116
|
for (const field of fields) {
|
|
113
117
|
if (field !== 'id' && field in issue) {
|
|
114
118
|
projected[field] = issue[field];
|
|
@@ -124,7 +128,7 @@ export function registerSearchTools(server, client, store, embedder) {
|
|
|
124
128
|
return projected;
|
|
125
129
|
}
|
|
126
130
|
catch {
|
|
127
|
-
const result = { id, score };
|
|
131
|
+
const result = { id, score, view_url: buildIssueViewUrl(baseUrl, id) };
|
|
128
132
|
if (terms.length > 0) {
|
|
129
133
|
const item = await store.getItem(id);
|
|
130
134
|
if (item) {
|
package/dist/tools/files.js
CHANGED
|
@@ -15,7 +15,9 @@ export function registerFileTools(server, client, uploadDir) {
|
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
server.registerTool('list_issue_files', {
|
|
17
17
|
title: 'List Issue File Attachments',
|
|
18
|
-
description:
|
|
18
|
+
description: `List all file attachments of a MantisBT issue. Returns an array of attachment objects, each containing id, filename, size in bytes, content_type, and download_url. Returns an empty array if the issue has no attachments.
|
|
19
|
+
|
|
20
|
+
Use this tool when you need to inspect or enumerate files attached to an issue. To add a new attachment, use upload_file instead. To retrieve full issue details that include attachments alongside other fields, use get_issue instead.`,
|
|
19
21
|
inputSchema: z.object({
|
|
20
22
|
issue_id: z.coerce.number().int().positive().describe('Numeric issue ID'),
|
|
21
23
|
}),
|
|
@@ -42,13 +44,15 @@ export function registerFileTools(server, client, uploadDir) {
|
|
|
42
44
|
// ---------------------------------------------------------------------------
|
|
43
45
|
server.registerTool('upload_file', {
|
|
44
46
|
title: 'Upload File Attachment',
|
|
45
|
-
description: `Upload a file as an attachment to a MantisBT issue
|
|
47
|
+
description: `Upload a file as an attachment to a MantisBT issue. Adds the file to the issue without modifying any issue fields or status. Returns the created attachment metadata on success.
|
|
48
|
+
|
|
49
|
+
Two input modes — exactly one must be provided:
|
|
50
|
+
- file_path: absolute path to a local file; filename is derived from the path automatically
|
|
51
|
+
- content: Base64-encoded file content; filename must be supplied explicitly via the filename parameter
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
- file_path: absolute path to a local file — filename is derived from the path automatically
|
|
49
|
-
- content: Base64-encoded file content — filename must be supplied explicitly via the filename parameter
|
|
53
|
+
The optional content_type sets the MIME type (e.g. "image/png"); defaults to "application/octet-stream". Use the optional description to annotate the attachment.
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
Use this tool to attach files such as logs, screenshots, or patches to an existing issue. To list existing attachments, use list_issue_files. To retrieve issue details, use get_issue.`,
|
|
52
56
|
inputSchema: z.object({
|
|
53
57
|
issue_id: z.coerce.number().int().positive().describe('Numeric issue ID'),
|
|
54
58
|
file_path: z.string().min(1).optional().describe('Absolute path to the local file to upload (mutually exclusive with content)'),
|
|
@@ -56,12 +60,6 @@ The optional content_type parameter sets the MIME type (e.g. "image/png"). If om
|
|
|
56
60
|
filename: z.string().min(1).optional().describe('File name for the attachment (required when using content; overrides the derived name when using file_path)'),
|
|
57
61
|
content_type: z.string().optional().describe('MIME type of the file, e.g. "image/png" (default: "application/octet-stream")'),
|
|
58
62
|
description: z.string().optional().describe('Optional description for the attachment'),
|
|
59
|
-
}).refine(d => !!(d.file_path ?? d.content), {
|
|
60
|
-
message: 'Either file_path or content must be provided',
|
|
61
|
-
}).refine(d => !(d.file_path && d.content), {
|
|
62
|
-
message: 'Only one of file_path or content may be provided',
|
|
63
|
-
}).refine(d => !d.content || !!d.filename, {
|
|
64
|
-
message: 'filename is required when using content',
|
|
65
63
|
}),
|
|
66
64
|
annotations: {
|
|
67
65
|
readOnlyHint: false,
|
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) {
|
|
@@ -419,9 +439,13 @@ Important: when resolving an issue, always set BOTH status and resolution to avo
|
|
|
419
439
|
patch[field] = resolved;
|
|
420
440
|
}
|
|
421
441
|
}
|
|
422
|
-
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;
|
|
423
447
|
return {
|
|
424
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
448
|
+
content: [{ type: 'text', text: JSON.stringify(enrichIssue(issue, baseUrl), null, 2) }],
|
|
425
449
|
};
|
|
426
450
|
}
|
|
427
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/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.1",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"registryType": "npm",
|
|
10
10
|
"identifier": "@dpesch/mantisbt-mcp-server",
|
|
11
|
-
"version": "1.
|
|
11
|
+
"version": "1.10.1",
|
|
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 () => {
|