@dpesch/mantisbt-mcp-server 1.10.2 → 1.10.6

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 (49) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.de.md +2 -1
  3. package/README.md +2 -1
  4. package/dist/client.js +0 -15
  5. package/dist/tools/files.js +14 -15
  6. package/docs/cookbook.de.md +1 -1
  7. package/docs/cookbook.md +1 -1
  8. package/package.json +1 -1
  9. package/server.json +2 -2
  10. package/.gitea/PULL_REQUEST_TEMPLATE.md +0 -17
  11. package/.gitea/workflows/ci.yml +0 -95
  12. package/.github/workflows/ci.yml +0 -32
  13. package/scripts/hooks/pre-push.mjs +0 -220
  14. package/scripts/init.mjs +0 -95
  15. package/scripts/record-fixtures.ts +0 -160
  16. package/tests/cache.test.ts +0 -265
  17. package/tests/client.test.ts +0 -372
  18. package/tests/config.test.ts +0 -263
  19. package/tests/fixtures/get_current_user.json +0 -40
  20. package/tests/fixtures/get_issue.json +0 -133
  21. package/tests/fixtures/get_issue_enums.json +0 -67
  22. package/tests/fixtures/get_issue_fields_sample.json +0 -76
  23. package/tests/fixtures/get_project_categories.json +0 -157
  24. package/tests/fixtures/get_project_versions.json +0 -3
  25. package/tests/fixtures/get_project_versions_with_data.json +0 -52
  26. package/tests/fixtures/list_issues.json +0 -95
  27. package/tests/fixtures/list_projects.json +0 -65
  28. package/tests/helpers/mock-server.ts +0 -166
  29. package/tests/helpers/search-mocks.ts +0 -84
  30. package/tests/prompts/prompts.test.ts +0 -242
  31. package/tests/resources/resources.test.ts +0 -309
  32. package/tests/search/embedder.test.ts +0 -81
  33. package/tests/search/highlight.test.ts +0 -129
  34. package/tests/search/store.test.ts +0 -193
  35. package/tests/search/sync.test.ts +0 -249
  36. package/tests/search/tools.test.ts +0 -661
  37. package/tests/tools/config.test.ts +0 -212
  38. package/tests/tools/files.test.ts +0 -344
  39. package/tests/tools/issues.test.ts +0 -1180
  40. package/tests/tools/metadata.test.ts +0 -509
  41. package/tests/tools/monitors.test.ts +0 -101
  42. package/tests/tools/projects.test.ts +0 -338
  43. package/tests/tools/relationships.test.ts +0 -177
  44. package/tests/tools/string-coercion.test.ts +0 -317
  45. package/tests/tools/users.test.ts +0 -62
  46. package/tests/utils/date-filter.test.ts +0 -169
  47. package/tests/version-hint.test.ts +0 -230
  48. package/tsconfig.build.json +0 -8
  49. 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
- });