@dpesch/mantisbt-mcp-server 1.10.1 → 1.10.3
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 +24 -0
- package/README.de.md +3 -2
- package/README.md +3 -2
- package/dist/client.js +0 -15
- package/dist/index.js +31 -21
- package/dist/tools/files.js +14 -15
- package/dist/tools/issues.js +27 -16
- package/dist/tools/monitors.js +9 -3
- package/dist/tools/notes.js +17 -7
- package/dist/tools/projects.js +19 -7
- package/docs/cookbook.de.md +1 -1
- package/docs/cookbook.md +1 -1
- package/package.json +16 -5
- package/server.json +2 -2
- package/tests/tools/files.test.ts +15 -16
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## [1.10.3] – 2026-05-15
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- `upload_file`: file uploads previously used `multipart/form-data` via a now-removed `postFormData` helper. The MantisBT REST API does not accept `multipart/form-data` for file uploads — it expects a JSON body with Base64-encoded content. The tool now sends `POST issues/{id}/files` with a JSON body containing `{ files: [{ name, type, content }] }`. The caller-facing API is unchanged: `file_path` and `content` continue to work as before; the server handles Base64 encoding internally when `file_path` is used.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- `upload_file` tool and parameter descriptions updated: `file_path` is now explicitly marked as the preferred input mode (use whenever the file exists on disk — the server reads and encodes it automatically); `content` is marked as a fallback to be used only when `file_path` is not available (e.g. in-memory data or files not accessible via the server filesystem).
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## [1.10.2] – 2026-05-03
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- Improved descriptions for `create_issue`, `add_monitor`, `add_note`, `delete_note`, `get_project_users`, and `get_project_versions`: response shapes, prerequisites, and cross-references to related tools (e.g. `find_project_member`, `get_issue_enums`) are now spelled out in each tool description — no behavioural change.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- HTTP transport (`TRANSPORT=http`): concurrent requests (e.g. MCP Inspector sending `resources/list`, `resources/read`, and `tools/list` in parallel) caused `server.close()` to kill the transport of a still-running request, resulting in `ECONNRESET` on the client side. Fixed by serialising requests through a Promise-based queue — each request waits for the previous transport to be fully closed before connecting its own.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
10
34
|
## [1.10.1] – 2026-05-02
|
|
11
35
|
|
|
12
36
|
### Added
|
package/README.de.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@dpesch/mantisbt-mcp-server)
|
|
4
4
|
[](LICENSE)
|
|
5
|
+
[](https://modelcontextprotocol.io/)
|
|
5
6
|
[](https://lobehub.com/mcp/dpesch-mantisbt-mcp-server)
|
|
6
7
|
[](https://glama.ai/mcp/servers/dpesch/mantisbt-mcp-server)
|
|
7
8
|
|
|
@@ -106,7 +107,7 @@ npm run build
|
|
|
106
107
|
| Tool | Beschreibung |
|
|
107
108
|
|---|---|
|
|
108
109
|
| `list_issue_files` | Anhänge eines Issues auflisten |
|
|
109
|
-
| `upload_file` | Datei an ein Issue anhängen –
|
|
110
|
+
| `upload_file` | Datei an ein Issue anhängen – bevorzugt: lokaler `file_path` (der Server liest und kodiert die Datei automatisch); Fallback: Base64-kodiertes `content` + `filename` (nur verwenden, wenn `file_path` nicht verfügbar ist) |
|
|
110
111
|
|
|
111
112
|
### Beziehungen
|
|
112
113
|
|
|
@@ -119,7 +120,7 @@ npm run build
|
|
|
119
120
|
|
|
120
121
|
| Tool | Beschreibung |
|
|
121
122
|
|---|---|
|
|
122
|
-
| `add_monitor` |
|
|
123
|
+
| `add_monitor` | Einen Benutzer als Beobachter eines Issues eintragen |
|
|
123
124
|
| `remove_monitor` | Benutzer als Beobachter eines Issues austragen |
|
|
124
125
|
|
|
125
126
|
### Tags
|
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@dpesch/mantisbt-mcp-server)
|
|
4
4
|
[](LICENSE)
|
|
5
|
+
[](https://modelcontextprotocol.io/)
|
|
5
6
|
[](https://lobehub.com/mcp/dpesch-mantisbt-mcp-server)
|
|
6
7
|
[](https://glama.ai/mcp/servers/dpesch/mantisbt-mcp-server)
|
|
7
8
|
|
|
@@ -106,7 +107,7 @@ npm run build
|
|
|
106
107
|
| Tool | Description |
|
|
107
108
|
|---|---|
|
|
108
109
|
| `list_issue_files` | List attachments of an issue |
|
|
109
|
-
| `upload_file` | Upload a file to an issue —
|
|
110
|
+
| `upload_file` | Upload a file to an issue — preferred: local `file_path` (server reads and encodes automatically); fallback: Base64-encoded `content` + `filename` (use only when `file_path` is not available) |
|
|
110
111
|
|
|
111
112
|
### Relationships
|
|
112
113
|
|
|
@@ -119,7 +120,7 @@ npm run build
|
|
|
119
120
|
|
|
120
121
|
| Tool | Description |
|
|
121
122
|
|---|---|
|
|
122
|
-
| `add_monitor` | Add
|
|
123
|
+
| `add_monitor` | Add a user as a monitor of an issue |
|
|
123
124
|
| `remove_monitor` | Remove a user as a monitor of an issue |
|
|
124
125
|
|
|
125
126
|
### Tags
|
package/dist/client.js
CHANGED
|
@@ -140,21 +140,6 @@ export class MantisClient {
|
|
|
140
140
|
});
|
|
141
141
|
return this.handleResponse(response);
|
|
142
142
|
}
|
|
143
|
-
async postFormData(path, formData) {
|
|
144
|
-
// Note: Content-Type must NOT be set here — fetch sets it automatically
|
|
145
|
-
// with the correct multipart/form-data boundary.
|
|
146
|
-
const { apiKey } = await this.getCredentials();
|
|
147
|
-
const response = await fetch(await this.buildUrl(path), {
|
|
148
|
-
method: 'POST',
|
|
149
|
-
headers: {
|
|
150
|
-
'Authorization': apiKey,
|
|
151
|
-
'Accept': 'application/json',
|
|
152
|
-
},
|
|
153
|
-
body: formData,
|
|
154
|
-
signal: AbortSignal.timeout(MantisClient.TIMEOUT_MS),
|
|
155
|
-
});
|
|
156
|
-
return this.handleResponse(response);
|
|
157
|
-
}
|
|
158
143
|
async getVersion() {
|
|
159
144
|
const response = await fetch(await this.buildUrl('users/me'), {
|
|
160
145
|
method: 'GET',
|
package/dist/index.js
CHANGED
|
@@ -88,6 +88,12 @@ async function runHttp() {
|
|
|
88
88
|
const startupConfig = await getStartupConfig();
|
|
89
89
|
const server = await createMcpServer();
|
|
90
90
|
const port = startupConfig.httpPort;
|
|
91
|
+
// Serialize stateless requests: each request waits for the previous transport
|
|
92
|
+
// to be fully closed before connecting a new one. Without this, concurrent
|
|
93
|
+
// requests (e.g. from MCP Inspector sending resources/list + resources/read +
|
|
94
|
+
// tools/list in parallel) would cause server.close() to kill the transport of
|
|
95
|
+
// a still-running request, leaving those responses never sent.
|
|
96
|
+
let requestQueue = Promise.resolve();
|
|
91
97
|
const httpServer = createServer(async (req, res) => {
|
|
92
98
|
if (req.method === 'POST' && req.url === '/mcp') {
|
|
93
99
|
if (startupConfig.httpToken) {
|
|
@@ -100,28 +106,32 @@ async function runHttp() {
|
|
|
100
106
|
}
|
|
101
107
|
const chunks = [];
|
|
102
108
|
req.on('data', (chunk) => chunks.push(chunk));
|
|
103
|
-
req.on('end',
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
await server.connect(transport);
|
|
117
|
-
await transport.handleRequest(req, res, body);
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
if (!res.headersSent) {
|
|
121
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
122
|
-
res.end(JSON.stringify({ error: 'Bad Request' }));
|
|
109
|
+
req.on('end', () => {
|
|
110
|
+
const prev = requestQueue;
|
|
111
|
+
let releaseLock;
|
|
112
|
+
requestQueue = new Promise(resolve => { releaseLock = resolve; });
|
|
113
|
+
void prev.then(async () => {
|
|
114
|
+
try {
|
|
115
|
+
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
116
|
+
const transport = new StreamableHTTPServerTransport({
|
|
117
|
+
sessionIdGenerator: undefined,
|
|
118
|
+
enableJsonResponse: true,
|
|
119
|
+
});
|
|
120
|
+
await server.connect(transport);
|
|
121
|
+
await transport.handleRequest(req, res, body);
|
|
123
122
|
}
|
|
124
|
-
|
|
123
|
+
catch (err) {
|
|
124
|
+
console.error('[HTTP handler error]', err instanceof Error ? err.stack : err);
|
|
125
|
+
if (!res.headersSent) {
|
|
126
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
127
|
+
res.end(JSON.stringify({ error: 'Bad Request' }));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
await server.close();
|
|
132
|
+
releaseLock();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
125
135
|
});
|
|
126
136
|
}
|
|
127
137
|
else if (req.method === 'GET' && req.url === '/health') {
|
package/dist/tools/files.js
CHANGED
|
@@ -46,17 +46,17 @@ Use this tool when you need to inspect or enumerate files attached to an issue.
|
|
|
46
46
|
title: 'Upload File Attachment',
|
|
47
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
48
|
|
|
49
|
-
|
|
50
|
-
- file_path: absolute path to a local file; filename is derived from the path
|
|
51
|
-
- content: Base64-encoded file content; filename must be supplied explicitly via the filename parameter
|
|
49
|
+
Provide exactly one of the two input modes:
|
|
50
|
+
- file_path (preferred): absolute path to a local file — use this whenever the file exists on disk; the server reads and encodes it automatically; filename is derived from the path
|
|
51
|
+
- content: Base64-encoded file content — only use this when the file is not accessible via a path (e.g. in-memory data); filename must be supplied explicitly via the filename parameter
|
|
52
52
|
|
|
53
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.
|
|
54
54
|
|
|
55
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.`,
|
|
56
56
|
inputSchema: z.object({
|
|
57
57
|
issue_id: z.coerce.number().int().positive().describe('Numeric issue ID'),
|
|
58
|
-
file_path: z.string().min(1).optional().describe('
|
|
59
|
-
content: z.string().min(1).optional().describe('Base64-encoded file content (mutually exclusive with file_path)'),
|
|
58
|
+
file_path: z.string().min(1).optional().describe('Preferred: absolute path to the local file to upload — use this whenever the file exists on disk (mutually exclusive with content)'),
|
|
59
|
+
content: z.string().min(1).optional().describe('Fallback: Base64-encoded file content — only use when file_path is not available (mutually exclusive with file_path)'),
|
|
60
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)'),
|
|
61
61
|
content_type: z.string().optional().describe('MIME type of the file, e.g. "image/png" (default: "application/octet-stream")'),
|
|
62
62
|
description: z.string().optional().describe('Optional description for the attachment'),
|
|
@@ -74,7 +74,7 @@ Use this tool to attach files such as logs, screenshots, or patches to an existi
|
|
|
74
74
|
if (file_path && content) {
|
|
75
75
|
return { content: [{ type: 'text', text: 'Error: Only one of file_path or content may be provided' }], isError: true };
|
|
76
76
|
}
|
|
77
|
-
let
|
|
77
|
+
let base64Content;
|
|
78
78
|
let fileName;
|
|
79
79
|
if (file_path) {
|
|
80
80
|
if (normalizedUploadDir) {
|
|
@@ -83,23 +83,22 @@ Use this tool to attach files such as logs, screenshots, or patches to an existi
|
|
|
83
83
|
return { content: [{ type: 'text', text: errorText('file_path is not allowed — access restricted to the designated upload directory') }], isError: true };
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
fileBuffer = await readFile(file_path);
|
|
86
|
+
const fileBuffer = await readFile(file_path);
|
|
87
|
+
base64Content = fileBuffer.toString('base64');
|
|
87
88
|
fileName = filename ?? basename(file_path);
|
|
88
89
|
}
|
|
89
90
|
else {
|
|
90
91
|
if (!filename) {
|
|
91
92
|
return { content: [{ type: 'text', text: 'Error: filename is required when using content' }], isError: true };
|
|
92
93
|
}
|
|
93
|
-
|
|
94
|
+
base64Content = content;
|
|
94
95
|
fileName = filename;
|
|
95
96
|
}
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
const result = await client.postFormData(`issues/${issue_id}/files`, formData);
|
|
97
|
+
const body = {
|
|
98
|
+
files: [{ name: fileName, type: content_type ?? 'application/octet-stream', content: base64Content }],
|
|
99
|
+
...(description && { description }),
|
|
100
|
+
};
|
|
101
|
+
const result = await client.post(`issues/${issue_id}/files`, body);
|
|
103
102
|
return {
|
|
104
103
|
content: [{ type: 'text', text: JSON.stringify(result ?? { success: true }, null, 2) }],
|
|
105
104
|
};
|
package/dist/tools/issues.js
CHANGED
|
@@ -250,23 +250,34 @@ export function registerIssueTools(server, client, cache) {
|
|
|
250
250
|
// ---------------------------------------------------------------------------
|
|
251
251
|
server.registerTool('create_issue', {
|
|
252
252
|
title: 'Create Issue',
|
|
253
|
-
description:
|
|
253
|
+
description: `Create a new MantisBT issue. Returns the full created issue object including the assigned id, summary, status, priority, severity, category, reporter, created_at, and view_url.
|
|
254
|
+
|
|
255
|
+
Required fields: summary, description, project_id, category. All other fields are optional with sensible defaults (priority: "normal", severity: "minor").
|
|
256
|
+
|
|
257
|
+
Recommended workflow:
|
|
258
|
+
1. Call get_project_categories to obtain a valid category name
|
|
259
|
+
2. Optionally call get_project_versions to obtain version names
|
|
260
|
+
3. Optionally call find_project_member to resolve the assignee's username
|
|
261
|
+
|
|
262
|
+
Both priority and severity accept canonical English names or localized labels from the connected MantisBT instance — call get_issue_enums to see all available values.
|
|
263
|
+
|
|
264
|
+
For the handler, prefer the username field (resolved server-side) over handler_id when working interactively.`,
|
|
254
265
|
inputSchema: z.object({
|
|
255
|
-
summary: z.string().min(1).describe('Issue summary/title'),
|
|
256
|
-
description: z.string().min(1).describe('Detailed issue description.
|
|
257
|
-
project_id: z.coerce.number().int().positive().describe('Project ID the issue belongs to'),
|
|
258
|
-
category: z.string().min(1).describe('Category name (
|
|
259
|
-
priority: z.string().default('normal').describe('Priority
|
|
260
|
-
severity: z.string().default('minor').describe('Severity
|
|
261
|
-
handler_id: z.coerce.number().int().positive().optional().describe('
|
|
262
|
-
handler: z.string().optional().describe('
|
|
263
|
-
version: z.string().optional().describe('Affected product version name
|
|
264
|
-
target_version: z.string().optional().describe('Target version
|
|
265
|
-
fixed_in_version: z.string().optional().describe('Version
|
|
266
|
-
steps_to_reproduce: z.string().optional().describe('
|
|
267
|
-
additional_information: z.string().optional().describe('Additional
|
|
268
|
-
reproducibility: z.string().optional().describe('
|
|
269
|
-
view_state: z.enum(['public', 'private']).optional().describe('Visibility of the issue: "public" (default) or "private"'),
|
|
266
|
+
summary: z.string().min(1).describe('Issue summary/title (required)'),
|
|
267
|
+
description: z.string().min(1).describe('Detailed issue description (required). Do not create issues without a description. Plain text or Markdown.'),
|
|
268
|
+
project_id: z.coerce.number().int().positive().describe('Project ID the issue belongs to — use list_projects to discover project IDs'),
|
|
269
|
+
category: z.string().min(1).describe('Category name (required). Use get_project_categories to list available categories for the project.'),
|
|
270
|
+
priority: z.string().default('normal').describe('Priority level. Canonical English names: none, low, normal, high, urgent, immediate. Default: "normal". Use get_issue_enums to see localized labels.'),
|
|
271
|
+
severity: z.string().default('minor').describe('Severity level. Canonical English names: feature, trivial, text, tweak, minor, major, crash, block. Default: "minor". Use get_issue_enums to see localized labels.'),
|
|
272
|
+
handler_id: z.coerce.number().int().positive().optional().describe('Numeric user ID of the assignee. Alternative to the handler field — use one or the other, not both.'),
|
|
273
|
+
handler: z.string().optional().describe('MantisBT login name of the assignee. The server resolves the name to a user ID from the project member list. Use find_project_member or get_project_users to look up valid login names.'),
|
|
274
|
+
version: z.string().optional().describe('Affected product version name. Use get_project_versions to list available version names for the project.'),
|
|
275
|
+
target_version: z.string().optional().describe('Target fix version — version in which the issue is planned to be resolved. Use get_project_versions to list available version names.'),
|
|
276
|
+
fixed_in_version: z.string().optional().describe('Version in which the issue was fixed. Use get_project_versions to list available version names.'),
|
|
277
|
+
steps_to_reproduce: z.string().optional().describe('Step-by-step instructions to reproduce the issue. Plain text or Markdown.'),
|
|
278
|
+
additional_information: z.string().optional().describe('Additional context or notes about the issue. Plain text or Markdown.'),
|
|
279
|
+
reproducibility: z.string().optional().describe('How reliably the issue reproduces. Canonical English names: always, sometimes, random, have not tried, unable to reproduce, N/A. Use get_issue_enums to see localized labels.'),
|
|
280
|
+
view_state: z.enum(['public', 'private']).optional().describe('Visibility of the issue: "public" (visible to all, default) or "private" (restricted to higher-access users).'),
|
|
270
281
|
}),
|
|
271
282
|
annotations: {
|
|
272
283
|
readOnlyHint: false,
|
package/dist/tools/monitors.js
CHANGED
|
@@ -12,10 +12,16 @@ export function registerMonitorTools(server, client) {
|
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
server.registerTool('add_monitor', {
|
|
14
14
|
title: 'Add Issue Monitor',
|
|
15
|
-
description:
|
|
15
|
+
description: `Add a user as a monitor (watcher) of a MantisBT issue. Monitors receive email notifications whenever the issue is updated. Returns a success confirmation object.
|
|
16
|
+
|
|
17
|
+
Use add_monitor to subscribe team members to issue updates without assigning them as the handler. To unsubscribe a user, call remove_monitor with the same parameters.
|
|
18
|
+
|
|
19
|
+
Adding a user who is already a monitor is a no-op — the operation succeeds without creating duplicates.
|
|
20
|
+
|
|
21
|
+
Prerequisites: obtain issue_id from list_issues or get_issue; use find_project_member or get_project_users to look up valid MantisBT login names.`,
|
|
16
22
|
inputSchema: z.object({
|
|
17
|
-
issue_id: z.coerce.number().int().positive().describe('Numeric issue ID'),
|
|
18
|
-
username: z.string().min(1).describe('
|
|
23
|
+
issue_id: z.coerce.number().int().positive().describe('Numeric issue ID — use list_issues or get_issue to obtain issue IDs'),
|
|
24
|
+
username: z.string().min(1).describe('MantisBT login name (not the display name) of the user to add as monitor. Use find_project_member or get_project_users to discover valid login names for a project.'),
|
|
19
25
|
}),
|
|
20
26
|
annotations: {
|
|
21
27
|
readOnlyHint: false,
|
package/dist/tools/notes.js
CHANGED
|
@@ -46,11 +46,17 @@ export function registerNoteTools(server, client) {
|
|
|
46
46
|
// ---------------------------------------------------------------------------
|
|
47
47
|
server.registerTool('add_note', {
|
|
48
48
|
title: 'Add Note to Issue',
|
|
49
|
-
description:
|
|
49
|
+
description: `Add a note (comment) to an existing MantisBT issue. Returns the created note object including id, created_at, reporter, text, view_state, and a view_url linking directly to the note in the MantisBT web UI.
|
|
50
|
+
|
|
51
|
+
Full UTF-8 text is supported. Markdown syntax is stored as-is — rendering depends on the MantisBT instance's configured text renderer.
|
|
52
|
+
|
|
53
|
+
Use view_state="private" to restrict the note to users with reporter-level access or higher; public notes are visible to all users who can view the issue.
|
|
54
|
+
|
|
55
|
+
Prerequisites: obtain issue_id from list_issues, get_issue, or search_issues.`,
|
|
50
56
|
inputSchema: z.object({
|
|
51
|
-
issue_id: z.coerce.number().int().positive().describe('Numeric issue ID'),
|
|
52
|
-
text: z.string().min(1).describe('Note text (
|
|
53
|
-
view_state: z.enum(['public', 'private']).default('public').describe('Visibility of the note (default
|
|
57
|
+
issue_id: z.coerce.number().int().positive().describe('Numeric issue ID — use list_issues or get_issue to obtain issue IDs'),
|
|
58
|
+
text: z.string().min(1).describe('Note text (minimum 1 character). Full UTF-8 including emoji is supported. Markdown is stored as-is.'),
|
|
59
|
+
view_state: z.enum(['public', 'private']).default('public').describe('Visibility of the note: "public" (visible to all, default) or "private" (visible only to users with sufficient access level).'),
|
|
54
60
|
}),
|
|
55
61
|
annotations: {
|
|
56
62
|
readOnlyHint: false,
|
|
@@ -82,10 +88,14 @@ export function registerNoteTools(server, client) {
|
|
|
82
88
|
// ---------------------------------------------------------------------------
|
|
83
89
|
server.registerTool('delete_note', {
|
|
84
90
|
title: 'Delete Note',
|
|
85
|
-
description:
|
|
91
|
+
description: `Permanently delete a note from a MantisBT issue. This action is irreversible — deleted notes cannot be recovered.
|
|
92
|
+
|
|
93
|
+
Returns a plain-text confirmation message on success. Returns an error if the note does not exist or the current user lacks permission to delete it (MantisBT enforces access control: users can typically only delete their own notes unless they have manager-level access or higher).
|
|
94
|
+
|
|
95
|
+
Prerequisites: obtain note_id from list_notes or from get_issue (notes[].id); obtain issue_id from the same source.`,
|
|
86
96
|
inputSchema: z.object({
|
|
87
|
-
issue_id: z.coerce.number().int().positive().describe('Numeric issue ID that owns the note'),
|
|
88
|
-
note_id: z.coerce.number().int().positive().describe('Numeric note ID to delete'),
|
|
97
|
+
issue_id: z.coerce.number().int().positive().describe('Numeric issue ID that owns the note — use get_issue or list_notes to identify this value'),
|
|
98
|
+
note_id: z.coerce.number().int().positive().describe('Numeric note ID to delete — obtain from get_issue (notes[].id) or list_notes'),
|
|
89
99
|
}),
|
|
90
100
|
annotations: {
|
|
91
101
|
readOnlyHint: false,
|
package/dist/tools/projects.js
CHANGED
|
@@ -40,10 +40,16 @@ export function registerProjectTools(server, client, cache) {
|
|
|
40
40
|
// ---------------------------------------------------------------------------
|
|
41
41
|
server.registerTool('get_project_users', {
|
|
42
42
|
title: 'Get Project Users',
|
|
43
|
-
description:
|
|
43
|
+
description: `List all users with access to a specific MantisBT project. Returns an array of user objects, each containing id, name (login name), real_name, email, and access_level fields.
|
|
44
|
+
|
|
45
|
+
Use get_project_users when you need the complete user list for a project — for example, to verify who has access or to build a handler list. For name-based lookup of a single user, prefer find_project_member which supports case-insensitive substring search and is significantly faster on large projects.
|
|
46
|
+
|
|
47
|
+
Access level IDs: 10=viewer, 25=reporter, 40=updater, 55=developer, 70=manager, 90=administrator.
|
|
48
|
+
|
|
49
|
+
Prerequisites: obtain project_id from list_projects.`,
|
|
44
50
|
inputSchema: z.object({
|
|
45
|
-
project_id: z.coerce.number().int().positive().describe('Numeric project ID'),
|
|
46
|
-
access_level: z.coerce.number().int().optional().describe('
|
|
51
|
+
project_id: z.coerce.number().int().positive().describe('Numeric project ID — use list_projects to discover project IDs'),
|
|
52
|
+
access_level: z.coerce.number().int().optional().describe('Return only users at or above this access level. Common values: 10=viewer, 25=reporter, 40=updater, 55=developer, 70=manager, 90=administrator. Omit to return all users.'),
|
|
47
53
|
}),
|
|
48
54
|
annotations: {
|
|
49
55
|
readOnlyHint: true,
|
|
@@ -70,11 +76,17 @@ export function registerProjectTools(server, client, cache) {
|
|
|
70
76
|
// ---------------------------------------------------------------------------
|
|
71
77
|
server.registerTool('get_project_versions', {
|
|
72
78
|
title: 'Get Project Versions',
|
|
73
|
-
description:
|
|
79
|
+
description: `List all versions defined for a MantisBT project. Returns an array of version objects, each containing id, name, released (boolean), obsolete (boolean), and optionally a date field.
|
|
80
|
+
|
|
81
|
+
Use the returned version names directly when creating or updating issues via create_issue and update_issue (version, target_version, fixed_in_version fields).
|
|
82
|
+
|
|
83
|
+
By default, obsolete and inherited parent-project versions are excluded. Set obsolete=true to include deprecated versions; set inherit=true to also return versions from parent projects.
|
|
84
|
+
|
|
85
|
+
Prerequisites: obtain project_id from list_projects.`,
|
|
74
86
|
inputSchema: z.object({
|
|
75
|
-
project_id: z.coerce.number().int().positive().describe('Numeric project ID'),
|
|
76
|
-
obsolete: z.preprocess(coerceBool, z.boolean()).default(false).describe('Include obsolete (deprecated) versions
|
|
77
|
-
inherit: z.preprocess(coerceBool, z.boolean()).default(false).describe('Include versions inherited from parent projects
|
|
87
|
+
project_id: z.coerce.number().int().positive().describe('Numeric project ID — use list_projects to discover project IDs'),
|
|
88
|
+
obsolete: z.preprocess(coerceBool, z.boolean()).default(false).describe('Include obsolete (deprecated) versions in the response. Default: false. Set to true to see all versions including those no longer actively used.'),
|
|
89
|
+
inherit: z.preprocess(coerceBool, z.boolean()).default(false).describe('Include versions inherited from parent projects. Default: false. Set to true for sub-projects that share versions with a parent project.'),
|
|
78
90
|
}),
|
|
79
91
|
annotations: {
|
|
80
92
|
readOnlyHint: true,
|
package/docs/cookbook.de.md
CHANGED
|
@@ -821,7 +821,7 @@ Hängt eine Datei aus dem lokalen Dateisystem an ein Issue an.
|
|
|
821
821
|
|
|
822
822
|
### Dateiinhalt hochladen (Base64)
|
|
823
823
|
|
|
824
|
-
Hängt eine Datei an, indem ihr base64-kodierter Inhalt direkt übergeben wird.
|
|
824
|
+
Hängt eine Datei an, indem ihr base64-kodierter Inhalt direkt übergeben wird. Nur als **Fallback** verwenden, wenn die Datei nicht über einen Pfad im Server-Dateisystem erreichbar ist (z.B. im Speicher erzeugte Daten oder Dateien, die nur auf der Client-Seite vorliegen). `file_path` bevorzugen, wenn die Datei auf der Festplatte existiert – der Server liest und kodiert sie automatisch.
|
|
825
825
|
|
|
826
826
|
**Tool:** `upload_file`
|
|
827
827
|
|
package/docs/cookbook.md
CHANGED
|
@@ -821,7 +821,7 @@ Attaches a file from the local filesystem to an issue.
|
|
|
821
821
|
|
|
822
822
|
### Upload file content (base64)
|
|
823
823
|
|
|
824
|
-
Attaches a file by passing its base64-encoded content directly. Use this when the file is not
|
|
824
|
+
Attaches a file by passing its base64-encoded content directly. Use this **only** as a fallback when the file is not accessible via a path on the server filesystem (e.g. in-memory data or files generated on the client side). Prefer `file_path` whenever the file exists on disk — the server reads and encodes it automatically.
|
|
825
825
|
|
|
826
826
|
**Tool:** `upload_file`
|
|
827
827
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dpesch/mantisbt-mcp-server",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.3",
|
|
4
4
|
"mcpName": "io.github.dpesch/mantisbt-mcp-server",
|
|
5
5
|
"description": "MCP server for MantisBT REST API – read and manage bug tracker issues",
|
|
6
6
|
"author": "Dominik Pesch",
|
|
@@ -14,7 +14,18 @@
|
|
|
14
14
|
"url": "https://github.com/dpesch/mantisbt-mcp-server",
|
|
15
15
|
"_note": "GitHub mirror for ecosystem compatibility only — canonical source: https://codeberg.org/dpesch/mantisbt-mcp-server"
|
|
16
16
|
},
|
|
17
|
-
"keywords": [
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"mcp-server",
|
|
20
|
+
"mantisbt",
|
|
21
|
+
"bugtracker",
|
|
22
|
+
"mantis",
|
|
23
|
+
"issue-tracker",
|
|
24
|
+
"bug-tracker",
|
|
25
|
+
"model-context-protocol",
|
|
26
|
+
"claude",
|
|
27
|
+
"claude-code"
|
|
28
|
+
],
|
|
18
29
|
"main": "dist/index.js",
|
|
19
30
|
"bin": {
|
|
20
31
|
"mantisbt-mcp-server": "dist/index.js"
|
|
@@ -35,14 +46,14 @@
|
|
|
35
46
|
"dependencies": {
|
|
36
47
|
"@huggingface/transformers": "^3.0.0",
|
|
37
48
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
38
|
-
"zod": "^3.22.4"
|
|
49
|
+
"zod": "^3.22.4"
|
|
39
50
|
},
|
|
40
51
|
"devDependencies": {
|
|
41
52
|
"@types/node": "^20.0.0",
|
|
42
|
-
"@vitest/coverage-v8": "^
|
|
53
|
+
"@vitest/coverage-v8": "^4.1.5",
|
|
43
54
|
"tsx": "^4.0.0",
|
|
44
55
|
"typescript": "^5.3.0",
|
|
45
|
-
"vitest": "^
|
|
56
|
+
"vitest": "^4.1.5"
|
|
46
57
|
},
|
|
47
58
|
"engines": {
|
|
48
59
|
"node": ">=18"
|
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.10.
|
|
6
|
+
"version": "1.10.3",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"registryType": "npm",
|
|
10
10
|
"identifier": "@dpesch/mantisbt-mcp-server",
|
|
11
|
-
"version": "1.10.
|
|
11
|
+
"version": "1.10.3",
|
|
12
12
|
"runtimeHint": "npx",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|
|
@@ -99,7 +99,7 @@ describe('upload_file', () => {
|
|
|
99
99
|
expect(calledUrl).toContain('issues/42/files');
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
it('sends a POST request with
|
|
102
|
+
it('sends a POST request with a JSON body', async () => {
|
|
103
103
|
vi.mocked(readFile).mockResolvedValue(Buffer.from('content') as never);
|
|
104
104
|
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ id: 5 })));
|
|
105
105
|
|
|
@@ -107,10 +107,12 @@ describe('upload_file', () => {
|
|
|
107
107
|
|
|
108
108
|
const options = vi.mocked(fetch).mock.calls[0]![1] as RequestInit;
|
|
109
109
|
expect(options.method).toBe('POST');
|
|
110
|
-
|
|
110
|
+
const body = JSON.parse(options.body as string) as { files: Array<{ name: string; type: string; content: string }> };
|
|
111
|
+
expect(Array.isArray(body.files)).toBe(true);
|
|
112
|
+
expect(body.files[0]!.name).toBe('test.txt');
|
|
111
113
|
});
|
|
112
114
|
|
|
113
|
-
it('
|
|
115
|
+
it('sets Content-Type header to application/json', async () => {
|
|
114
116
|
vi.mocked(readFile).mockResolvedValue(Buffer.from('content') as never);
|
|
115
117
|
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ id: 5 })));
|
|
116
118
|
|
|
@@ -118,18 +120,18 @@ describe('upload_file', () => {
|
|
|
118
120
|
|
|
119
121
|
const options = vi.mocked(fetch).mock.calls[0]![1] as RequestInit;
|
|
120
122
|
const headers = options.headers as Record<string, string>;
|
|
121
|
-
expect(headers['Content-Type']).
|
|
123
|
+
expect(headers['Content-Type']).toBe('application/json');
|
|
122
124
|
});
|
|
123
125
|
|
|
124
|
-
it('
|
|
126
|
+
it('includes description in JSON body when provided', async () => {
|
|
125
127
|
vi.mocked(readFile).mockResolvedValue(Buffer.from('content') as never);
|
|
126
128
|
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ id: 5 })));
|
|
127
129
|
|
|
128
130
|
await mockServer.callTool('upload_file', { issue_id: 42, file_path: '/tmp/test.txt', description: 'My attachment' });
|
|
129
131
|
|
|
130
132
|
const options = vi.mocked(fetch).mock.calls[0]![1] as RequestInit;
|
|
131
|
-
const
|
|
132
|
-
expect(
|
|
133
|
+
const body = JSON.parse(options.body as string) as { description: string };
|
|
134
|
+
expect(body.description).toBe('My attachment');
|
|
133
135
|
});
|
|
134
136
|
|
|
135
137
|
it('returns the API response', async () => {
|
|
@@ -170,9 +172,8 @@ describe('upload_file', () => {
|
|
|
170
172
|
await mockServer.callTool('upload_file', { issue_id: 42, file_path: '/tmp/report.pdf', filename: 'custom-name.pdf' });
|
|
171
173
|
|
|
172
174
|
const options = vi.mocked(fetch).mock.calls[0]![1] as RequestInit;
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
expect(fileEntry.name).toBe('custom-name.pdf');
|
|
175
|
+
const body = JSON.parse(options.body as string) as { files: Array<{ name: string }> };
|
|
176
|
+
expect(body.files[0]!.name).toBe('custom-name.pdf');
|
|
176
177
|
});
|
|
177
178
|
});
|
|
178
179
|
|
|
@@ -208,9 +209,8 @@ describe('upload_file (Base64)', () => {
|
|
|
208
209
|
});
|
|
209
210
|
|
|
210
211
|
const options = vi.mocked(fetch).mock.calls[0]![1] as RequestInit;
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
expect(fileEntry.name).toBe('export.csv');
|
|
212
|
+
const body = JSON.parse(options.body as string) as { files: Array<{ name: string }> };
|
|
213
|
+
expect(body.files[0]!.name).toBe('export.csv');
|
|
214
214
|
});
|
|
215
215
|
|
|
216
216
|
it('sets the content_type when provided', async () => {
|
|
@@ -224,9 +224,8 @@ describe('upload_file (Base64)', () => {
|
|
|
224
224
|
});
|
|
225
225
|
|
|
226
226
|
const options = vi.mocked(fetch).mock.calls[0]![1] as RequestInit;
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
expect(fileEntry.type).toBe('image/png');
|
|
227
|
+
const body = JSON.parse(options.body as string) as { files: Array<{ type: string }> };
|
|
228
|
+
expect(body.files[0]!.type).toBe('image/png');
|
|
230
229
|
});
|
|
231
230
|
|
|
232
231
|
it('returns isError when neither file_path nor content is provided', async () => {
|