@dpesch/mantisbt-mcp-server 1.5.8 → 1.6.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.
- package/CHANGELOG.md +34 -0
- package/README.de.md +28 -12
- package/README.md +28 -12
- package/dist/client.js +37 -23
- package/dist/config.js +41 -50
- package/dist/constants.js +12 -0
- package/dist/index.js +23 -13
- package/dist/prompts/index.js +112 -0
- package/dist/resources/index.js +29 -0
- package/dist/tools/config.js +44 -40
- package/dist/tools/issues.js +22 -6
- package/docs/cookbook.de.md +1664 -0
- package/docs/cookbook.md +1664 -0
- package/docs/examples.de.md +200 -0
- package/docs/examples.md +200 -0
- package/package.json +6 -2
- package/server.json +2 -2
- package/tests/client.test.ts +70 -0
- package/tests/config.test.ts +47 -37
- package/tests/helpers/mock-server.ts +61 -0
- package/tests/prompts/prompts.test.ts +242 -0
- package/tests/resources/resources.test.ts +192 -0
- package/tests/tools/issues.test.ts +26 -10
|
@@ -24,9 +24,33 @@ interface ToolDefinition {
|
|
|
24
24
|
[key: string]: unknown;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export interface PromptMessage {
|
|
28
|
+
role: string;
|
|
29
|
+
content: { type: string; text: string };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PromptResult {
|
|
33
|
+
messages: PromptMessage[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ResourceResult {
|
|
37
|
+
contents: Array<{ uri: string; mimeType?: string; text: string }>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type PromptHandler = (args: Record<string, unknown>) => PromptResult;
|
|
41
|
+
|
|
42
|
+
type ResourceHandler = (uri: URL) => Promise<ResourceResult>;
|
|
43
|
+
|
|
44
|
+
interface PromptDefinition {
|
|
45
|
+
argsSchema?: Record<string, z.ZodTypeAny>;
|
|
46
|
+
[key: string]: unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
27
49
|
export class MockMcpServer {
|
|
28
50
|
private readonly handlers = new Map<string, ToolHandler>();
|
|
29
51
|
private readonly schemas = new Map<string, z.ZodTypeAny>();
|
|
52
|
+
private readonly promptHandlers = new Map<string, PromptHandler>();
|
|
53
|
+
private readonly resourceHandlers = new Map<string, ResourceHandler>();
|
|
30
54
|
|
|
31
55
|
// Nachahmt McpServer.registerTool – fängt Handler und Schema ein
|
|
32
56
|
registerTool(name: string, definition: ToolDefinition, handler: ToolHandler): void {
|
|
@@ -36,6 +60,11 @@ export class MockMcpServer {
|
|
|
36
60
|
}
|
|
37
61
|
}
|
|
38
62
|
|
|
63
|
+
// Nachahmt McpServer.registerPrompt
|
|
64
|
+
registerPrompt(name: string, _definition: PromptDefinition, handler: PromptHandler): void {
|
|
65
|
+
this.promptHandlers.set(name, handler);
|
|
66
|
+
}
|
|
67
|
+
|
|
39
68
|
/**
|
|
40
69
|
* Ruft den Handler auf. Wenn `validate: true`, wird der Input zuerst
|
|
41
70
|
* durch das Zod-Schema geparst (wie der echte MCP-Server es tut).
|
|
@@ -66,11 +95,43 @@ export class MockMcpServer {
|
|
|
66
95
|
return handler(args);
|
|
67
96
|
}
|
|
68
97
|
|
|
98
|
+
callPrompt(name: string, args: Record<string, unknown> = {}): PromptResult {
|
|
99
|
+
const handler = this.promptHandlers.get(name);
|
|
100
|
+
if (!handler) throw new Error(`Prompt not registered: ${name}`);
|
|
101
|
+
return handler(args);
|
|
102
|
+
}
|
|
103
|
+
|
|
69
104
|
hasToolRegistered(name: string): boolean {
|
|
70
105
|
return this.handlers.has(name);
|
|
71
106
|
}
|
|
72
107
|
|
|
108
|
+
hasPromptRegistered(name: string): boolean {
|
|
109
|
+
return this.promptHandlers.has(name);
|
|
110
|
+
}
|
|
111
|
+
|
|
73
112
|
registeredToolNames(): string[] {
|
|
74
113
|
return [...this.handlers.keys()];
|
|
75
114
|
}
|
|
115
|
+
|
|
116
|
+
registeredPromptNames(): string[] {
|
|
117
|
+
return [...this.promptHandlers.keys()];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
registerResource(name: string, uri: string, _config: unknown, handler: ResourceHandler): void {
|
|
121
|
+
this.resourceHandlers.set(uri, handler);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async callResource(uri: string): Promise<ResourceResult> {
|
|
125
|
+
const handler = this.resourceHandlers.get(uri);
|
|
126
|
+
if (!handler) throw new Error(`Resource not registered: ${uri}`);
|
|
127
|
+
return handler(new URL(uri));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
hasResourceRegistered(uri: string): boolean {
|
|
131
|
+
return this.resourceHandlers.has(uri);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
registeredResourceUris(): string[] {
|
|
135
|
+
return [...this.resourceHandlers.keys()];
|
|
136
|
+
}
|
|
76
137
|
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { registerPrompts } from '../../src/prompts/index.js';
|
|
3
|
+
import { MockMcpServer } from '../helpers/mock-server.js';
|
|
4
|
+
|
|
5
|
+
let mockServer: MockMcpServer;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
mockServer = new MockMcpServer();
|
|
9
|
+
registerPrompts(mockServer as never);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Registration
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
describe('prompt registration', () => {
|
|
17
|
+
it('registers create-bug-report', () => {
|
|
18
|
+
expect(mockServer.hasPromptRegistered('create-bug-report')).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('registers create-feature-request', () => {
|
|
22
|
+
expect(mockServer.hasPromptRegistered('create-feature-request')).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('registers summarize-issue', () => {
|
|
26
|
+
expect(mockServer.hasPromptRegistered('summarize-issue')).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('registers project-status', () => {
|
|
30
|
+
expect(mockServer.hasPromptRegistered('project-status')).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// create-bug-report
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
describe('create-bug-report', () => {
|
|
39
|
+
it('returns a single user message', () => {
|
|
40
|
+
const result = mockServer.callPrompt('create-bug-report', {
|
|
41
|
+
project_id: 1,
|
|
42
|
+
category: 'General',
|
|
43
|
+
summary: 'Login fails',
|
|
44
|
+
description: 'Cannot log in after password reset',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(result.messages).toHaveLength(1);
|
|
48
|
+
expect(result.messages[0]!.role).toBe('user');
|
|
49
|
+
expect(result.messages[0]!.content.type).toBe('text');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('includes project_id, category, summary, and description in the text', () => {
|
|
53
|
+
const result = mockServer.callPrompt('create-bug-report', {
|
|
54
|
+
project_id: 7,
|
|
55
|
+
category: 'Authentication',
|
|
56
|
+
summary: 'Session expires too early',
|
|
57
|
+
description: 'Session is invalidated after 5 minutes of inactivity',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const text = result.messages[0]!.content.text;
|
|
61
|
+
expect(text).toContain('project 7');
|
|
62
|
+
expect(text).toContain('Authentication');
|
|
63
|
+
expect(text).toContain('Session expires too early');
|
|
64
|
+
expect(text).toContain('Session is invalidated after 5 minutes');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('includes optional steps_to_reproduce when provided', () => {
|
|
68
|
+
const result = mockServer.callPrompt('create-bug-report', {
|
|
69
|
+
project_id: 1,
|
|
70
|
+
category: 'General',
|
|
71
|
+
summary: 'Bug',
|
|
72
|
+
description: 'Desc',
|
|
73
|
+
steps_to_reproduce: '1. Open app\n2. Click login',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(result.messages[0]!.content.text).toContain('Steps to reproduce');
|
|
77
|
+
expect(result.messages[0]!.content.text).toContain('1. Open app');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('omits optional sections when not provided', () => {
|
|
81
|
+
const result = mockServer.callPrompt('create-bug-report', {
|
|
82
|
+
project_id: 1,
|
|
83
|
+
category: 'General',
|
|
84
|
+
summary: 'Bug',
|
|
85
|
+
description: 'Desc',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const text = result.messages[0]!.content.text;
|
|
89
|
+
expect(text).not.toContain('Steps to reproduce');
|
|
90
|
+
expect(text).not.toContain('Expected behavior');
|
|
91
|
+
expect(text).not.toContain('Actual behavior');
|
|
92
|
+
expect(text).not.toContain('Environment');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('includes all optional fields when provided', () => {
|
|
96
|
+
const result = mockServer.callPrompt('create-bug-report', {
|
|
97
|
+
project_id: 1,
|
|
98
|
+
category: 'UI',
|
|
99
|
+
summary: 'Button broken',
|
|
100
|
+
description: 'Save button does nothing',
|
|
101
|
+
steps_to_reproduce: 'Click Save',
|
|
102
|
+
expected: 'Form is saved',
|
|
103
|
+
actual: 'Nothing happens',
|
|
104
|
+
environment: 'Chrome 120, Windows 11',
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const text = result.messages[0]!.content.text;
|
|
108
|
+
expect(text).toContain('Steps to reproduce');
|
|
109
|
+
expect(text).toContain('Expected behavior');
|
|
110
|
+
expect(text).toContain('Actual behavior');
|
|
111
|
+
expect(text).toContain('Environment');
|
|
112
|
+
expect(text).toContain('Chrome 120');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('mentions create_issue tool', () => {
|
|
116
|
+
const result = mockServer.callPrompt('create-bug-report', {
|
|
117
|
+
project_id: 1,
|
|
118
|
+
category: 'General',
|
|
119
|
+
summary: 'Bug',
|
|
120
|
+
description: 'Desc',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(result.messages[0]!.content.text).toContain('create_issue');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// create-feature-request
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
describe('create-feature-request', () => {
|
|
132
|
+
it('returns a single user message', () => {
|
|
133
|
+
const result = mockServer.callPrompt('create-feature-request', {
|
|
134
|
+
project_id: 2,
|
|
135
|
+
category: 'General',
|
|
136
|
+
summary: 'Dark mode',
|
|
137
|
+
description: 'Add a dark mode toggle to the UI',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(result.messages).toHaveLength(1);
|
|
141
|
+
expect(result.messages[0]!.role).toBe('user');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('includes project_id, category, summary, and description', () => {
|
|
145
|
+
const result = mockServer.callPrompt('create-feature-request', {
|
|
146
|
+
project_id: 5,
|
|
147
|
+
category: 'UX',
|
|
148
|
+
summary: 'Export to CSV',
|
|
149
|
+
description: 'Allow exporting issue lists as CSV',
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const text = result.messages[0]!.content.text;
|
|
153
|
+
expect(text).toContain('project 5');
|
|
154
|
+
expect(text).toContain('UX');
|
|
155
|
+
expect(text).toContain('Export to CSV');
|
|
156
|
+
expect(text).toContain('Allow exporting issue lists');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('includes use_case when provided', () => {
|
|
160
|
+
const result = mockServer.callPrompt('create-feature-request', {
|
|
161
|
+
project_id: 1,
|
|
162
|
+
category: 'General',
|
|
163
|
+
summary: 'Feature',
|
|
164
|
+
description: 'Desc',
|
|
165
|
+
use_case: 'Needed for monthly reporting',
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(result.messages[0]!.content.text).toContain('Needed for monthly reporting');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('omits use_case section when not provided', () => {
|
|
172
|
+
const result = mockServer.callPrompt('create-feature-request', {
|
|
173
|
+
project_id: 1,
|
|
174
|
+
category: 'General',
|
|
175
|
+
summary: 'Feature',
|
|
176
|
+
description: 'Desc',
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(result.messages[0]!.content.text).not.toContain('Use case');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('mentions create_issue tool', () => {
|
|
183
|
+
const result = mockServer.callPrompt('create-feature-request', {
|
|
184
|
+
project_id: 1,
|
|
185
|
+
category: 'General',
|
|
186
|
+
summary: 'Feature',
|
|
187
|
+
description: 'Desc',
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(result.messages[0]!.content.text).toContain('create_issue');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// summarize-issue
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
describe('summarize-issue', () => {
|
|
199
|
+
it('returns a single user message', () => {
|
|
200
|
+
const result = mockServer.callPrompt('summarize-issue', { issue_id: 42 });
|
|
201
|
+
|
|
202
|
+
expect(result.messages).toHaveLength(1);
|
|
203
|
+
expect(result.messages[0]!.role).toBe('user');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('includes the issue ID in the text', () => {
|
|
207
|
+
const result = mockServer.callPrompt('summarize-issue', { issue_id: 1234 });
|
|
208
|
+
|
|
209
|
+
expect(result.messages[0]!.content.text).toContain('1234');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('mentions get_issue tool', () => {
|
|
213
|
+
const result = mockServer.callPrompt('summarize-issue', { issue_id: 1 });
|
|
214
|
+
|
|
215
|
+
expect(result.messages[0]!.content.text).toContain('get_issue');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// project-status
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
describe('project-status', () => {
|
|
224
|
+
it('returns a single user message', () => {
|
|
225
|
+
const result = mockServer.callPrompt('project-status', { project_id: 3 });
|
|
226
|
+
|
|
227
|
+
expect(result.messages).toHaveLength(1);
|
|
228
|
+
expect(result.messages[0]!.role).toBe('user');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('includes the project ID in the text', () => {
|
|
232
|
+
const result = mockServer.callPrompt('project-status', { project_id: 99 });
|
|
233
|
+
|
|
234
|
+
expect(result.messages[0]!.content.text).toContain('99');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('mentions list_issues tool', () => {
|
|
238
|
+
const result = mockServer.callPrompt('project-status', { project_id: 1 });
|
|
239
|
+
|
|
240
|
+
expect(result.messages[0]!.content.text).toContain('list_issues');
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { MantisClient } from '../../src/client.js';
|
|
3
|
+
import { MetadataCache } from '../../src/cache.js';
|
|
4
|
+
import { registerResources } from '../../src/resources/index.js';
|
|
5
|
+
import { MockMcpServer, makeResponse } from '../helpers/mock-server.js';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Inline fixtures
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
const USER_FIXTURE = { id: 1, name: 'jsmith', real_name: 'John Smith', email: 'jsmith@example.com' };
|
|
12
|
+
|
|
13
|
+
const PROJECTS_FIXTURE = [
|
|
14
|
+
{ id: 10, name: 'Alpha', enabled: true },
|
|
15
|
+
{ id: 11, name: 'Beta', enabled: true },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const ENUM_FIXTURE = {
|
|
19
|
+
configs: [
|
|
20
|
+
{ option: 'severity_enum_string', value: '10:feature,50:minor,80:block' },
|
|
21
|
+
{ option: 'status_enum_string', value: '10:new,80:resolved,90:closed' },
|
|
22
|
+
{ option: 'priority_enum_string', value: '10:none,30:normal,60:immediate' },
|
|
23
|
+
{ option: 'resolution_enum_string', value: '10:open,20:fixed' },
|
|
24
|
+
{ option: 'reproducibility_enum_string', value: '10:always,70:have not tried' },
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Setup
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
let mockServer: MockMcpServer;
|
|
33
|
+
let client: MantisClient;
|
|
34
|
+
let cache: MetadataCache;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
mockServer = new MockMcpServer();
|
|
38
|
+
client = new MantisClient('https://mantis.example.com', 'test-token');
|
|
39
|
+
cache = new MetadataCache('/tmp/cache-resources-test', 86400);
|
|
40
|
+
registerResources(mockServer as never, client, cache);
|
|
41
|
+
vi.stubGlobal('fetch', vi.fn());
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(async () => {
|
|
45
|
+
vi.unstubAllGlobals();
|
|
46
|
+
await cache.invalidate();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Registration
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
describe('resource registration', () => {
|
|
54
|
+
it('registers mantis://me', () => {
|
|
55
|
+
expect(mockServer.hasResourceRegistered('mantis://me')).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('registers mantis://projects', () => {
|
|
59
|
+
expect(mockServer.hasResourceRegistered('mantis://projects')).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('registers mantis://enums', () => {
|
|
63
|
+
expect(mockServer.hasResourceRegistered('mantis://enums')).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// mantis://me
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
describe('mantis://me', () => {
|
|
72
|
+
it('returns a single content item with application/json', async () => {
|
|
73
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(USER_FIXTURE)));
|
|
74
|
+
|
|
75
|
+
const result = await mockServer.callResource('mantis://me');
|
|
76
|
+
|
|
77
|
+
expect(result.contents).toHaveLength(1);
|
|
78
|
+
expect(result.contents[0]!.mimeType).toBe('application/json');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('sets uri to mantis://me', async () => {
|
|
82
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(USER_FIXTURE)));
|
|
83
|
+
|
|
84
|
+
const result = await mockServer.callResource('mantis://me');
|
|
85
|
+
|
|
86
|
+
expect(result.contents[0]!.uri).toBe('mantis://me');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('returns valid JSON with id and name', async () => {
|
|
90
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(USER_FIXTURE)));
|
|
91
|
+
|
|
92
|
+
const result = await mockServer.callResource('mantis://me');
|
|
93
|
+
|
|
94
|
+
const parsed = JSON.parse(result.contents[0]!.text) as { id: number; name: string };
|
|
95
|
+
expect(parsed.id).toBe(USER_FIXTURE.id);
|
|
96
|
+
expect(parsed.name).toBe(USER_FIXTURE.name);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// mantis://projects
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
describe('mantis://projects', () => {
|
|
105
|
+
it('returns a single content item with application/json', async () => {
|
|
106
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ projects: PROJECTS_FIXTURE })));
|
|
107
|
+
|
|
108
|
+
const result = await mockServer.callResource('mantis://projects');
|
|
109
|
+
|
|
110
|
+
expect(result.contents).toHaveLength(1);
|
|
111
|
+
expect(result.contents[0]!.mimeType).toBe('application/json');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('sets uri to mantis://projects', async () => {
|
|
115
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ projects: PROJECTS_FIXTURE })));
|
|
116
|
+
|
|
117
|
+
const result = await mockServer.callResource('mantis://projects');
|
|
118
|
+
|
|
119
|
+
expect(result.contents[0]!.uri).toBe('mantis://projects');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('returns a JSON array of projects from live API when cache is empty', async () => {
|
|
123
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ projects: PROJECTS_FIXTURE })));
|
|
124
|
+
|
|
125
|
+
const result = await mockServer.callResource('mantis://projects');
|
|
126
|
+
|
|
127
|
+
const parsed = JSON.parse(result.contents[0]!.text) as unknown[];
|
|
128
|
+
expect(Array.isArray(parsed)).toBe(true);
|
|
129
|
+
expect(parsed).toHaveLength(2);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('serves from cache without calling the API when cache is valid', async () => {
|
|
133
|
+
await cache.save({
|
|
134
|
+
timestamp: Date.now(),
|
|
135
|
+
projects: PROJECTS_FIXTURE,
|
|
136
|
+
byProject: {},
|
|
137
|
+
tags: [],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const result = await mockServer.callResource('mantis://projects');
|
|
141
|
+
|
|
142
|
+
expect(vi.mocked(fetch)).not.toHaveBeenCalled();
|
|
143
|
+
const parsed = JSON.parse(result.contents[0]!.text) as unknown[];
|
|
144
|
+
expect(parsed).toHaveLength(2);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// mantis://enums
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
describe('mantis://enums', () => {
|
|
153
|
+
it('returns a single content item with application/json', async () => {
|
|
154
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(ENUM_FIXTURE)));
|
|
155
|
+
|
|
156
|
+
const result = await mockServer.callResource('mantis://enums');
|
|
157
|
+
|
|
158
|
+
expect(result.contents).toHaveLength(1);
|
|
159
|
+
expect(result.contents[0]!.mimeType).toBe('application/json');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('sets uri to mantis://enums', async () => {
|
|
163
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(ENUM_FIXTURE)));
|
|
164
|
+
|
|
165
|
+
const result = await mockServer.callResource('mantis://enums');
|
|
166
|
+
|
|
167
|
+
expect(result.contents[0]!.uri).toBe('mantis://enums');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('returns parsed enum groups with id and name entries', async () => {
|
|
171
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(ENUM_FIXTURE)));
|
|
172
|
+
|
|
173
|
+
const result = await mockServer.callResource('mantis://enums');
|
|
174
|
+
|
|
175
|
+
const parsed = JSON.parse(result.contents[0]!.text) as Record<string, Array<{ id: number; name: string }>>;
|
|
176
|
+
for (const key of ['severity', 'priority', 'status', 'resolution', 'reproducibility']) {
|
|
177
|
+
expect(Array.isArray(parsed[key])).toBe(true);
|
|
178
|
+
expect(parsed[key]!.length).toBeGreaterThan(0);
|
|
179
|
+
expect(parsed[key]![0]).toMatchObject({ id: expect.any(Number), name: expect.any(String) });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('parses severity values correctly from fixture', async () => {
|
|
184
|
+
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(ENUM_FIXTURE)));
|
|
185
|
+
|
|
186
|
+
const result = await mockServer.callResource('mantis://enums');
|
|
187
|
+
|
|
188
|
+
const parsed = JSON.parse(result.contents[0]!.text) as Record<string, Array<{ id: number; name: string }>>;
|
|
189
|
+
expect(parsed['severity']).toContainEqual({ id: 10, name: 'feature' });
|
|
190
|
+
expect(parsed['severity']).toContainEqual({ id: 50, name: 'minor' });
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -112,8 +112,10 @@ describe('create_issue', () => {
|
|
|
112
112
|
expect(mockServer.hasToolRegistered('create_issue')).toBe(true);
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
-
it('sends severity
|
|
115
|
+
it('sends severity as { id: 50 } (minor) by default when no severity is provided', async () => {
|
|
116
116
|
// Regression: omitting severity caused MantisBT to store 0 → displayed as "@0@".
|
|
117
|
+
// The server now resolves canonical English names to IDs so MantisBT language setting
|
|
118
|
+
// does not affect how the value is stored.
|
|
117
119
|
// validate: true ensures Zod defaults are applied before the handler runs.
|
|
118
120
|
vi.mocked(fetch).mockResolvedValue(
|
|
119
121
|
makeResponse(201, JSON.stringify({ issue: { id: 100, summary: 'Test' } }))
|
|
@@ -126,7 +128,7 @@ describe('create_issue', () => {
|
|
|
126
128
|
}, { validate: true });
|
|
127
129
|
|
|
128
130
|
const body = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]!.body as string) as Record<string, unknown>;
|
|
129
|
-
expect(body.severity).toEqual({
|
|
131
|
+
expect(body.severity).toEqual({ id: 50 });
|
|
130
132
|
});
|
|
131
133
|
|
|
132
134
|
it('returns full issue object when API responds with complete issue', async () => {
|
|
@@ -137,7 +139,7 @@ describe('create_issue', () => {
|
|
|
137
139
|
|
|
138
140
|
const result = await mockServer.callTool('create_issue', {
|
|
139
141
|
summary: 'New issue', project_id: 1, category: 'General',
|
|
140
|
-
});
|
|
142
|
+
}, { validate: true });
|
|
141
143
|
|
|
142
144
|
expect(result.isError).toBeUndefined();
|
|
143
145
|
const parsed = JSON.parse(result.content[0]!.text) as typeof fullIssue;
|
|
@@ -155,7 +157,7 @@ describe('create_issue', () => {
|
|
|
155
157
|
|
|
156
158
|
const result = await mockServer.callTool('create_issue', {
|
|
157
159
|
summary: 'Created issue', project_id: 1, category: 'General',
|
|
158
|
-
});
|
|
160
|
+
}, { validate: true });
|
|
159
161
|
|
|
160
162
|
expect(result.isError).toBeUndefined();
|
|
161
163
|
const parsed = JSON.parse(result.content[0]!.text) as typeof fullIssue;
|
|
@@ -173,7 +175,7 @@ describe('create_issue', () => {
|
|
|
173
175
|
|
|
174
176
|
const result = await mockServer.callTool('create_issue', {
|
|
175
177
|
summary: 'Test', project_id: 1, category: 'General',
|
|
176
|
-
});
|
|
178
|
+
}, { validate: true });
|
|
177
179
|
|
|
178
180
|
expect(result.isError).toBeUndefined();
|
|
179
181
|
const parsed = JSON.parse(result.content[0]!.text) as { id: number };
|
|
@@ -193,7 +195,21 @@ describe('create_issue', () => {
|
|
|
193
195
|
}, { validate: true });
|
|
194
196
|
|
|
195
197
|
const body = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]!.body as string) as Record<string, unknown>;
|
|
196
|
-
expect(body.severity).toEqual({
|
|
198
|
+
expect(body.severity).toEqual({ id: 70 });
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('returns an error for an unknown severity name', async () => {
|
|
202
|
+
const result = await mockServer.callTool('create_issue', {
|
|
203
|
+
summary: 'Test',
|
|
204
|
+
project_id: 1,
|
|
205
|
+
category: 'General',
|
|
206
|
+
severity: 'schwerer Fehler',
|
|
207
|
+
}, { validate: true });
|
|
208
|
+
|
|
209
|
+
expect(result.isError).toBe(true);
|
|
210
|
+
expect(result.content[0]!.text).toContain('schwerer Fehler');
|
|
211
|
+
expect(result.content[0]!.text).toContain('minor');
|
|
212
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
197
213
|
});
|
|
198
214
|
});
|
|
199
215
|
|
|
@@ -213,7 +229,7 @@ describe('create_issue – handler username', () => {
|
|
|
213
229
|
|
|
214
230
|
await server.callTool('create_issue', {
|
|
215
231
|
summary: 'Test', project_id: 1, category: 'General', handler: 'dom',
|
|
216
|
-
});
|
|
232
|
+
}, { validate: true });
|
|
217
233
|
|
|
218
234
|
const body = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]!.body as string) as { handler: { id: number } };
|
|
219
235
|
expect(body.handler).toEqual({ id: 7 });
|
|
@@ -230,7 +246,7 @@ describe('create_issue – handler username', () => {
|
|
|
230
246
|
|
|
231
247
|
await server.callTool('create_issue', {
|
|
232
248
|
summary: 'Test', project_id: 1, category: 'General', handler: 'John Doe',
|
|
233
|
-
});
|
|
249
|
+
}, { validate: true });
|
|
234
250
|
|
|
235
251
|
const body = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]!.body as string) as { handler: { id: number } };
|
|
236
252
|
expect(body.handler).toEqual({ id: 9 });
|
|
@@ -247,7 +263,7 @@ describe('create_issue – handler username', () => {
|
|
|
247
263
|
|
|
248
264
|
await server.callTool('create_issue', {
|
|
249
265
|
summary: 'Test', project_id: 1, category: 'General', handler: 'alice',
|
|
250
|
-
});
|
|
266
|
+
}, { validate: true });
|
|
251
267
|
|
|
252
268
|
const projectUsersCall = vi.mocked(fetch).mock.calls[0]![0] as string;
|
|
253
269
|
expect(projectUsersCall).toContain('projects/1/users');
|
|
@@ -282,7 +298,7 @@ describe('create_issue – handler username', () => {
|
|
|
282
298
|
|
|
283
299
|
await server.callTool('create_issue', {
|
|
284
300
|
summary: 'Test', project_id: 1, category: 'General', handler_id: 99, handler: 'dom',
|
|
285
|
-
});
|
|
301
|
+
}, { validate: true });
|
|
286
302
|
|
|
287
303
|
const body = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]!.body as string) as { handler: { id: number } };
|
|
288
304
|
expect(body.handler).toEqual({ id: 99 });
|