@dpesch/mantisbt-mcp-server 1.10.3 → 1.11.0

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.de.md +11 -8
  3. package/README.md +11 -8
  4. package/dist/config.js +13 -0
  5. package/dist/index.js +12 -12
  6. package/dist/tools/files.js +11 -2
  7. package/dist/tools/issues.js +47 -15
  8. package/dist/tools/notes.js +15 -10
  9. package/docs/cookbook.de.md +67 -0
  10. package/docs/cookbook.md +67 -0
  11. package/docs/examples.de.md +8 -0
  12. package/docs/examples.md +8 -0
  13. package/package.json +1 -1
  14. package/server.json +2 -2
  15. package/.gitea/PULL_REQUEST_TEMPLATE.md +0 -17
  16. package/.gitea/workflows/ci.yml +0 -95
  17. package/.github/workflows/ci.yml +0 -32
  18. package/scripts/hooks/pre-push.mjs +0 -220
  19. package/scripts/init.mjs +0 -95
  20. package/scripts/record-fixtures.ts +0 -160
  21. package/tests/cache.test.ts +0 -265
  22. package/tests/client.test.ts +0 -372
  23. package/tests/config.test.ts +0 -263
  24. package/tests/fixtures/get_current_user.json +0 -40
  25. package/tests/fixtures/get_issue.json +0 -133
  26. package/tests/fixtures/get_issue_enums.json +0 -67
  27. package/tests/fixtures/get_issue_fields_sample.json +0 -76
  28. package/tests/fixtures/get_project_categories.json +0 -157
  29. package/tests/fixtures/get_project_versions.json +0 -3
  30. package/tests/fixtures/get_project_versions_with_data.json +0 -52
  31. package/tests/fixtures/list_issues.json +0 -95
  32. package/tests/fixtures/list_projects.json +0 -65
  33. package/tests/helpers/mock-server.ts +0 -166
  34. package/tests/helpers/search-mocks.ts +0 -84
  35. package/tests/prompts/prompts.test.ts +0 -242
  36. package/tests/resources/resources.test.ts +0 -309
  37. package/tests/search/embedder.test.ts +0 -81
  38. package/tests/search/highlight.test.ts +0 -129
  39. package/tests/search/store.test.ts +0 -193
  40. package/tests/search/sync.test.ts +0 -249
  41. package/tests/search/tools.test.ts +0 -661
  42. package/tests/tools/config.test.ts +0 -212
  43. package/tests/tools/files.test.ts +0 -343
  44. package/tests/tools/issues.test.ts +0 -1180
  45. package/tests/tools/metadata.test.ts +0 -509
  46. package/tests/tools/monitors.test.ts +0 -101
  47. package/tests/tools/projects.test.ts +0 -338
  48. package/tests/tools/relationships.test.ts +0 -177
  49. package/tests/tools/string-coercion.test.ts +0 -317
  50. package/tests/tools/users.test.ts +0 -62
  51. package/tests/utils/date-filter.test.ts +0 -169
  52. package/tests/version-hint.test.ts +0 -230
  53. package/tsconfig.build.json +0 -8
  54. package/vitest.config.ts +0 -8
@@ -1,160 +0,0 @@
1
- import { writeFileSync, mkdirSync, readFileSync } from 'node:fs';
2
- import { join, dirname } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { MantisClient } from '../src/client.js';
5
- import { ISSUE_ENUM_OPTIONS } from '../src/constants.js';
6
-
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = dirname(__filename);
9
-
10
- // ---------------------------------------------------------------------------
11
- // .env.local laden (falls vorhanden)
12
- // ---------------------------------------------------------------------------
13
-
14
- try {
15
- const envPath = join(__dirname, '..', '.env.local');
16
- const lines = readFileSync(envPath, 'utf-8').split('\n');
17
- for (const line of lines) {
18
- const match = line.match(/^([^#=\s][^=]*)=(.*)$/);
19
- if (match) {
20
- const key = match[1].trim();
21
- const value = match[2].trim().replace(/^["']|["']$/g, '');
22
- if (!process.env[key]) process.env[key] = value;
23
- }
24
- }
25
- } catch {
26
- // .env.local nicht vorhanden – ENV-Vars direkt nutzen
27
- }
28
-
29
- // ---------------------------------------------------------------------------
30
- // Config aus ENV
31
- // ---------------------------------------------------------------------------
32
-
33
- const baseUrlRaw = process.env['MANTIS_BASE_URL'];
34
- const apiKey = process.env['MANTIS_API_KEY'];
35
-
36
- if (!baseUrlRaw || !apiKey) {
37
- console.error('Error: MANTIS_BASE_URL and MANTIS_API_KEY environment variables must be set.');
38
- process.exit(1);
39
- }
40
-
41
- const baseUrl = baseUrlRaw.replace(/\/$/, '');
42
-
43
- // ---------------------------------------------------------------------------
44
- // Fixtures-Verzeichnis
45
- // ---------------------------------------------------------------------------
46
-
47
- const fixturesDir = join(__dirname, '..', 'tests', 'fixtures', 'recorded');
48
- mkdirSync(fixturesDir, { recursive: true });
49
-
50
- function saveFixture(filename: string, data: unknown): void {
51
- const filePath = join(fixturesDir, filename);
52
- writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
53
- console.log(`Saved: ${filePath}`);
54
- }
55
-
56
- // ---------------------------------------------------------------------------
57
- // Client instanziieren
58
- // ---------------------------------------------------------------------------
59
-
60
- const client = new MantisClient(baseUrl, apiKey);
61
-
62
- // ---------------------------------------------------------------------------
63
- // Fixtures aufzeichnen
64
- // ---------------------------------------------------------------------------
65
-
66
- async function recordFixtures(): Promise<void> {
67
- // GET projects
68
- let firstProjectId: number | undefined;
69
- let projectIdWithVersions: number | undefined;
70
- try {
71
- const projectsResult = await client.get<{ projects: Array<{ id: number; name: string; versions?: unknown[] }> }>('projects');
72
- saveFixture('list_projects.json', projectsResult);
73
- if (Array.isArray(projectsResult.projects) && projectsResult.projects.length > 0) {
74
- firstProjectId = projectsResult.projects[0]?.id;
75
- // Erstes Projekt mit nicht-leeren Versionen bevorzugen
76
- projectIdWithVersions = projectsResult.projects.find(
77
- (p) => Array.isArray(p.versions) && p.versions.length > 0,
78
- )?.id ?? firstProjectId;
79
- }
80
- } catch (err) {
81
- console.error('Failed to fetch projects:', err instanceof Error ? err.message : String(err));
82
- }
83
-
84
- // GET users/me
85
- try {
86
- const userResult = await client.get<unknown>('users/me');
87
- saveFixture('get_current_user.json', userResult);
88
- } catch (err) {
89
- console.error('Failed to fetch users/me:', err instanceof Error ? err.message : String(err));
90
- }
91
-
92
- // GET issues?page=1&page_size=3
93
- let firstIssueId: number | undefined;
94
- try {
95
- const issuesResult = await client.get<{ issues: Array<{ id: number }> }>('issues', {
96
- page: 1,
97
- page_size: 3,
98
- });
99
- saveFixture('list_issues.json', issuesResult);
100
- if (Array.isArray(issuesResult.issues) && issuesResult.issues.length > 0) {
101
- firstIssueId = issuesResult.issues[0]?.id;
102
- }
103
- } catch (err) {
104
- console.error('Failed to fetch issues:', err instanceof Error ? err.message : String(err));
105
- }
106
-
107
- // GET issues?page=1&page_size=1 — single issue sample for get_issue_fields field discovery
108
- try {
109
- const sampleResult = await client.get<{ issues: unknown[] }>('issues', {
110
- page: 1,
111
- page_size: 1,
112
- });
113
- saveFixture('get_issue_fields_sample.json', sampleResult);
114
- } catch (err) {
115
- console.error('Failed to fetch issues sample:', err instanceof Error ? err.message : String(err));
116
- }
117
-
118
- // GET issues/{id}
119
- if (firstIssueId !== undefined) {
120
- try {
121
- const issueResult = await client.get<unknown>(`issues/${firstIssueId}`);
122
- saveFixture('get_issue.json', issueResult);
123
- } catch (err) {
124
- console.error(`Failed to fetch issues/${firstIssueId}:`, err instanceof Error ? err.message : String(err));
125
- }
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
-
138
- // GET projects/{id}/versions + categories
139
- if (firstProjectId !== undefined) {
140
- const versionProjectId = projectIdWithVersions ?? firstProjectId;
141
- try {
142
- const versionsResult = await client.get<unknown>(`projects/${versionProjectId}/versions`, { obsolete: 1, inherit: 1 });
143
- saveFixture('get_project_versions.json', versionsResult);
144
- } catch (err) {
145
- console.error(`Failed to fetch projects/${versionProjectId}/versions:`, err instanceof Error ? err.message : String(err));
146
- }
147
-
148
- try {
149
- const projectResult = await client.get<{ projects: Array<{ categories?: unknown[] }> }>(`projects/${firstProjectId}`);
150
- saveFixture('get_project_categories.json', { categories: projectResult.projects?.[0]?.categories ?? [] });
151
- } catch (err) {
152
- console.error(`Failed to fetch projects/${firstProjectId} (categories):`, err instanceof Error ? err.message : String(err));
153
- }
154
- }
155
- }
156
-
157
- recordFixtures().catch((err) => {
158
- console.error('Unexpected error:', err);
159
- process.exit(1);
160
- });
@@ -1,265 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
-
3
- // vi.mock must be at module top level — vitest hoists it automatically
4
- vi.mock('node:fs/promises');
5
-
6
- import { readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
7
- import { MetadataCache, type CachedMetadata } from '../src/cache.js';
8
-
9
- // ---------------------------------------------------------------------------
10
- // Helpers
11
- // ---------------------------------------------------------------------------
12
-
13
- const CACHE_DIR = '/tmp/test-cache';
14
- const TTL = 3600; // 1 hour in seconds
15
-
16
- function makeCache(): MetadataCache {
17
- return new MetadataCache(CACHE_DIR, TTL);
18
- }
19
-
20
- function makeSampleMetadata(): CachedMetadata {
21
- return {
22
- timestamp: Date.now(),
23
- projects: [{ id: 1, name: 'Test Project' }],
24
- byProject: {},
25
- tags: [],
26
- };
27
- }
28
-
29
- // ---------------------------------------------------------------------------
30
- // Setup
31
- // ---------------------------------------------------------------------------
32
-
33
- beforeEach(() => {
34
- vi.resetAllMocks();
35
- });
36
-
37
- // ---------------------------------------------------------------------------
38
- // isValid()
39
- // ---------------------------------------------------------------------------
40
-
41
- describe('MetadataCache – isValid()', () => {
42
- it('returns false when file does not exist (readFile throws)', async () => {
43
- vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
44
-
45
- const cache = makeCache();
46
- await expect(cache.isValid()).resolves.toBe(false);
47
- });
48
-
49
- it('returns true when file is fresh (within TTL)', async () => {
50
- const file = { timestamp: Date.now(), data: makeSampleMetadata() };
51
- vi.mocked(readFile).mockResolvedValue(JSON.stringify(file) as any);
52
-
53
- const cache = makeCache();
54
- await expect(cache.isValid()).resolves.toBe(true);
55
- });
56
-
57
- it('returns false when file has expired (timestamp older than TTL)', async () => {
58
- const expiredTimestamp = Date.now() - (TTL + 1) * 1000;
59
- const file = { timestamp: expiredTimestamp, data: makeSampleMetadata() };
60
- vi.mocked(readFile).mockResolvedValue(JSON.stringify(file) as any);
61
-
62
- const cache = makeCache();
63
- await expect(cache.isValid()).resolves.toBe(false);
64
- });
65
- });
66
-
67
- // ---------------------------------------------------------------------------
68
- // load()
69
- // ---------------------------------------------------------------------------
70
-
71
- describe('MetadataCache – load()', () => {
72
- it('returns null when file does not exist', async () => {
73
- vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
74
-
75
- const cache = makeCache();
76
- await expect(cache.load()).resolves.toBeNull();
77
- });
78
-
79
- it('returns CachedMetadata when file exists', async () => {
80
- const metadata = makeSampleMetadata();
81
- const file = { timestamp: Date.now(), data: metadata };
82
- vi.mocked(readFile).mockResolvedValue(JSON.stringify(file) as any);
83
-
84
- const cache = makeCache();
85
- const result = await cache.load();
86
- expect(result).toEqual(metadata);
87
- });
88
- });
89
-
90
- // ---------------------------------------------------------------------------
91
- // save()
92
- // ---------------------------------------------------------------------------
93
-
94
- describe('MetadataCache – save()', () => {
95
- it('calls mkdir with recursive:true and writeFile with JSON content', async () => {
96
- vi.mocked(mkdir).mockResolvedValue(undefined);
97
- vi.mocked(writeFile).mockResolvedValue(undefined);
98
-
99
- const cache = makeCache();
100
- const metadata = makeSampleMetadata();
101
- await cache.save(metadata);
102
-
103
- expect(mkdir).toHaveBeenCalledWith(CACHE_DIR, { recursive: true });
104
- expect(writeFile).toHaveBeenCalledOnce();
105
-
106
- // Verify the written JSON contains our data
107
- const writtenContent = vi.mocked(writeFile).mock.calls[0][1] as string;
108
- const parsed = JSON.parse(writtenContent) as { timestamp: number; data: CachedMetadata };
109
- expect(parsed.data).toEqual(metadata);
110
- });
111
-
112
- it('writes a timestamp to the cache file', async () => {
113
- vi.mocked(mkdir).mockResolvedValue(undefined);
114
- vi.mocked(writeFile).mockResolvedValue(undefined);
115
-
116
- const before = Date.now();
117
- const cache = makeCache();
118
- await cache.save(makeSampleMetadata());
119
- const after = Date.now();
120
-
121
- const writtenContent = vi.mocked(writeFile).mock.calls[0][1] as string;
122
- const parsed = JSON.parse(writtenContent) as { timestamp: number };
123
- expect(parsed.timestamp).toBeGreaterThanOrEqual(before);
124
- expect(parsed.timestamp).toBeLessThanOrEqual(after);
125
- });
126
- });
127
-
128
- // ---------------------------------------------------------------------------
129
- // invalidate()
130
- // ---------------------------------------------------------------------------
131
-
132
- describe('MetadataCache – invalidate()', () => {
133
- it('calls unlink on the cache file', async () => {
134
- vi.mocked(unlink).mockResolvedValue(undefined);
135
-
136
- const cache = makeCache();
137
- await cache.invalidate();
138
-
139
- expect(unlink).toHaveBeenCalledOnce();
140
- const calledPath = vi.mocked(unlink).mock.calls[0][0] as string;
141
- expect(calledPath).toContain('metadata.json');
142
- });
143
-
144
- it('does not throw when file does not exist (unlink rejects)', async () => {
145
- vi.mocked(unlink).mockRejectedValue(new Error('ENOENT'));
146
-
147
- const cache = makeCache();
148
- await expect(cache.invalidate()).resolves.toBeUndefined();
149
- });
150
- });
151
-
152
- // ---------------------------------------------------------------------------
153
- // loadIfValid()
154
- // ---------------------------------------------------------------------------
155
-
156
- describe('MetadataCache – loadIfValid()', () => {
157
- it('returns null when file does not exist', async () => {
158
- vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
159
-
160
- const cache = makeCache();
161
- await expect(cache.loadIfValid()).resolves.toBeNull();
162
- });
163
-
164
- it('returns data when file is fresh (within TTL)', async () => {
165
- const metadata = makeSampleMetadata();
166
- const file = { timestamp: Date.now(), data: metadata };
167
- vi.mocked(readFile).mockResolvedValue(JSON.stringify(file) as any);
168
-
169
- const cache = makeCache();
170
- const result = await cache.loadIfValid();
171
- expect(result).toEqual(metadata);
172
- });
173
-
174
- it('returns null when file has expired', async () => {
175
- const expiredTimestamp = Date.now() - (TTL + 1) * 1000;
176
- const metadata = makeSampleMetadata();
177
- const file = { timestamp: expiredTimestamp, data: metadata };
178
- vi.mocked(readFile).mockResolvedValue(JSON.stringify(file) as any);
179
-
180
- const cache = makeCache();
181
- await expect(cache.loadIfValid()).resolves.toBeNull();
182
- });
183
-
184
- it('reads from metadata.json path', async () => {
185
- vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
186
-
187
- const cache = makeCache();
188
- await cache.loadIfValid();
189
-
190
- expect(readFile).toHaveBeenCalledOnce();
191
- const calledPath = vi.mocked(readFile).mock.calls[0][0] as string;
192
- expect(calledPath).toContain('metadata.json');
193
- expect(calledPath).not.toContain('issue_fields.json');
194
- });
195
- });
196
-
197
- // ---------------------------------------------------------------------------
198
- // loadIssueFields()
199
- // ---------------------------------------------------------------------------
200
-
201
- describe('MetadataCache – loadIssueFields()', () => {
202
- it('returns null when file does not exist', async () => {
203
- vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
204
-
205
- const cache = makeCache();
206
- await expect(cache.loadIssueFields()).resolves.toBeNull();
207
- });
208
-
209
- it('returns fields array when file is fresh', async () => {
210
- const fields = ['id', 'summary', 'status'];
211
- const file = { timestamp: Date.now(), fields };
212
- vi.mocked(readFile).mockResolvedValue(JSON.stringify(file) as any);
213
-
214
- const cache = makeCache();
215
- const result = await cache.loadIssueFields();
216
- expect(result).toEqual(fields);
217
- });
218
-
219
- it('returns null when file has expired', async () => {
220
- const expiredTimestamp = Date.now() - (TTL + 1) * 1000;
221
- const file = { timestamp: expiredTimestamp, fields: ['id', 'summary'] };
222
- vi.mocked(readFile).mockResolvedValue(JSON.stringify(file) as any);
223
-
224
- const cache = makeCache();
225
- await expect(cache.loadIssueFields()).resolves.toBeNull();
226
- });
227
- });
228
-
229
- // ---------------------------------------------------------------------------
230
- // saveIssueFields()
231
- // ---------------------------------------------------------------------------
232
-
233
- describe('MetadataCache – saveIssueFields()', () => {
234
- it('calls mkdir with recursive:true and writes to issue_fields.json', async () => {
235
- vi.mocked(mkdir).mockResolvedValue(undefined);
236
- vi.mocked(writeFile).mockResolvedValue(undefined);
237
-
238
- const cache = makeCache();
239
- const fields = ['id', 'summary', 'status', 'priority'];
240
- await cache.saveIssueFields(fields);
241
-
242
- expect(mkdir).toHaveBeenCalledWith(CACHE_DIR, { recursive: true });
243
- expect(writeFile).toHaveBeenCalledOnce();
244
-
245
- const calledPath = vi.mocked(writeFile).mock.calls[0][0] as string;
246
- expect(calledPath).toContain('issue_fields.json');
247
- });
248
-
249
- it('written JSON contains the fields array and a timestamp', async () => {
250
- vi.mocked(mkdir).mockResolvedValue(undefined);
251
- vi.mocked(writeFile).mockResolvedValue(undefined);
252
-
253
- const cache = makeCache();
254
- const fields = ['id', 'summary', 'status'];
255
- const before = Date.now();
256
- await cache.saveIssueFields(fields);
257
- const after = Date.now();
258
-
259
- const writtenContent = vi.mocked(writeFile).mock.calls[0][1] as string;
260
- const parsed = JSON.parse(writtenContent) as { timestamp: number; fields: string[] };
261
- expect(parsed.fields).toEqual(fields);
262
- expect(parsed.timestamp).toBeGreaterThanOrEqual(before);
263
- expect(parsed.timestamp).toBeLessThanOrEqual(after);
264
- });
265
- });