@dpesch/mantisbt-mcp-server 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,13 @@ This project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.3.1] – 2026-03-16
11
+
12
+ ### Fixed
13
+ - `get_issue_enums` returned an empty object `{}` on MantisBT 2.x installations. The MantisBT REST API returns enum config values as pre-parsed `[{id, name, label}]` arrays, not as legacy `"id:name,..."` strings. The handler now covers both formats; the `label` field is stripped from the output.
14
+
15
+ ---
16
+
10
17
  ## [1.3.0] – 2026-03-16
11
18
 
12
19
  ### Added
package/dist/constants.js CHANGED
@@ -15,6 +15,17 @@ export const RELATIONSHIP_TYPES = {
15
15
  // MantisBT default status ID for "resolved". Issues with status.id strictly
16
16
  // below this value are considered open (new/feedback/acknowledged/confirmed/assigned).
17
17
  export const MANTIS_RESOLVED_STATUS_ID = 80;
18
+ // ---------------------------------------------------------------------------
19
+ // Issue enum config option names
20
+ // ---------------------------------------------------------------------------
21
+ export const ISSUE_ENUM_OPTIONS = [
22
+ 'severity_enum_string',
23
+ 'status_enum_string',
24
+ 'priority_enum_string',
25
+ 'resolution_enum_string',
26
+ 'reproducibility_enum_string',
27
+ ];
28
+ // ---------------------------------------------------------------------------
18
29
  export const STATUS_NAMES = [
19
30
  'new',
20
31
  'feedback',
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { ISSUE_ENUM_OPTIONS } from '../constants.js';
2
3
  import { getVersionHint } from '../version-hint.js';
3
4
  function errorText(msg) {
4
5
  const vh = getVersionHint();
@@ -88,15 +89,8 @@ The "name" field is the value to pass to create_issue or update_issue.`,
88
89
  },
89
90
  }, async () => {
90
91
  try {
91
- const enumOptions = [
92
- 'severity_enum_string',
93
- 'status_enum_string',
94
- 'priority_enum_string',
95
- 'resolution_enum_string',
96
- 'reproducibility_enum_string',
97
- ];
98
92
  const params = {};
99
- enumOptions.forEach((opt, i) => {
93
+ ISSUE_ENUM_OPTIONS.forEach((opt, i) => {
100
94
  params[`option[${i}]`] = opt;
101
95
  });
102
96
  const result = await client.get('config', params);
@@ -111,9 +105,14 @@ The "name" field is the value to pass to create_issue or update_issue.`,
111
105
  const enums = {};
112
106
  for (const { option, value } of configs) {
113
107
  const key = keyMap[option];
114
- if (key && typeof value === 'string') {
108
+ if (!key)
109
+ continue;
110
+ if (typeof value === 'string') {
115
111
  enums[key] = parseEnumString(value);
116
112
  }
113
+ else if (Array.isArray(value)) {
114
+ enums[key] = value.map(({ id, name }) => ({ id, name }));
115
+ }
117
116
  }
118
117
  return {
119
118
  content: [{ type: 'text', text: JSON.stringify(enums, null, 2) }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dpesch/mantisbt-mcp-server",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "MCP server for MantisBT REST API – read and manage bug tracker issues",
5
5
  "author": "Dominik Pesch",
6
6
  "license": "MIT",
@@ -2,6 +2,7 @@ import { writeFileSync, mkdirSync, readFileSync } from 'node:fs';
2
2
  import { join, dirname } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { MantisClient } from '../src/client.js';
5
+ import { ISSUE_ENUM_OPTIONS } from '../src/constants.js';
5
6
 
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
  const __dirname = dirname(__filename);
@@ -124,6 +125,16 @@ async function recordFixtures(): Promise<void> {
124
125
  }
125
126
  }
126
127
 
128
+ // GET config — enum values for severity, status, priority, resolution, reproducibility
129
+ try {
130
+ const enumParams: Record<string, string> = {};
131
+ ISSUE_ENUM_OPTIONS.forEach((opt, i) => { enumParams[`option[${i}]`] = opt; });
132
+ const configResult = await client.get<unknown>('config', enumParams);
133
+ saveFixture('get_issue_enums.json', configResult);
134
+ } catch (err) {
135
+ console.error('Failed to fetch config enums:', err instanceof Error ? err.message : String(err));
136
+ }
137
+
127
138
  // GET projects/{id}/versions + categories
128
139
  if (firstProjectId !== undefined) {
129
140
  const versionProjectId = projectIdWithVersions ?? firstProjectId;
@@ -0,0 +1,67 @@
1
+ {
2
+ "configs": [
3
+ {
4
+ "option": "severity_enum_string",
5
+ "value": [
6
+ { "id": 10, "name": "Feature-Wunsch", "label": "Feature-Wunsch" },
7
+ { "id": 20, "name": "Trivial", "label": "Trivial" },
8
+ { "id": 30, "name": "Fehler im Text", "label": "Fehler im Text" },
9
+ { "id": 40, "name": "Unschönheit", "label": "Unschönheit" },
10
+ { "id": 50, "name": "kleinerer Fehler", "label": "kleinerer Fehler" },
11
+ { "id": 60, "name": "schwerer Fehler", "label": "schwerer Fehler" },
12
+ { "id": 70, "name": "Absturz", "label": "Absturz" },
13
+ { "id": 80, "name": "Blocker", "label": "Blocker" },
14
+ { "id": 200, "name": "Technische Schuld", "label": "Technische Schuld" },
15
+ { "id": 210, "name": "Wartung", "label": "Wartung" }
16
+ ]
17
+ },
18
+ {
19
+ "option": "status_enum_string",
20
+ "value": [
21
+ { "id": 10, "name": "new", "label": "neu" },
22
+ { "id": 20, "name": "feedback", "label": "Rückmeldung" },
23
+ { "id": 30, "name": "acknowledged", "label": "anerkannt" },
24
+ { "id": 40, "name": "confirmed", "label": "bestätigt" },
25
+ { "id": 50, "name": "assigned", "label": "zugewiesen" },
26
+ { "id": 80, "name": "resolved", "label": "erledigt" },
27
+ { "id": 90, "name": "closed", "label": "geschlossen" }
28
+ ]
29
+ },
30
+ {
31
+ "option": "priority_enum_string",
32
+ "value": [
33
+ { "id": 10, "name": "none", "label": "keine" },
34
+ { "id": 20, "name": "low", "label": "niedrig" },
35
+ { "id": 30, "name": "normal", "label": "normal" },
36
+ { "id": 40, "name": "high", "label": "hoch" },
37
+ { "id": 50, "name": "urgent", "label": "dringend" },
38
+ { "id": 60, "name": "immediate", "label": "sofort" }
39
+ ]
40
+ },
41
+ {
42
+ "option": "resolution_enum_string",
43
+ "value": [
44
+ { "id": 10, "name": "open", "label": "offen" },
45
+ { "id": 20, "name": "fixed", "label": "erledigt" },
46
+ { "id": 30, "name": "reopened", "label": "wiedereröffnet" },
47
+ { "id": 40, "name": "unable to duplicate", "label": "nicht reproduzierbar" },
48
+ { "id": 50, "name": "not fixable", "label": "unlösbar" },
49
+ { "id": 60, "name": "duplicate", "label": "doppelt" },
50
+ { "id": 70, "name": "not a bug", "label": "keine Änderung notwendig" },
51
+ { "id": 80, "name": "suspended", "label": "aufgeschoben" },
52
+ { "id": 90, "name": "wont fix", "label": "wird nicht behoben" }
53
+ ]
54
+ },
55
+ {
56
+ "option": "reproducibility_enum_string",
57
+ "value": [
58
+ { "id": 10, "name": "always", "label": "immer" },
59
+ { "id": 30, "name": "sometimes", "label": "manchmal" },
60
+ { "id": 50, "name": "random", "label": "zufällig" },
61
+ { "id": 70, "name": "have not tried", "label": "nicht getestet" },
62
+ { "id": 90, "name": "unable to duplicate", "label": "nicht reproduzierbar" },
63
+ { "id": 100, "name": "N/A", "label": "N/A" }
64
+ ]
65
+ }
66
+ ]
67
+ }
@@ -1,23 +1,39 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
2
5
  import { MantisClient } from '../../src/client.js';
3
6
  import { MetadataCache } from '../../src/cache.js';
4
7
  import { registerConfigTools } from '../../src/tools/config.js';
5
8
  import { MockMcpServer, makeResponse } from '../helpers/mock-server.js';
6
9
 
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+ const fixturesDir = join(__dirname, '..', 'fixtures');
13
+
7
14
  // ---------------------------------------------------------------------------
8
- // Setup
15
+ // Fixtures
9
16
  // ---------------------------------------------------------------------------
10
17
 
11
- const ENUM_FIXTURE = {
18
+ const enumFixture = JSON.parse(
19
+ readFileSync(join(fixturesDir, 'get_issue_enums.json'), 'utf-8')
20
+ ) as { configs: Array<{ option: string; value: Array<{ id: number; name: string; label: string }> }> };
21
+
22
+ // Legacy: some MantisBT versions return value as a comma-separated "id:name" string
23
+ const ENUM_FIXTURE_STRING = {
12
24
  configs: [
13
- { option: 'severity_enum_string', value: '10:feature,20:trivial,30:text,40:tweak,50:minor,60:major,70:crash,80:block' },
14
- { option: 'status_enum_string', value: '10:new,20:feedback,30:acknowledged,40:confirmed,50:assigned,80:resolved,90:closed' },
15
- { option: 'priority_enum_string', value: '10:none,20:low,30:normal,40:high,50:urgent,60:immediate' },
16
- { option: 'resolution_enum_string', value: '10:open,20:fixed,30:reopened,40:unable to duplicate,50:not fixable,60:duplicate,70:no change required,80:suspended,90:wont fix' },
17
- { option: 'reproducibility_enum_string', value: '10:always,30:sometimes,50:random,70:have not tried,90:unable to reproduce,100:N/A' },
25
+ { option: 'severity_enum_string', value: '10:feature,50:minor,80:block' },
26
+ { option: 'status_enum_string', value: '10:new,80:resolved,90:closed' },
27
+ { option: 'priority_enum_string', value: '10:none,30:normal,60:immediate' },
28
+ { option: 'resolution_enum_string', value: '10:open,20:fixed' },
29
+ { option: 'reproducibility_enum_string', value: '10:always,70:have not tried' },
18
30
  ],
19
31
  };
20
32
 
33
+ // ---------------------------------------------------------------------------
34
+ // Setup
35
+ // ---------------------------------------------------------------------------
36
+
21
37
  let mockServer: MockMcpServer;
22
38
  let client: MantisClient;
23
39
  let cache: MetadataCache;
@@ -44,7 +60,7 @@ describe('get_issue_enums', () => {
44
60
  });
45
61
 
46
62
  it('gibt strukturierte Enum-Arrays für alle 5 Felder zurück', async () => {
47
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(ENUM_FIXTURE)));
63
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(enumFixture)));
48
64
 
49
65
  const result = await mockServer.callTool('get_issue_enums', {});
50
66
 
@@ -58,23 +74,39 @@ describe('get_issue_enums', () => {
58
74
  expect(Array.isArray(parsed.reproducibility)).toBe(true);
59
75
  });
60
76
 
61
- it('parst id und name korrekt', async () => {
62
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(ENUM_FIXTURE)));
77
+ it('parst Array-Format (MantisBT 2.x): id und name korrekt, label wird verworfen', async () => {
78
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(enumFixture)));
63
79
 
64
80
  const result = await mockServer.callTool('get_issue_enums', {});
65
81
 
66
82
  const parsed = JSON.parse(result.content[0]!.text) as Record<string, Array<{ id: number; name: string }>>;
67
83
 
68
- expect(parsed.severity).toContainEqual({ id: 50, name: 'minor' });
69
- expect(parsed.severity).toContainEqual({ id: 80, name: 'block' });
84
+ // Werte aus der echten Fixture prüfen
85
+ expect(parsed.severity).toContainEqual({ id: 50, name: 'kleinerer Fehler' });
86
+ expect(parsed.severity).toContainEqual({ id: 80, name: 'Blocker' });
87
+ expect(parsed.severity).toContainEqual({ id: 200, name: 'Technische Schuld' });
70
88
  expect(parsed.status).toContainEqual({ id: 10, name: 'new' });
71
89
  expect(parsed.status).toContainEqual({ id: 80, name: 'resolved' });
72
90
  expect(parsed.priority).toContainEqual({ id: 30, name: 'normal' });
73
91
  expect(parsed.reproducibility).toContainEqual({ id: 70, name: 'have not tried' });
92
+ // label darf nicht im Output enthalten sein
93
+ expect(Object.keys(parsed.severity[0]!)).toEqual(['id', 'name']);
94
+ });
95
+
96
+ it('parst String-Format (Legacy): "id:name,..."-Strings korrekt', async () => {
97
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(ENUM_FIXTURE_STRING)));
98
+
99
+ const result = await mockServer.callTool('get_issue_enums', {});
100
+
101
+ const parsed = JSON.parse(result.content[0]!.text) as Record<string, Array<{ id: number; name: string }>>;
102
+
103
+ expect(parsed.severity).toContainEqual({ id: 50, name: 'minor' });
104
+ expect(parsed.status).toContainEqual({ id: 80, name: 'resolved' });
105
+ expect(parsed.reproducibility).toContainEqual({ id: 70, name: 'have not tried' });
74
106
  });
75
107
 
76
108
  it('fragt alle 5 Enum-Optionen ab', async () => {
77
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(ENUM_FIXTURE)));
109
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(enumFixture)));
78
110
 
79
111
  await mockServer.callTool('get_issue_enums', {});
80
112