@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,166 +0,0 @@
1
- export function makeResponse(status: number, body: string): Response {
2
- return {
3
- ok: status >= 200 && status < 300,
4
- status,
5
- statusText: `Status ${status}`,
6
- text: () => Promise.resolve(body),
7
- headers: { get: (_key: string) => null },
8
- } as unknown as Response;
9
- }
10
-
11
- // Typ für das Result-Objekt das die Tools zurückgeben
12
- export interface ToolResult {
13
- content: Array<{ type: string; text: string }>;
14
- isError?: boolean;
15
- }
16
-
17
- import { z } from 'zod';
18
-
19
- // Handler-Typ (args ist der Zod-geparste Input)
20
- type ToolHandler = (args: Record<string, unknown>) => Promise<ToolResult>;
21
-
22
- interface ToolDefinition {
23
- inputSchema?: z.ZodTypeAny;
24
- [key: string]: unknown;
25
- }
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, variables?: Record<string, string>) => Promise<ResourceResult>;
43
-
44
- interface ResourceEntry {
45
- handler: ResourceHandler;
46
- /** URI template pattern, e.g. 'mantis://projects/{id}'. Present for template resources only. */
47
- template?: string;
48
- }
49
-
50
- function matchTemplate(template: string, uri: string): Record<string, string> | null {
51
- const names = [...template.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]!);
52
- const pattern = template.replace(/\{[^}]+\}/g, '([^/]+)');
53
- const match = uri.match(new RegExp(`^${pattern}$`));
54
- if (!match) return null;
55
- return Object.fromEntries(names.map((name, i) => [name, match[i + 1]!]));
56
- }
57
-
58
- interface PromptDefinition {
59
- argsSchema?: Record<string, z.ZodTypeAny>;
60
- [key: string]: unknown;
61
- }
62
-
63
- export class MockMcpServer {
64
- private readonly handlers = new Map<string, ToolHandler>();
65
- private readonly schemas = new Map<string, z.ZodTypeAny>();
66
- private readonly promptHandlers = new Map<string, PromptHandler>();
67
- private readonly resourceEntries = new Map<string, ResourceEntry>();
68
-
69
- // Nachahmt McpServer.registerTool – fängt Handler und Schema ein
70
- registerTool(name: string, definition: ToolDefinition, handler: ToolHandler): void {
71
- this.handlers.set(name, handler);
72
- if (definition.inputSchema) {
73
- this.schemas.set(name, definition.inputSchema);
74
- }
75
- }
76
-
77
- // Nachahmt McpServer.registerPrompt
78
- registerPrompt(name: string, _definition: PromptDefinition, handler: PromptHandler): void {
79
- this.promptHandlers.set(name, handler);
80
- }
81
-
82
- /**
83
- * Ruft den Handler auf. Wenn `validate: true`, wird der Input zuerst
84
- * durch das Zod-Schema geparst (wie der echte MCP-Server es tut).
85
- * Das ermöglicht Tests für Coercion und Validierungsfehler.
86
- */
87
- async callTool(
88
- name: string,
89
- args: Record<string, unknown> = {},
90
- options: { validate?: boolean } = {},
91
- ): Promise<ToolResult> {
92
- const handler = this.handlers.get(name);
93
- if (!handler) throw new Error(`Tool not registered: ${name}`);
94
-
95
- const schema = this.schemas.get(name);
96
- if (schema) {
97
- const parsed = schema.safeParse(args);
98
- if (!parsed.success) {
99
- if (options.validate) {
100
- return {
101
- content: [{ type: 'text', text: `Validation error: ${parsed.error.message}` }],
102
- isError: true,
103
- };
104
- }
105
- } else {
106
- return handler(parsed.data as Record<string, unknown>);
107
- }
108
- }
109
-
110
- return handler(args);
111
- }
112
-
113
- callPrompt(name: string, args: Record<string, unknown> = {}): PromptResult {
114
- const handler = this.promptHandlers.get(name);
115
- if (!handler) throw new Error(`Prompt not registered: ${name}`);
116
- return handler(args);
117
- }
118
-
119
- hasToolRegistered(name: string): boolean {
120
- return this.handlers.has(name);
121
- }
122
-
123
- hasPromptRegistered(name: string): boolean {
124
- return this.promptHandlers.has(name);
125
- }
126
-
127
- registeredToolNames(): string[] {
128
- return [...this.handlers.keys()];
129
- }
130
-
131
- registeredPromptNames(): string[] {
132
- return [...this.promptHandlers.keys()];
133
- }
134
-
135
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
136
- registerResource(name: string, uriOrTemplate: any, _config: unknown, handler: ResourceHandler): void {
137
- if (typeof uriOrTemplate === 'string') {
138
- this.resourceEntries.set(uriOrTemplate, { handler });
139
- } else {
140
- // ResourceTemplate — uriTemplate getter returns a UriTemplate object; .toString() gives the pattern string
141
- const templateStr: string = uriOrTemplate.uriTemplate?.toString() ?? name;
142
- this.resourceEntries.set(templateStr, { handler, template: templateStr });
143
- }
144
- }
145
-
146
- async callResource(uri: string): Promise<ResourceResult> {
147
- const exact = this.resourceEntries.get(uri);
148
- if (exact) return exact.handler(new URL(uri), {});
149
-
150
- for (const entry of this.resourceEntries.values()) {
151
- if (!entry.template) continue;
152
- const variables = matchTemplate(entry.template, uri);
153
- if (variables) return entry.handler(new URL(uri), variables);
154
- }
155
-
156
- throw new Error(`Resource not registered: ${uri}`);
157
- }
158
-
159
- hasResourceRegistered(uri: string): boolean {
160
- return this.resourceEntries.has(uri);
161
- }
162
-
163
- registeredResourceUris(): string[] {
164
- return [...this.resourceEntries.keys()];
165
- }
166
- }
@@ -1,84 +0,0 @@
1
- import { vi } from 'vitest';
2
- import type { VectorStore, VectorStoreItem } from '../../src/search/store.js';
3
- import type { Embedder } from '../../src/search/embedder.js';
4
-
5
- // ---------------------------------------------------------------------------
6
- // Shared constants
7
- // ---------------------------------------------------------------------------
8
-
9
- export const MOCK_VECTOR = Array(384).fill(0.1) as number[];
10
-
11
- // ---------------------------------------------------------------------------
12
- // makeMockStore
13
- // ---------------------------------------------------------------------------
14
-
15
- export interface MockStoreItem {
16
- id: number;
17
- score?: number;
18
- updated_at?: string;
19
- }
20
-
21
- export function makeMockStore(options?: {
22
- lastSyncedAt?: string | null;
23
- itemCount?: number;
24
- lastKnownTotal?: number | null;
25
- items?: MockStoreItem[];
26
- }): VectorStore {
27
- const lastSyncedAt = options?.lastSyncedAt ?? null;
28
- const seedItems = options?.items ?? null;
29
- const addedItems: VectorStoreItem[] = [];
30
- const itemMap = new Map<number, VectorStoreItem>(
31
- (seedItems ?? []).map(i => [i.id, {
32
- id: i.id,
33
- vector: MOCK_VECTOR,
34
- metadata: { summary: `Issue ${i.id}`, updated_at: i.updated_at },
35
- }])
36
- );
37
- let count = options?.itemCount ?? seedItems?.length ?? 0;
38
-
39
- return {
40
- add: vi.fn(async (item: VectorStoreItem) => {
41
- addedItems.push(item);
42
- count++;
43
- }),
44
- addBatch: vi.fn(async (items: VectorStoreItem[]) => {
45
- for (const item of items) {
46
- addedItems.push(item);
47
- }
48
- count += items.length;
49
- }),
50
- search: vi.fn(async (_vec: number[], topN: number) => {
51
- if (seedItems) {
52
- return seedItems.slice(0, topN).map(i => ({ id: i.id, score: i.score ?? 0.9 }));
53
- }
54
- return Array.from({ length: Math.min(topN, count) }, (_, i) => ({
55
- id: i + 1,
56
- score: 1 - i * 0.1,
57
- }));
58
- }),
59
- getItem: vi.fn(async (id: number) => itemMap.get(id) ?? null),
60
- delete: vi.fn(async () => {}),
61
- count: vi.fn(async () => count),
62
- clear: vi.fn(async () => {
63
- addedItems.splice(0);
64
- count = 0;
65
- }),
66
- getLastSyncedAt: vi.fn(async () => lastSyncedAt),
67
- setLastSyncedAt: vi.fn(async () => {}),
68
- resetLastSyncedAt: vi.fn(async () => {}),
69
- getLastKnownTotal: vi.fn(async () => options?.lastKnownTotal ?? null),
70
- setLastKnownTotal: vi.fn(async () => {}),
71
- flush: vi.fn(async () => {}),
72
- };
73
- }
74
-
75
- // ---------------------------------------------------------------------------
76
- // makeMockEmbedder
77
- // ---------------------------------------------------------------------------
78
-
79
- export function makeMockEmbedder(): Embedder {
80
- return {
81
- embed: vi.fn(async () => MOCK_VECTOR),
82
- embedBatch: vi.fn(async (texts: string[]) => texts.map(() => MOCK_VECTOR)),
83
- } as unknown as Embedder;
84
- }
@@ -1,242 +0,0 @@
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
- });