@dpesch/mantisbt-mcp-server 1.0.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/.env.local +2 -0
- package/CHANGELOG.md +68 -0
- package/LICENSE +21 -0
- package/README.de.md +177 -0
- package/README.md +177 -0
- package/dist/cache.js +52 -0
- package/dist/client.js +114 -0
- package/dist/config.js +54 -0
- package/dist/constants.js +23 -0
- package/dist/index.js +120 -0
- package/dist/tools/config.js +107 -0
- package/dist/tools/files.js +37 -0
- package/dist/tools/filters.js +35 -0
- package/dist/tools/issues.js +191 -0
- package/dist/tools/metadata.js +119 -0
- package/dist/tools/monitors.js +38 -0
- package/dist/tools/notes.js +96 -0
- package/dist/tools/projects.js +127 -0
- package/dist/tools/relationships.js +54 -0
- package/dist/tools/tags.js +78 -0
- package/dist/tools/users.js +34 -0
- package/dist/tools/version.js +58 -0
- package/dist/types.js +4 -0
- package/dist/version-hint.js +117 -0
- package/package.json +41 -0
- package/scripts/record-fixtures.ts +138 -0
- package/tests/cache.test.ts +149 -0
- package/tests/client.test.ts +241 -0
- package/tests/config.test.ts +164 -0
- package/tests/fixtures/get_current_user.json +39 -0
- package/tests/fixtures/get_issue.json +151 -0
- package/tests/fixtures/get_project_categories.json +60 -0
- package/tests/fixtures/get_project_versions.json +3 -0
- package/tests/fixtures/get_project_versions_with_data.json +28 -0
- package/tests/fixtures/list_issues.json +67 -0
- package/tests/fixtures/list_projects.json +65 -0
- package/tests/fixtures/recorded/get_current_user.json +108 -0
- package/tests/fixtures/recorded/get_issue.json +320 -0
- package/tests/fixtures/recorded/get_project_categories.json +241 -0
- package/tests/fixtures/recorded/get_project_versions.json +3 -0
- package/tests/fixtures/recorded/list_issues.json +824 -0
- package/tests/fixtures/recorded/list_projects.json +10641 -0
- package/tests/helpers/mock-server.ts +32 -0
- package/tests/tools/issues.test.ts +130 -0
- package/tests/tools/projects.test.ts +169 -0
- package/tests/tools/users.test.ts +76 -0
- package/tests/version-hint.test.ts +230 -0
- package/tsconfig.build.json +8 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getVersionHint } from '../version-hint.js';
|
|
3
|
+
function errorText(msg) {
|
|
4
|
+
const vh = getVersionHint();
|
|
5
|
+
vh?.triggerLatestVersionFetch();
|
|
6
|
+
const hint = vh?.getUpdateHint();
|
|
7
|
+
return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
|
|
8
|
+
}
|
|
9
|
+
export function registerNoteTools(server, client) {
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// list_notes
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
server.registerTool('list_notes', {
|
|
14
|
+
title: 'List Issue Notes',
|
|
15
|
+
description: 'List all notes (comments) attached to a MantisBT issue.',
|
|
16
|
+
inputSchema: z.object({
|
|
17
|
+
issue_id: z.number().int().positive().describe('Numeric issue ID'),
|
|
18
|
+
}),
|
|
19
|
+
annotations: {
|
|
20
|
+
readOnlyHint: true,
|
|
21
|
+
destructiveHint: false,
|
|
22
|
+
idempotentHint: true,
|
|
23
|
+
},
|
|
24
|
+
}, async ({ issue_id }) => {
|
|
25
|
+
try {
|
|
26
|
+
const result = await client.get(`issues/${issue_id}`);
|
|
27
|
+
const notes = result.issues?.[0]?.notes ?? [];
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: 'text', text: JSON.stringify(notes, null, 2) }],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
34
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// add_note
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
server.registerTool('add_note', {
|
|
41
|
+
title: 'Add Note to Issue',
|
|
42
|
+
description: 'Add a note (comment) to an existing MantisBT issue. Full UTF-8 text is supported.',
|
|
43
|
+
inputSchema: z.object({
|
|
44
|
+
issue_id: z.number().int().positive().describe('Numeric issue ID'),
|
|
45
|
+
text: z.string().min(1).describe('Note text (supports full UTF-8, markdown will be stored as-is)'),
|
|
46
|
+
view_state: z.enum(['public', 'private']).default('public').describe('Visibility of the note (default: public)'),
|
|
47
|
+
}),
|
|
48
|
+
annotations: {
|
|
49
|
+
readOnlyHint: false,
|
|
50
|
+
destructiveHint: false,
|
|
51
|
+
idempotentHint: false,
|
|
52
|
+
},
|
|
53
|
+
}, async ({ issue_id, text, view_state }) => {
|
|
54
|
+
try {
|
|
55
|
+
const body = {
|
|
56
|
+
text,
|
|
57
|
+
view_state: { name: view_state },
|
|
58
|
+
};
|
|
59
|
+
const result = await client.post(`issues/${issue_id}/notes`, body);
|
|
60
|
+
return {
|
|
61
|
+
content: [{ type: 'text', text: JSON.stringify(result.note ?? result, null, 2) }],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
66
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// delete_note
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
server.registerTool('delete_note', {
|
|
73
|
+
title: 'Delete Note',
|
|
74
|
+
description: 'Permanently delete a note from a MantisBT issue. This action is irreversible.',
|
|
75
|
+
inputSchema: z.object({
|
|
76
|
+
issue_id: z.number().int().positive().describe('Numeric issue ID that owns the note'),
|
|
77
|
+
note_id: z.number().int().positive().describe('Numeric note ID to delete'),
|
|
78
|
+
}),
|
|
79
|
+
annotations: {
|
|
80
|
+
readOnlyHint: false,
|
|
81
|
+
destructiveHint: true,
|
|
82
|
+
idempotentHint: true,
|
|
83
|
+
},
|
|
84
|
+
}, async ({ issue_id, note_id }) => {
|
|
85
|
+
try {
|
|
86
|
+
await client.delete(`issues/${issue_id}/notes/${note_id}`);
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: 'text', text: `Note #${note_id} deleted from issue #${issue_id}.` }],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
93
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getVersionHint } from '../version-hint.js';
|
|
3
|
+
function errorText(msg) {
|
|
4
|
+
const vh = getVersionHint();
|
|
5
|
+
vh?.triggerLatestVersionFetch();
|
|
6
|
+
const hint = vh?.getUpdateHint();
|
|
7
|
+
return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
|
|
8
|
+
}
|
|
9
|
+
const ALL_PROJECTS_PREFIX = '[All Projects] ';
|
|
10
|
+
export function registerProjectTools(server, client) {
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// list_projects
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
server.registerTool('list_projects', {
|
|
15
|
+
title: 'List Projects',
|
|
16
|
+
description: 'List all MantisBT projects accessible to the current API user.',
|
|
17
|
+
inputSchema: z.object({}),
|
|
18
|
+
annotations: {
|
|
19
|
+
readOnlyHint: true,
|
|
20
|
+
destructiveHint: false,
|
|
21
|
+
idempotentHint: true,
|
|
22
|
+
},
|
|
23
|
+
}, async () => {
|
|
24
|
+
try {
|
|
25
|
+
const result = await client.get('projects');
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: 'text', text: JSON.stringify(result.projects ?? result, null, 2) }],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
32
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// get_project_users
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
server.registerTool('get_project_users', {
|
|
39
|
+
title: 'Get Project Users',
|
|
40
|
+
description: 'List all users with access to a specific MantisBT project.',
|
|
41
|
+
inputSchema: z.object({
|
|
42
|
+
project_id: z.number().int().positive().describe('Numeric project ID'),
|
|
43
|
+
access_level: z.number().int().optional().describe('Minimum access level filter (e.g. 55 = developer, 90 = manager)'),
|
|
44
|
+
}),
|
|
45
|
+
annotations: {
|
|
46
|
+
readOnlyHint: true,
|
|
47
|
+
destructiveHint: false,
|
|
48
|
+
idempotentHint: true,
|
|
49
|
+
},
|
|
50
|
+
}, async ({ project_id, access_level }) => {
|
|
51
|
+
try {
|
|
52
|
+
const params = {};
|
|
53
|
+
if (access_level !== undefined)
|
|
54
|
+
params.access_level = access_level;
|
|
55
|
+
const result = await client.get(`projects/${project_id}/users`, params);
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: 'text', text: JSON.stringify(result.users ?? result, null, 2) }],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
62
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// get_project_versions
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
server.registerTool('get_project_versions', {
|
|
69
|
+
title: 'Get Project Versions',
|
|
70
|
+
description: 'List all versions defined for a MantisBT project.',
|
|
71
|
+
inputSchema: z.object({
|
|
72
|
+
project_id: z.number().int().positive().describe('Numeric project ID'),
|
|
73
|
+
}),
|
|
74
|
+
annotations: {
|
|
75
|
+
readOnlyHint: true,
|
|
76
|
+
destructiveHint: false,
|
|
77
|
+
idempotentHint: true,
|
|
78
|
+
},
|
|
79
|
+
}, async ({ project_id }) => {
|
|
80
|
+
try {
|
|
81
|
+
const result = await client.get(`projects/${project_id}/versions`);
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: 'text', text: JSON.stringify(result.versions ?? result, null, 2) }],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
88
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// get_project_categories
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
server.registerTool('get_project_categories', {
|
|
95
|
+
title: 'Get Project Categories',
|
|
96
|
+
description: `List all categories available for a MantisBT project.
|
|
97
|
+
|
|
98
|
+
Note: The MantisBT API returns global (cross-project) categories with a "[All Projects] " prefix.
|
|
99
|
+
This tool strips that prefix so the returned names can be used directly when creating issues.`,
|
|
100
|
+
inputSchema: z.object({
|
|
101
|
+
project_id: z.number().int().positive().describe('Numeric project ID'),
|
|
102
|
+
}),
|
|
103
|
+
annotations: {
|
|
104
|
+
readOnlyHint: true,
|
|
105
|
+
destructiveHint: false,
|
|
106
|
+
idempotentHint: true,
|
|
107
|
+
},
|
|
108
|
+
}, async ({ project_id }) => {
|
|
109
|
+
try {
|
|
110
|
+
const result = await client.get(`projects/${project_id}`);
|
|
111
|
+
const raw = result.projects?.[0]?.categories ?? [];
|
|
112
|
+
const categories = raw.map((cat) => ({
|
|
113
|
+
...cat,
|
|
114
|
+
name: cat.name.startsWith(ALL_PROJECTS_PREFIX)
|
|
115
|
+
? cat.name.slice(ALL_PROJECTS_PREFIX.length)
|
|
116
|
+
: cat.name,
|
|
117
|
+
}));
|
|
118
|
+
return {
|
|
119
|
+
content: [{ type: 'text', text: JSON.stringify(categories, null, 2) }],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
124
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { RELATIONSHIP_TYPES } from '../constants.js';
|
|
3
|
+
import { getVersionHint } from '../version-hint.js';
|
|
4
|
+
function errorText(msg) {
|
|
5
|
+
const vh = getVersionHint();
|
|
6
|
+
vh?.triggerLatestVersionFetch();
|
|
7
|
+
const hint = vh?.getUpdateHint();
|
|
8
|
+
return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
|
|
9
|
+
}
|
|
10
|
+
export function registerRelationshipTools(server, client) {
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// add_relationship
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
server.registerTool('add_relationship', {
|
|
15
|
+
title: 'Add Issue Relationship',
|
|
16
|
+
description: `Add a relationship between two MantisBT issues.
|
|
17
|
+
|
|
18
|
+
Relationship type IDs:
|
|
19
|
+
- ${RELATIONSHIP_TYPES.DUPLICATE_OF} = duplicate_of — this issue is a duplicate of target
|
|
20
|
+
- ${RELATIONSHIP_TYPES.RELATED_TO} = related_to — this issue is related to target
|
|
21
|
+
- ${RELATIONSHIP_TYPES.PARENT_OF} = parent_of — this issue depends on target (target must be done first)
|
|
22
|
+
- ${RELATIONSHIP_TYPES.CHILD_OF} = child_of — this issue blocks target (target can't proceed until this is done)
|
|
23
|
+
- ${RELATIONSHIP_TYPES.HAS_DUPLICATE} = has_duplicate — this issue has target as a duplicate
|
|
24
|
+
|
|
25
|
+
Directionality note: "A child_of B" means A blocks B. "A parent_of B" means A depends on B.
|
|
26
|
+
|
|
27
|
+
Important: The API only accepts numeric type IDs, not string names.`,
|
|
28
|
+
inputSchema: z.object({
|
|
29
|
+
issue_id: z.number().int().positive().describe('The source issue ID (the one the relationship is added to)'),
|
|
30
|
+
target_id: z.number().int().positive().describe('The target issue ID'),
|
|
31
|
+
type_id: z.number().int().min(0).max(4).describe(`Relationship type ID: 0=duplicate_of, 1=related_to, 2=parent_of (depends on), 3=child_of (blocks), 4=has_duplicate`),
|
|
32
|
+
}),
|
|
33
|
+
annotations: {
|
|
34
|
+
readOnlyHint: false,
|
|
35
|
+
destructiveHint: false,
|
|
36
|
+
idempotentHint: false,
|
|
37
|
+
},
|
|
38
|
+
}, async ({ issue_id, target_id, type_id }) => {
|
|
39
|
+
try {
|
|
40
|
+
const body = {
|
|
41
|
+
issue: { id: target_id },
|
|
42
|
+
type: { id: type_id },
|
|
43
|
+
};
|
|
44
|
+
const result = await client.post(`issues/${issue_id}/relationships`, body);
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
51
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getVersionHint } from '../version-hint.js';
|
|
3
|
+
function errorText(msg) {
|
|
4
|
+
const vh = getVersionHint();
|
|
5
|
+
vh?.triggerLatestVersionFetch();
|
|
6
|
+
const hint = vh?.getUpdateHint();
|
|
7
|
+
return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
|
|
8
|
+
}
|
|
9
|
+
export function registerTagTools(server, client) {
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// attach_tags
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
server.registerTool('attach_tags', {
|
|
14
|
+
title: 'Attach Tags to Issue',
|
|
15
|
+
description: `Attach one or more tags to a MantisBT issue.
|
|
16
|
+
|
|
17
|
+
Each tag can be specified either by ID or by name. If a tag name is provided
|
|
18
|
+
that does not exist yet, MantisBT will create it automatically (requires
|
|
19
|
+
tag_create_threshold permission, default: REPORTER).
|
|
20
|
+
|
|
21
|
+
Requires tag_attach_threshold permission (default: REPORTER).`,
|
|
22
|
+
inputSchema: z.object({
|
|
23
|
+
issue_id: z.number().int().positive().describe('Numeric issue ID'),
|
|
24
|
+
tags: z.array(z.object({
|
|
25
|
+
id: z.number().int().positive().optional().describe('Tag ID'),
|
|
26
|
+
name: z.string().min(1).optional().describe('Tag name'),
|
|
27
|
+
}).refine(t => t.id !== undefined || t.name !== undefined, {
|
|
28
|
+
message: 'Each tag must have at least an id or a name',
|
|
29
|
+
})).min(1).describe('Tags to attach — each entry needs at least id or name'),
|
|
30
|
+
}),
|
|
31
|
+
annotations: {
|
|
32
|
+
readOnlyHint: false,
|
|
33
|
+
destructiveHint: false,
|
|
34
|
+
idempotentHint: false,
|
|
35
|
+
},
|
|
36
|
+
}, async ({ issue_id, tags }) => {
|
|
37
|
+
try {
|
|
38
|
+
await client.post(`issues/${issue_id}/tags`, { tags });
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: 'text', text: `Tags successfully attached to issue #${issue_id}.` }],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
45
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// detach_tag
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
server.registerTool('detach_tag', {
|
|
52
|
+
title: 'Detach Tag from Issue',
|
|
53
|
+
description: `Remove a tag from a MantisBT issue.
|
|
54
|
+
|
|
55
|
+
Requires tag_detach_own_threshold (default: REPORTER) for own tags,
|
|
56
|
+
or tag_detach_threshold (default: DEVELOPER) for tags attached by others.`,
|
|
57
|
+
inputSchema: z.object({
|
|
58
|
+
issue_id: z.number().int().positive().describe('Numeric issue ID'),
|
|
59
|
+
tag_id: z.number().int().positive().describe('Numeric tag ID to remove'),
|
|
60
|
+
}),
|
|
61
|
+
annotations: {
|
|
62
|
+
readOnlyHint: false,
|
|
63
|
+
destructiveHint: false,
|
|
64
|
+
idempotentHint: true,
|
|
65
|
+
},
|
|
66
|
+
}, async ({ issue_id, tag_id }) => {
|
|
67
|
+
try {
|
|
68
|
+
await client.delete(`issues/${issue_id}/tags/${tag_id}`);
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: 'text', text: `Tag #${tag_id} successfully removed from issue #${issue_id}.` }],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
75
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getVersionHint } from '../version-hint.js';
|
|
3
|
+
function errorText(msg) {
|
|
4
|
+
const vh = getVersionHint();
|
|
5
|
+
vh?.triggerLatestVersionFetch();
|
|
6
|
+
const hint = vh?.getUpdateHint();
|
|
7
|
+
return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
|
|
8
|
+
}
|
|
9
|
+
export function registerUserTools(server, client) {
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// get_current_user
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
server.registerTool('get_current_user', {
|
|
14
|
+
title: 'Get Current User',
|
|
15
|
+
description: 'Retrieve the profile of the user associated with the current API key.',
|
|
16
|
+
inputSchema: z.object({}),
|
|
17
|
+
annotations: {
|
|
18
|
+
readOnlyHint: true,
|
|
19
|
+
destructiveHint: false,
|
|
20
|
+
idempotentHint: true,
|
|
21
|
+
},
|
|
22
|
+
}, async () => {
|
|
23
|
+
try {
|
|
24
|
+
const result = await client.get('users/me');
|
|
25
|
+
return {
|
|
26
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
31
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { parseVersion, compareVersions } from '../version-hint.js';
|
|
3
|
+
export function registerVersionTools(server, client, versionHint) {
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// get_mantis_version
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
server.registerTool('get_mantis_version', {
|
|
8
|
+
title: 'Get MantisBT Version',
|
|
9
|
+
description: `Returns the version of the connected MantisBT installation and optionally compares it against the latest official release on GitHub.
|
|
10
|
+
|
|
11
|
+
The version is read from the X-Mantis-Version response header sent by every API call.
|
|
12
|
+
The GitHub comparison requires an outbound HTTPS request to the GitHub API.`,
|
|
13
|
+
inputSchema: z.object({
|
|
14
|
+
check_latest: z.boolean().default(true).describe('Whether to fetch the latest release from GitHub and compare (default: true)'),
|
|
15
|
+
}),
|
|
16
|
+
annotations: {
|
|
17
|
+
readOnlyHint: true,
|
|
18
|
+
destructiveHint: false,
|
|
19
|
+
idempotentHint: true,
|
|
20
|
+
},
|
|
21
|
+
}, async ({ check_latest }) => {
|
|
22
|
+
try {
|
|
23
|
+
const installedRaw = await client.getVersion();
|
|
24
|
+
const result = { installed_version: installedRaw };
|
|
25
|
+
if (check_latest) {
|
|
26
|
+
versionHint.triggerLatestVersionFetch();
|
|
27
|
+
const latestRaw = await versionHint.waitForLatestVersion(5000);
|
|
28
|
+
result.latest_version = latestRaw;
|
|
29
|
+
if (latestRaw === null) {
|
|
30
|
+
result.status = 'unknown';
|
|
31
|
+
result.github_note = 'Could not fetch latest version from GitHub (timeout or network error).';
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const installed = parseVersion(installedRaw);
|
|
35
|
+
const latest = parseVersion(latestRaw);
|
|
36
|
+
if (installed && latest) {
|
|
37
|
+
const cmp = compareVersions(installed, latest);
|
|
38
|
+
result.status = cmp === 0
|
|
39
|
+
? 'up-to-date'
|
|
40
|
+
: cmp > 0
|
|
41
|
+
? 'newer-than-release'
|
|
42
|
+
: 'update-available';
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
result.status = 'unknown';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
55
|
+
return { content: [{ type: 'text', text: `Error: ${msg}` }], isError: true };
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Version parsing utilities
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
export function parseVersion(raw) {
|
|
5
|
+
const match = raw.replace(/^v/, '').match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
6
|
+
if (!match)
|
|
7
|
+
return null;
|
|
8
|
+
return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)];
|
|
9
|
+
}
|
|
10
|
+
export function compareVersions(a, b) {
|
|
11
|
+
for (let i = 0; i < 3; i++) {
|
|
12
|
+
if (a[i] < b[i])
|
|
13
|
+
return -1;
|
|
14
|
+
if (a[i] > b[i])
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// VersionHintService
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
const MANTISBT_TAGS_URL = 'https://api.github.com/repos/mantisbt/mantisbt/tags?per_page=10';
|
|
23
|
+
/**
|
|
24
|
+
* Reads the installed MantisBT version from API response headers and lazily
|
|
25
|
+
* fetches the latest release from GitHub. Provides a non-blocking update hint
|
|
26
|
+
* that can be appended to API error messages.
|
|
27
|
+
*
|
|
28
|
+
* Intentionally has no imports from the rest of this project to avoid
|
|
29
|
+
* circular dependencies.
|
|
30
|
+
*/
|
|
31
|
+
export class VersionHintService {
|
|
32
|
+
installedVersion = null;
|
|
33
|
+
latestVersion = null;
|
|
34
|
+
fetchStarted = false;
|
|
35
|
+
/** Called by MantisClient after every successful API response. */
|
|
36
|
+
onSuccessfulResponse(response) {
|
|
37
|
+
if (!this.installedVersion) {
|
|
38
|
+
const v = response.headers.get('X-Mantis-Version');
|
|
39
|
+
if (v)
|
|
40
|
+
this.installedVersion = v;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Starts the GitHub fetch exactly once (fire-and-forget).
|
|
45
|
+
* Safe to call multiple times — subsequent calls are no-ops.
|
|
46
|
+
*/
|
|
47
|
+
triggerLatestVersionFetch() {
|
|
48
|
+
if (this.fetchStarted)
|
|
49
|
+
return;
|
|
50
|
+
this.fetchStarted = true;
|
|
51
|
+
void this.doFetch();
|
|
52
|
+
}
|
|
53
|
+
async doFetch() {
|
|
54
|
+
try {
|
|
55
|
+
const resp = await fetch(MANTISBT_TAGS_URL, {
|
|
56
|
+
headers: {
|
|
57
|
+
'Accept': 'application/vnd.github+json',
|
|
58
|
+
'User-Agent': 'mantisbt-mcp-server',
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
if (!resp.ok)
|
|
62
|
+
return;
|
|
63
|
+
const tags = await resp.json();
|
|
64
|
+
const tag = tags.find(t => /^release-\d+\.\d+\.\d+$/.test(t.name));
|
|
65
|
+
this.latestVersion = tag ? tag.name.replace('release-', '') : null;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Network error — no hint, no crash
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Returns an update hint string if a newer version is known, or null.
|
|
73
|
+
* Never blocks — returns null while the GitHub fetch is still in flight.
|
|
74
|
+
*/
|
|
75
|
+
getUpdateHint() {
|
|
76
|
+
if (!this.installedVersion || !this.latestVersion)
|
|
77
|
+
return null;
|
|
78
|
+
const installed = parseVersion(this.installedVersion);
|
|
79
|
+
const latest = parseVersion(this.latestVersion);
|
|
80
|
+
if (!installed || !latest)
|
|
81
|
+
return null;
|
|
82
|
+
if (compareVersions(installed, latest) < 0) {
|
|
83
|
+
return (`Note: MantisBT ${this.latestVersion} is available ` +
|
|
84
|
+
`(installed: ${this.installedVersion}) — updating may resolve this issue.`);
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
getInstalledVersion() { return this.installedVersion; }
|
|
89
|
+
getLatestVersion() { return this.latestVersion; }
|
|
90
|
+
/**
|
|
91
|
+
* Waits up to `timeoutMs` for the GitHub fetch to complete.
|
|
92
|
+
* Used by the get_mantis_version tool where blocking is acceptable.
|
|
93
|
+
*/
|
|
94
|
+
async waitForLatestVersion(timeoutMs = 5000) {
|
|
95
|
+
if (this.latestVersion !== null)
|
|
96
|
+
return this.latestVersion;
|
|
97
|
+
if (!this.fetchStarted)
|
|
98
|
+
return null;
|
|
99
|
+
const deadline = Date.now() + timeoutMs;
|
|
100
|
+
while (Date.now() < deadline) {
|
|
101
|
+
if (this.latestVersion !== null)
|
|
102
|
+
return this.latestVersion;
|
|
103
|
+
await new Promise(r => setTimeout(r, 100));
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Module-level singleton (one per server process)
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
let globalInstance = null;
|
|
112
|
+
export function setGlobalVersionHint(svc) {
|
|
113
|
+
globalInstance = svc;
|
|
114
|
+
}
|
|
115
|
+
export function getVersionHint() {
|
|
116
|
+
return globalInstance;
|
|
117
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dpesch/mantisbt-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for MantisBT REST API – read and manage bug tracker issues",
|
|
5
|
+
"author": "Dominik Pesch",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://codeberg.org/dpesch/mantisbt-mcp-server"
|
|
10
|
+
},
|
|
11
|
+
"keywords": ["mcp", "mantisbt", "bugtracker", "mantis", "model-context-protocol"],
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"bin": {
|
|
14
|
+
"mantisbt-mcp-server": "dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.build.json",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"start": "node dist/index.js",
|
|
21
|
+
"dev": "tsc -p tsconfig.build.json --watch",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"test:coverage": "vitest run --coverage",
|
|
25
|
+
"test:record": "tsx scripts/record-fixtures.ts"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
29
|
+
"zod": "^3.22.4"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^20.0.0",
|
|
33
|
+
"@vitest/coverage-v8": "^2.0.0",
|
|
34
|
+
"tsx": "^4.0.0",
|
|
35
|
+
"typescript": "^5.3.0",
|
|
36
|
+
"vitest": "^2.0.0"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
}
|
|
41
|
+
}
|