@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 +7 -0
- package/dist/constants.js +11 -0
- package/dist/tools/config.js +8 -9
- package/package.json +1 -1
- package/scripts/record-fixtures.ts +11 -0
- package/tests/fixtures/get_issue_enums.json +67 -0
- package/tests/tools/config.test.ts +45 -13
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',
|
package/dist/tools/config.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
@@ -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
|
-
//
|
|
15
|
+
// Fixtures
|
|
9
16
|
// ---------------------------------------------------------------------------
|
|
10
17
|
|
|
11
|
-
const
|
|
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',
|
|
14
|
-
{ option: 'status_enum_string',
|
|
15
|
-
{ option: 'priority_enum_string',
|
|
16
|
-
{ option: 'resolution_enum_string',
|
|
17
|
-
{ option: 'reproducibility_enum_string', value: '10:always,
|
|
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(
|
|
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(
|
|
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
|
-
|
|
69
|
-
expect(parsed.severity).toContainEqual({ id:
|
|
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(
|
|
109
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(enumFixture)));
|
|
78
110
|
|
|
79
111
|
await mockServer.callTool('get_issue_enums', {});
|
|
80
112
|
|