@dpesch/mantisbt-mcp-server 1.10.2 → 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 CHANGED
@@ -11,6 +11,16 @@ This project adheres to [Semantic Versioning](https://semver.org/).
11
11
 
12
12
  ---
13
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
+
14
24
  ## [1.10.2] – 2026-05-03
15
25
 
16
26
  ### Changed
package/README.de.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@dpesch/mantisbt-mcp-server)](https://www.npmjs.com/package/@dpesch/mantisbt-mcp-server)
4
4
  [![license](https://img.shields.io/npm/l/@dpesch/mantisbt-mcp-server)](LICENSE)
5
+ [![MCP compatible](https://img.shields.io/badge/MCP-compatible-green.svg?style=flat-square)](https://modelcontextprotocol.io/)
5
6
  [![MCP Badge](https://lobehub.com/badge/mcp/dpesch-mantisbt-mcp-server)](https://lobehub.com/mcp/dpesch-mantisbt-mcp-server)
6
7
  [![MantisBT MCP Server](https://glama.ai/mcp/servers/dpesch/mantisbt-mcp-server/badges/card.svg)](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 – entweder per lokalem `file_path` oder Base64-kodiertem `content` + `filename` |
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
 
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@dpesch/mantisbt-mcp-server)](https://www.npmjs.com/package/@dpesch/mantisbt-mcp-server)
4
4
  [![license](https://img.shields.io/npm/l/@dpesch/mantisbt-mcp-server)](LICENSE)
5
+ [![MCP compatible](https://img.shields.io/badge/MCP-compatible-green.svg?style=flat-square)](https://modelcontextprotocol.io/)
5
6
  [![MCP Badge](https://lobehub.com/badge/mcp/dpesch-mantisbt-mcp-server)](https://lobehub.com/mcp/dpesch-mantisbt-mcp-server)
6
7
  [![MantisBT MCP Server](https://glama.ai/mcp/servers/dpesch/mantisbt-mcp-server/badges/card.svg)](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 — either by local `file_path` or Base64-encoded `content` + `filename` |
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
 
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',
@@ -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
- 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
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('Absolute path to the local file to upload (mutually exclusive with content)'),
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 fileBuffer;
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
- fileBuffer = Buffer.from(content, 'base64');
94
+ base64Content = content;
94
95
  fileName = filename;
95
96
  }
96
- const blob = new Blob([new Uint8Array(fileBuffer)], { type: content_type ?? 'application/octet-stream' });
97
- const formData = new FormData();
98
- formData.append('file', blob, fileName);
99
- if (description) {
100
- formData.append('description', description);
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
  };
@@ -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. Verwenden, wenn die Datei nicht auf einem lokal zugänglichen Dateisystem liegt.
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 on a local filesystem accessible to the server.
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.2",
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",
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.2",
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.2",
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 FormData', async () => {
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
- expect(options.body).toBeInstanceOf(FormData);
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('does not set Content-Type header (set automatically by fetch)', async () => {
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']).toBeUndefined();
123
+ expect(headers['Content-Type']).toBe('application/json');
122
124
  });
123
125
 
124
- it('appends description when provided', async () => {
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 formData = options.body as FormData;
132
- expect(formData.get('description')).toBe('My attachment');
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 formData = options.body as FormData;
174
- const fileEntry = formData.get('file') as File;
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 formData = options.body as FormData;
212
- const fileEntry = formData.get('file') as File;
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 formData = options.body as FormData;
228
- const fileEntry = formData.get('file') as File;
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 () => {