@dpesch/mantisbt-mcp-server 1.5.1 → 1.5.4

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.
@@ -146,6 +146,75 @@ describe('getConfig() – errors', () => {
146
146
  });
147
147
  });
148
148
 
149
+ // ---------------------------------------------------------------------------
150
+ // HTTP transport configuration
151
+ // ---------------------------------------------------------------------------
152
+
153
+ describe('getConfig() – HTTP transport', () => {
154
+ it('uses 127.0.0.1 as default httpHost', async () => {
155
+ vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com');
156
+ vi.stubEnv('MANTIS_API_KEY', 'key');
157
+
158
+ const getConfig = await freshGetConfig();
159
+ const config = await getConfig();
160
+
161
+ expect(config.httpHost).toBe('127.0.0.1');
162
+ });
163
+
164
+ it('uses 3000 as default httpPort', async () => {
165
+ vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com');
166
+ vi.stubEnv('MANTIS_API_KEY', 'key');
167
+
168
+ const getConfig = await freshGetConfig();
169
+ const config = await getConfig();
170
+
171
+ expect(config.httpPort).toBe(3000);
172
+ });
173
+
174
+ it('uses PORT when set', async () => {
175
+ vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com');
176
+ vi.stubEnv('MANTIS_API_KEY', 'key');
177
+ vi.stubEnv('PORT', '8080');
178
+
179
+ const getConfig = await freshGetConfig();
180
+ const config = await getConfig();
181
+
182
+ expect(config.httpPort).toBe(8080);
183
+ });
184
+
185
+ it('uses MCP_HTTP_HOST when set', async () => {
186
+ vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com');
187
+ vi.stubEnv('MANTIS_API_KEY', 'key');
188
+ vi.stubEnv('MCP_HTTP_HOST', '0.0.0.0');
189
+
190
+ const getConfig = await freshGetConfig();
191
+ const config = await getConfig();
192
+
193
+ expect(config.httpHost).toBe('0.0.0.0');
194
+ });
195
+
196
+ it('leaves httpToken undefined when MCP_HTTP_TOKEN is not set', async () => {
197
+ vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com');
198
+ vi.stubEnv('MANTIS_API_KEY', 'key');
199
+
200
+ const getConfig = await freshGetConfig();
201
+ const config = await getConfig();
202
+
203
+ expect(config.httpToken).toBeUndefined();
204
+ });
205
+
206
+ it('reads httpToken from MCP_HTTP_TOKEN', async () => {
207
+ vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com');
208
+ vi.stubEnv('MANTIS_API_KEY', 'key');
209
+ vi.stubEnv('MCP_HTTP_TOKEN', 'secret-token');
210
+
211
+ const getConfig = await freshGetConfig();
212
+ const config = await getConfig();
213
+
214
+ expect(config.httpToken).toBe('secret-token');
215
+ });
216
+ });
217
+
149
218
  // ---------------------------------------------------------------------------
150
219
  // Singleton caching
151
220
  // ---------------------------------------------------------------------------
@@ -1,3 +1,4 @@
1
+ import path from 'node:path';
1
2
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
3
  import { MantisClient } from '../../src/client.js';
3
4
  import { registerFileTools } from '../../src/tools/files.js';
@@ -20,7 +21,7 @@ let client: MantisClient;
20
21
  beforeEach(() => {
21
22
  mockServer = new MockMcpServer();
22
23
  client = new MantisClient('https://mantis.example.com', 'test-token');
23
- registerFileTools(mockServer as never, client);
24
+ registerFileTools(mockServer as never, client, undefined);
24
25
  vi.stubGlobal('fetch', vi.fn());
25
26
  });
26
27
 
@@ -272,3 +273,72 @@ describe('upload_file (Base64)', () => {
272
273
  expect(result.content[0]!.text).toContain('Error:');
273
274
  });
274
275
  });
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // upload_file – Path Traversal protection (uploadDir)
279
+ // ---------------------------------------------------------------------------
280
+
281
+ describe('upload_file (uploadDir restriction)', () => {
282
+ const uploadDir = path.resolve('/tmp/uploads');
283
+
284
+ beforeEach(() => {
285
+ // Override the server registered in the outer beforeEach with one that
286
+ // has uploadDir set.
287
+ mockServer = new MockMcpServer();
288
+ client = new MantisClient('https://mantis.example.com', 'test-token');
289
+ registerFileTools(mockServer as never, client, uploadDir);
290
+ vi.stubGlobal('fetch', vi.fn());
291
+ });
292
+
293
+ it('allows file_path inside uploadDir', async () => {
294
+ vi.mocked(readFile).mockResolvedValue(Buffer.from('content') as never);
295
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ id: 5 })));
296
+
297
+ const result = await mockServer.callTool('upload_file', {
298
+ issue_id: 42,
299
+ file_path: path.join(uploadDir, 'report.pdf'),
300
+ });
301
+
302
+ expect(result.isError).toBeUndefined();
303
+ expect(readFile).toHaveBeenCalled();
304
+ });
305
+
306
+ it('blocks file_path outside uploadDir', async () => {
307
+ const result = await mockServer.callTool('upload_file', {
308
+ issue_id: 42,
309
+ file_path: '/etc/passwd',
310
+ });
311
+
312
+ expect(result.isError).toBe(true);
313
+ expect(result.content[0]!.text).toContain('not allowed');
314
+ expect(readFile).not.toHaveBeenCalled();
315
+ });
316
+
317
+ it('blocks path traversal escaping uploadDir', async () => {
318
+ const result = await mockServer.callTool('upload_file', {
319
+ issue_id: 42,
320
+ file_path: path.join(uploadDir, '..', 'secret.txt'),
321
+ });
322
+
323
+ expect(result.isError).toBe(true);
324
+ expect(result.content[0]!.text).toContain('not allowed');
325
+ expect(readFile).not.toHaveBeenCalled();
326
+ });
327
+
328
+ it('allows any file_path when uploadDir is undefined (no restriction)', async () => {
329
+ // This uses the outer beforeEach server (uploadDir = undefined).
330
+ const unrestrictedServer = new MockMcpServer();
331
+ registerFileTools(unrestrictedServer as never, client, undefined);
332
+
333
+ vi.mocked(readFile).mockResolvedValue(Buffer.from('content') as never);
334
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ id: 5 })));
335
+
336
+ const result = await unrestrictedServer.callTool('upload_file', {
337
+ issue_id: 42,
338
+ file_path: '/etc/passwd',
339
+ });
340
+
341
+ expect(result.isError).toBeUndefined();
342
+ expect(readFile).toHaveBeenCalledWith('/etc/passwd');
343
+ });
344
+ });
@@ -474,3 +474,55 @@ describe('list_issues – recorded fixtures', () => {
474
474
  expect(parsed.issues).toHaveLength(resolvedInFixture);
475
475
  });
476
476
  });
477
+
478
+ // ---------------------------------------------------------------------------
479
+ // update_issue – fields allowlist
480
+ // ---------------------------------------------------------------------------
481
+
482
+ describe('update_issue – fields allowlist', () => {
483
+ it('accepts known string fields (summary, description)', async () => {
484
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ issue: { id: 1, summary: 'Updated' } })));
485
+
486
+ const result = await mockServer.callTool(
487
+ 'update_issue',
488
+ { id: 1, fields: { summary: 'Updated', description: 'New desc' } },
489
+ { validate: true },
490
+ );
491
+
492
+ expect(result.isError).toBeUndefined();
493
+ });
494
+
495
+ it('accepts known object fields (status, resolution, handler)', async () => {
496
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ issue: { id: 1 } })));
497
+
498
+ const result = await mockServer.callTool(
499
+ 'update_issue',
500
+ { id: 1, fields: { status: { name: 'resolved' }, resolution: { id: 20 }, handler: { id: 5 } } },
501
+ { validate: true },
502
+ );
503
+
504
+ expect(result.isError).toBeUndefined();
505
+ });
506
+
507
+ it('rejects unknown fields without calling the API', async () => {
508
+ const result = await mockServer.callTool(
509
+ 'update_issue',
510
+ { id: 1, fields: { reporter: { id: 99 } } },
511
+ { validate: true },
512
+ );
513
+
514
+ expect(result.isError).toBe(true);
515
+ expect(vi.mocked(fetch)).not.toHaveBeenCalled();
516
+ });
517
+
518
+ it('rejects fields with an unknown key mixed with known keys without calling the API', async () => {
519
+ const result = await mockServer.callTool(
520
+ 'update_issue',
521
+ { id: 1, fields: { summary: 'ok', unknown_field: 'bad' } },
522
+ { validate: true },
523
+ );
524
+
525
+ expect(result.isError).toBe(true);
526
+ expect(vi.mocked(fetch)).not.toHaveBeenCalled();
527
+ });
528
+ });
package/.env.local DELETED
@@ -1,3 +0,0 @@
1
- MANTIS_BASE_URL=https://mantis.dev.11com7.de
2
- MANTIS_API_KEY=3vhKDwxVavmCzRUR7Cp0Lk7F8E4dha6n
3
- MANTIS_SEARCH_ENABLED=true
@@ -1,108 +0,0 @@
1
- {
2
- "id": 51,
3
- "name": "domAgent",
4
- "real_name": "Dominik Pesch (via Agent)",
5
- "email": "d.pesch+agent@11com7.de",
6
- "language": "german",
7
- "timezone": "Europe/Berlin",
8
- "access_level": {
9
- "id": 70,
10
- "name": "manager",
11
- "label": "Manager"
12
- },
13
- "created_at": "2026-02-28T09:08:30+01:00",
14
- "projects": [
15
- {
16
- "id": 30,
17
- "name": "((Haus)) (Bolliggasse 1a)"
18
- },
19
- {
20
- "id": 54,
21
- "name": "11com7 Claude MetaRepo"
22
- },
23
- {
24
- "id": 2,
25
- "name": "11com7 ContentGo-Lib"
26
- },
27
- {
28
- "id": 48,
29
- "name": "11com7 DevServer"
30
- },
31
- {
32
- "id": 25,
33
- "name": "11com7 GmbH"
34
- },
35
- {
36
- "id": 39,
37
- "name": "11com7 Hosting (Keyweb, Ansible)"
38
- },
39
- {
40
- "id": 44,
41
- "name": "11com7-Ausbildung-FIAE"
42
- },
43
- {
44
- "id": 52,
45
- "name": "11com7-DVMS (Datenschutzverletzungsmelder)"
46
- },
47
- {
48
- "id": 5,
49
- "name": "11com7-Homepage"
50
- },
51
- {
52
- "id": 27,
53
- "name": "b11com7"
54
- },
55
- {
56
- "id": 3,
57
- "name": "BIBB NAA309"
58
- },
59
- {
60
- "id": 11,
61
- "name": "BIBB wbmonitor"
62
- },
63
- {
64
- "id": 38,
65
- "name": "DSGV Budgetanalyse 3"
66
- },
67
- {
68
- "id": 28,
69
- "name": "DSGV Finanzchecker (App)"
70
- },
71
- {
72
- "id": 45,
73
- "name": "DSGV Finanzchecker (Landingpage)"
74
- },
75
- {
76
- "id": 40,
77
- "name": "DSGV Login"
78
- },
79
- {
80
- "id": 26,
81
- "name": "DSGV Referenzbudgets"
82
- },
83
- {
84
- "id": 8,
85
- "name": "DSGV Vortragsservice"
86
- },
87
- {
88
- "id": 14,
89
- "name": "DSGV Web-Budgetplaner"
90
- },
91
- {
92
- "id": 42,
93
- "name": "Lingua-World"
94
- },
95
- {
96
- "id": 49,
97
- "name": "SIK M&E-App"
98
- },
99
- {
100
- "id": 21,
101
- "name": "Uni Bonn (Hausprint V2)"
102
- },
103
- {
104
- "id": 53,
105
- "name": "ZEEEM GSM-Landingpage"
106
- }
107
- ]
108
- }
@@ -1,138 +0,0 @@
1
- {
2
- "issues": [
3
- {
4
- "id": 7860,
5
- "summary": "sync_metadata befüllt Kategorien und Tags nicht im Cache",
6
- "description": "## Was ist passiert?\n`/mantis-sync` zeigt für alle Projekte \"Kategorien: 0\" und \"Tags: 0\" an, obwohl in Mantis Kategorien und Tags vorhanden sind.\n\nDirektaufruf von `get_project_categories(project_id: 25)` liefert 9 Kategorien für \"11com7 GmbH\" — der Cache enthält jedoch ein leeres `categories: []`. Tags fehlen im Cache-Root vollständig (kein `tags`-Key vorhanden).\n\n## Betroffene Komponente\n- Skill: `/mantis-sync`\n- Helper: `~/.claude/helpers/mantis-metadata-summary.js`\n- Externer Dependency: `mantisbt-mcp` Server, Tool `sync_metadata`\n- Plattform: beide\n\n## Ursachenanalyse\n`sync_metadata` im mantisbt-mcp-Server befüllt `byProject[id].categories` nicht — das Feld existiert im Schema, enthält aber immer `[]`. Der `tags`-Key wird auf Root-Ebene gar nicht angelegt.\n\nDas Summary-Script liest bereits korrekt:\n- `pd.categories || proj.categories || []` → bekommt immer `[]`\n- `metadata.tags || []` → bekommt immer `[]` (Key fehlt im Cache)\n\n## Lösungsansätze\n1. **mantisbt-mcp `sync_metadata` erweitern (bevorzugt):**\n - Für jedes Projekt `get_project_categories(project_id)` aufrufen und Ergebnis in `byProject[id].categories` speichern\n - `list_tags()` einmalig aufrufen und als `tags: [...]` auf Root-Ebene speichern\n2. **Workaround im /mantis-sync-Skill:** Kategorien und Tags per Direkt-API-Aufruf nachladen — unabhängig vom MCP-Server, aber teurer.\n\n## So reproduzierbar\n1. `/mantis-sync` ausführen\n2. Alle Projekte zeigen \"0\" in der Kategorien-Spalte, Fußzeile \"0 Tags\"\n3. Kontrollaufruf: `get_project_categories(project_id: 25)` → 9 Kategorien\n\n## Hinweise zur Umsetzung\n- `mantis-metadata-summary.js` muss NICHT geändert werden — liest bereits korrekt\n- Kategorien sind projektspezifisch → Sync muss über alle Projekte iterieren (erhöhter API-Aufwand)\n- Tags sind global → ein einzelner `list_tags()`-Aufruf reicht\n- Nach MCP-Server-Fix: `sync_metadata()` neu ausführen und Cache prüfen",
7
- "project": {
8
- "id": 54,
9
- "name": "11com7 Claude MetaRepo"
10
- },
11
- "category": {
12
- "id": 393,
13
- "name": "Skills"
14
- },
15
- "reporter": {
16
- "id": 51,
17
- "name": "domAgent",
18
- "real_name": "Dominik Pesch (via Agent)",
19
- "email": "d.pesch+agent@11com7.de"
20
- },
21
- "handler": {
22
- "id": 51,
23
- "name": "domAgent",
24
- "real_name": "Dominik Pesch (via Agent)",
25
- "email": "d.pesch+agent@11com7.de"
26
- },
27
- "status": {
28
- "id": 50,
29
- "name": "assigned",
30
- "label": "zugewiesen",
31
- "color": "#afbed5"
32
- },
33
- "resolution": {
34
- "id": 10,
35
- "name": "open",
36
- "label": "offen"
37
- },
38
- "view_state": {
39
- "id": 10,
40
- "name": "public",
41
- "label": "öffentlich"
42
- },
43
- "priority": {
44
- "id": 30,
45
- "name": "normal",
46
- "label": "normal"
47
- },
48
- "severity": {
49
- "id": 200,
50
- "name": "Technische Schuld",
51
- "label": "Technische Schuld"
52
- },
53
- "reproducibility": {
54
- "id": 70,
55
- "name": "have not tried",
56
- "label": "nicht getestet"
57
- },
58
- "sticky": false,
59
- "created_at": "2026-03-16T15:40:38+01:00",
60
- "updated_at": "2026-03-16T15:40:38+01:00",
61
- "history": [
62
- {
63
- "created_at": "2026-03-16T15:40:38+01:00",
64
- "user": {
65
- "id": 51,
66
- "name": "domAgent",
67
- "real_name": "Dominik Pesch (via Agent)",
68
- "email": "d.pesch+agent@11com7.de"
69
- },
70
- "type": {
71
- "id": 1,
72
- "name": "issue-new"
73
- },
74
- "message": "Neuer Eintrag"
75
- },
76
- {
77
- "created_at": "2026-03-16T15:40:38+01:00",
78
- "user": {
79
- "id": 51,
80
- "name": "domAgent",
81
- "real_name": "Dominik Pesch (via Agent)",
82
- "email": "d.pesch+agent@11com7.de"
83
- },
84
- "field": {
85
- "name": "status",
86
- "label": "Status"
87
- },
88
- "type": {
89
- "id": 0,
90
- "name": "field-updated"
91
- },
92
- "old_value": {
93
- "id": 10,
94
- "name": "new",
95
- "label": "neu",
96
- "color": "#eeb3aa"
97
- },
98
- "new_value": {
99
- "id": 50,
100
- "name": "assigned",
101
- "label": "zugewiesen",
102
- "color": "#afbed5"
103
- },
104
- "message": "Status",
105
- "change": "neu => zugewiesen"
106
- },
107
- {
108
- "created_at": "2026-03-16T15:40:38+01:00",
109
- "user": {
110
- "id": 51,
111
- "name": "domAgent",
112
- "real_name": "Dominik Pesch (via Agent)",
113
- "email": "d.pesch+agent@11com7.de"
114
- },
115
- "field": {
116
- "name": "handler",
117
- "label": "Bearbeitung durch"
118
- },
119
- "type": {
120
- "id": 0,
121
- "name": "field-updated"
122
- },
123
- "old_value": {
124
- "id": 0
125
- },
126
- "new_value": {
127
- "id": 51,
128
- "name": "domAgent",
129
- "real_name": "Dominik Pesch (via Agent)",
130
- "email": "d.pesch+agent@11com7.de"
131
- },
132
- "message": "Bearbeitung durch",
133
- "change": " => domAgent"
134
- }
135
- ]
136
- }
137
- ]
138
- }
@@ -1,67 +0,0 @@
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,138 +0,0 @@
1
- {
2
- "issues": [
3
- {
4
- "id": 7860,
5
- "summary": "sync_metadata befüllt Kategorien und Tags nicht im Cache",
6
- "description": "## Was ist passiert?\n`/mantis-sync` zeigt für alle Projekte \"Kategorien: 0\" und \"Tags: 0\" an, obwohl in Mantis Kategorien und Tags vorhanden sind.\n\nDirektaufruf von `get_project_categories(project_id: 25)` liefert 9 Kategorien für \"11com7 GmbH\" — der Cache enthält jedoch ein leeres `categories: []`. Tags fehlen im Cache-Root vollständig (kein `tags`-Key vorhanden).\n\n## Betroffene Komponente\n- Skill: `/mantis-sync`\n- Helper: `~/.claude/helpers/mantis-metadata-summary.js`\n- Externer Dependency: `mantisbt-mcp` Server, Tool `sync_metadata`\n- Plattform: beide\n\n## Ursachenanalyse\n`sync_metadata` im mantisbt-mcp-Server befüllt `byProject[id].categories` nicht — das Feld existiert im Schema, enthält aber immer `[]`. Der `tags`-Key wird auf Root-Ebene gar nicht angelegt.\n\nDas Summary-Script liest bereits korrekt:\n- `pd.categories || proj.categories || []` → bekommt immer `[]`\n- `metadata.tags || []` → bekommt immer `[]` (Key fehlt im Cache)\n\n## Lösungsansätze\n1. **mantisbt-mcp `sync_metadata` erweitern (bevorzugt):**\n - Für jedes Projekt `get_project_categories(project_id)` aufrufen und Ergebnis in `byProject[id].categories` speichern\n - `list_tags()` einmalig aufrufen und als `tags: [...]` auf Root-Ebene speichern\n2. **Workaround im /mantis-sync-Skill:** Kategorien und Tags per Direkt-API-Aufruf nachladen — unabhängig vom MCP-Server, aber teurer.\n\n## So reproduzierbar\n1. `/mantis-sync` ausführen\n2. Alle Projekte zeigen \"0\" in der Kategorien-Spalte, Fußzeile \"0 Tags\"\n3. Kontrollaufruf: `get_project_categories(project_id: 25)` → 9 Kategorien\n\n## Hinweise zur Umsetzung\n- `mantis-metadata-summary.js` muss NICHT geändert werden — liest bereits korrekt\n- Kategorien sind projektspezifisch → Sync muss über alle Projekte iterieren (erhöhter API-Aufwand)\n- Tags sind global → ein einzelner `list_tags()`-Aufruf reicht\n- Nach MCP-Server-Fix: `sync_metadata()` neu ausführen und Cache prüfen",
7
- "project": {
8
- "id": 54,
9
- "name": "11com7 Claude MetaRepo"
10
- },
11
- "category": {
12
- "id": 393,
13
- "name": "Skills"
14
- },
15
- "reporter": {
16
- "id": 51,
17
- "name": "domAgent",
18
- "real_name": "Dominik Pesch (via Agent)",
19
- "email": "d.pesch+agent@11com7.de"
20
- },
21
- "handler": {
22
- "id": 51,
23
- "name": "domAgent",
24
- "real_name": "Dominik Pesch (via Agent)",
25
- "email": "d.pesch+agent@11com7.de"
26
- },
27
- "status": {
28
- "id": 50,
29
- "name": "assigned",
30
- "label": "zugewiesen",
31
- "color": "#afbed5"
32
- },
33
- "resolution": {
34
- "id": 10,
35
- "name": "open",
36
- "label": "offen"
37
- },
38
- "view_state": {
39
- "id": 10,
40
- "name": "public",
41
- "label": "öffentlich"
42
- },
43
- "priority": {
44
- "id": 30,
45
- "name": "normal",
46
- "label": "normal"
47
- },
48
- "severity": {
49
- "id": 200,
50
- "name": "Technische Schuld",
51
- "label": "Technische Schuld"
52
- },
53
- "reproducibility": {
54
- "id": 70,
55
- "name": "have not tried",
56
- "label": "nicht getestet"
57
- },
58
- "sticky": false,
59
- "created_at": "2026-03-16T15:40:38+01:00",
60
- "updated_at": "2026-03-16T15:40:38+01:00",
61
- "history": [
62
- {
63
- "created_at": "2026-03-16T15:40:38+01:00",
64
- "user": {
65
- "id": 51,
66
- "name": "domAgent",
67
- "real_name": "Dominik Pesch (via Agent)",
68
- "email": "d.pesch+agent@11com7.de"
69
- },
70
- "type": {
71
- "id": 1,
72
- "name": "issue-new"
73
- },
74
- "message": "Neuer Eintrag"
75
- },
76
- {
77
- "created_at": "2026-03-16T15:40:38+01:00",
78
- "user": {
79
- "id": 51,
80
- "name": "domAgent",
81
- "real_name": "Dominik Pesch (via Agent)",
82
- "email": "d.pesch+agent@11com7.de"
83
- },
84
- "field": {
85
- "name": "status",
86
- "label": "Status"
87
- },
88
- "type": {
89
- "id": 0,
90
- "name": "field-updated"
91
- },
92
- "old_value": {
93
- "id": 10,
94
- "name": "new",
95
- "label": "neu",
96
- "color": "#eeb3aa"
97
- },
98
- "new_value": {
99
- "id": 50,
100
- "name": "assigned",
101
- "label": "zugewiesen",
102
- "color": "#afbed5"
103
- },
104
- "message": "Status",
105
- "change": "neu => zugewiesen"
106
- },
107
- {
108
- "created_at": "2026-03-16T15:40:38+01:00",
109
- "user": {
110
- "id": 51,
111
- "name": "domAgent",
112
- "real_name": "Dominik Pesch (via Agent)",
113
- "email": "d.pesch+agent@11com7.de"
114
- },
115
- "field": {
116
- "name": "handler",
117
- "label": "Bearbeitung durch"
118
- },
119
- "type": {
120
- "id": 0,
121
- "name": "field-updated"
122
- },
123
- "old_value": {
124
- "id": 0
125
- },
126
- "new_value": {
127
- "id": 51,
128
- "name": "domAgent",
129
- "real_name": "Dominik Pesch (via Agent)",
130
- "email": "d.pesch+agent@11com7.de"
131
- },
132
- "message": "Bearbeitung durch",
133
- "change": " => domAgent"
134
- }
135
- ]
136
- }
137
- ]
138
- }