@dpesch/mantisbt-mcp-server 1.14.1 → 1.16.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 +64 -0
- package/README.de.md +123 -33
- package/README.md +123 -33
- package/dist/client.js +4 -0
- package/dist/coerce.js +8 -0
- package/dist/config.js +5 -2
- package/dist/deprecation.js +9 -0
- package/dist/index.js +20 -31
- package/dist/issue-enrich.js +23 -0
- package/dist/issue-envelope.js +4 -0
- package/dist/prompts/index.js +4 -4
- package/dist/resources/index.js +4 -8
- package/dist/search/embedder.js +83 -34
- package/dist/search/index.js +9 -7
- package/dist/search/lexical.js +88 -0
- package/dist/search/store.js +44 -3
- package/dist/search/sync.js +26 -15
- package/dist/search/tools.js +23 -22
- package/dist/tools/config.js +9 -4
- package/dist/tools/files.js +12 -5
- package/dist/tools/index.js +34 -0
- package/dist/tools/issues.js +285 -147
- package/dist/tools/metadata.js +99 -40
- package/dist/tools/monitors.js +145 -20
- package/dist/tools/notes.js +67 -9
- package/dist/tools/project-versions.js +40 -18
- package/dist/tools/projects.js +109 -37
- package/dist/tools/users.js +3 -1
- package/dist/tools/version.js +61 -35
- package/dist/toolsets.js +104 -0
- package/docs/cookbook.de.md +604 -183
- package/docs/cookbook.md +604 -183
- package/docs/examples.de.md +37 -3
- package/docs/examples.md +37 -3
- package/package.json +3 -1
- package/server.json +2 -2
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { coerceBool } from '../coerce.js';
|
|
2
3
|
import { getVersionHint } from '../version-hint.js';
|
|
4
|
+
import { deprecationNote } from '../deprecation.js';
|
|
3
5
|
function errorText(msg) {
|
|
4
6
|
const vh = getVersionHint();
|
|
5
7
|
vh?.triggerLatestVersionFetch();
|
|
6
8
|
const hint = vh?.getUpdateHint();
|
|
7
9
|
return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
|
|
8
10
|
}
|
|
9
|
-
const coerceBool = (val) => val === 'true' ? true : val === 'false' ? false : val;
|
|
10
11
|
/** Drops undefined values so PATCH only touches the fields the caller set. */
|
|
11
12
|
function definedFields(fields) {
|
|
12
13
|
return Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined));
|
|
13
14
|
}
|
|
14
15
|
const PERMISSION_NOTE = 'Requires the manage_project_threshold access level in the project (MantisBT default: manager).';
|
|
15
|
-
const OWNING_PROJECT_NOTE = 'The version must belong to project_id itself: versions inherited from a parent project can only be changed via the parent\'s project_id (otherwise MantisBT answers "Version not found").
|
|
16
|
+
const OWNING_PROJECT_NOTE = 'The version must belong to project_id itself: versions inherited from a parent project can only be changed via the parent\'s project_id (otherwise MantisBT answers "Version not found"). get_project with include: ["versions"] and without inherit lists the project\'s own versions.';
|
|
16
17
|
const projectIdSchema = z.coerce.number().int().positive().describe('Numeric ID of the project the version belongs to — use list_projects to discover project IDs');
|
|
17
|
-
const versionIdSchema = z.coerce.number().int().positive().describe('Numeric version ID — use
|
|
18
|
+
const versionIdSchema = z.coerce.number().int().positive().describe('Numeric version ID — use get_project with include: ["versions"] to discover version IDs');
|
|
18
19
|
const timestampSchema = z.string().min(1).describe('Version date as ISO 8601 string, e.g. "2026-09-27" or "2026-09-27T10:00:00+02:00". Determines the order in roadmap and changelog.');
|
|
19
20
|
const boolFlag = z.preprocess(coerceBool, z.boolean());
|
|
20
21
|
export function registerProjectVersionTools(server, client, cache) {
|
|
@@ -39,6 +40,18 @@ export function registerProjectVersionTools(server, client, cache) {
|
|
|
39
40
|
await patchCachedVersions((_pid, versions) => versions.map((v) => (v.id === versionId ? result.version : v)));
|
|
40
41
|
return result.version;
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Creates the follow-up version after a release. A failure must not hide the
|
|
45
|
+
* release that already succeeded, so it is reported as next_version_error.
|
|
46
|
+
*/
|
|
47
|
+
async function createFollowUpVersion(projectId, name) {
|
|
48
|
+
try {
|
|
49
|
+
return { next_version: await createVersion(projectId, { name }) };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return { next_version_error: error instanceof Error ? error.message : String(error) };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
42
55
|
// ---------------------------------------------------------------------------
|
|
43
56
|
// create_version
|
|
44
57
|
// ---------------------------------------------------------------------------
|
|
@@ -83,7 +96,9 @@ ${PERMISSION_NOTE}`,
|
|
|
83
96
|
|
|
84
97
|
Renaming a version also rewrites the version, target_version and fixed_in_version fields of all issues that reference it (including subprojects if versions are inherited).
|
|
85
98
|
|
|
86
|
-
Setting released=true does not change the version date — pass timestamp as well
|
|
99
|
+
Setting released=true does not change the version date — pass timestamp as well (e.g. the release date).
|
|
100
|
+
|
|
101
|
+
To release a version: released=true, timestamp=<release date>, and optionally next_version=<name> to create the follow-up version in the same call. With next_version the response is { version: <updated>, next_version: <created> } — or next_version_error: <message> if creating the follow-up failed (the update itself stays in effect). Without next_version the response is the updated version object.
|
|
87
102
|
|
|
88
103
|
${OWNING_PROJECT_NOTE}
|
|
89
104
|
|
|
@@ -96,36 +111,48 @@ ${PERMISSION_NOTE}`,
|
|
|
96
111
|
released: boolFlag.optional().describe('Released flag'),
|
|
97
112
|
obsolete: boolFlag.optional().describe('Obsolete flag'),
|
|
98
113
|
timestamp: timestampSchema.optional(),
|
|
114
|
+
next_version: z.string().trim().min(1).optional().describe('Only with released=true: name of a follow-up version to create as unreleased placeholder, e.g. "1.2.1". Used as-is — no automatic numbering.'),
|
|
99
115
|
}),
|
|
100
116
|
annotations: {
|
|
101
117
|
readOnlyHint: false,
|
|
102
118
|
destructiveHint: false,
|
|
103
119
|
idempotentHint: true,
|
|
104
120
|
},
|
|
105
|
-
}, async ({ project_id, version_id, ...fields }) => {
|
|
121
|
+
}, async ({ project_id, version_id, next_version, ...fields }) => {
|
|
106
122
|
if (Object.keys(definedFields(fields)).length === 0) {
|
|
107
123
|
return {
|
|
108
124
|
content: [{ type: 'text', text: errorText('No fields to update — pass at least one of name, description, released, obsolete, timestamp.') }],
|
|
109
125
|
isError: true,
|
|
110
126
|
};
|
|
111
127
|
}
|
|
112
|
-
|
|
113
|
-
const updated = await updateVersion(project_id, version_id, fields);
|
|
128
|
+
if (next_version !== undefined && fields.released !== true) {
|
|
114
129
|
return {
|
|
115
|
-
content: [{ type: 'text', text:
|
|
130
|
+
content: [{ type: 'text', text: errorText('next_version is only allowed together with released=true.') }],
|
|
131
|
+
isError: true,
|
|
116
132
|
};
|
|
117
133
|
}
|
|
134
|
+
let updated;
|
|
135
|
+
try {
|
|
136
|
+
updated = await updateVersion(project_id, version_id, fields);
|
|
137
|
+
}
|
|
118
138
|
catch (error) {
|
|
119
139
|
const msg = error instanceof Error ? error.message : String(error);
|
|
120
140
|
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
121
141
|
}
|
|
142
|
+
if (next_version === undefined) {
|
|
143
|
+
return { content: [{ type: 'text', text: JSON.stringify(updated, null, 2) }] };
|
|
144
|
+
}
|
|
145
|
+
const result = { version: updated, ...(await createFollowUpVersion(project_id, next_version)) };
|
|
146
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
122
147
|
});
|
|
123
148
|
// ---------------------------------------------------------------------------
|
|
124
149
|
// release_version
|
|
125
150
|
// ---------------------------------------------------------------------------
|
|
126
151
|
server.registerTool('release_version', {
|
|
127
152
|
title: 'Release Version',
|
|
128
|
-
description:
|
|
153
|
+
description: `${deprecationNote('release_version', 'update_version with released=true, timestamp and (optionally) next_version')}
|
|
154
|
+
|
|
155
|
+
Mark a version as released and set its date (default: now). Optionally create a follow-up version in the same step.
|
|
129
156
|
|
|
130
157
|
The follow-up version is only created when next_version is given; its name is used as-is — no automatic numbering.
|
|
131
158
|
|
|
@@ -157,15 +184,10 @@ ${PERMISSION_NOTE}`,
|
|
|
157
184
|
const msg = error instanceof Error ? error.message : String(error);
|
|
158
185
|
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
159
186
|
}
|
|
160
|
-
const result = {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
result.next_version_error = error instanceof Error ? error.message : String(error);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
187
|
+
const result = {
|
|
188
|
+
released,
|
|
189
|
+
...(next_version !== undefined ? await createFollowUpVersion(project_id, next_version) : {}),
|
|
190
|
+
};
|
|
169
191
|
return {
|
|
170
192
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
171
193
|
};
|
package/dist/tools/projects.js
CHANGED
|
@@ -1,15 +1,105 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { coerceBool } from '../coerce.js';
|
|
2
3
|
import { getVersionHint } from '../version-hint.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
4
|
+
import { normalizeProject, stripAllProjectsPrefix } from './metadata.js';
|
|
5
|
+
import { deprecationNote } from '../deprecation.js';
|
|
5
6
|
function errorText(msg) {
|
|
6
7
|
const vh = getVersionHint();
|
|
7
8
|
vh?.triggerLatestVersionFetch();
|
|
8
9
|
const hint = vh?.getUpdateHint();
|
|
9
10
|
return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
|
|
10
11
|
}
|
|
11
|
-
|
|
12
|
+
/** Case-insensitive substring match on login name, real name and email. */
|
|
13
|
+
const matchMembers = (users, query) => {
|
|
14
|
+
const q = query.toLowerCase();
|
|
15
|
+
return users.filter(u => u.name.toLowerCase().includes(q) ||
|
|
16
|
+
(u.real_name?.toLowerCase().includes(q) ?? false) ||
|
|
17
|
+
(u.email?.toLowerCase().includes(q) ?? false));
|
|
18
|
+
};
|
|
19
|
+
const PROJECT_INCLUDES = ['categories', 'users', 'versions'];
|
|
12
20
|
export function registerProjectTools(server, client, cache) {
|
|
21
|
+
async function fetchVersions(projectId, obsolete, inherit) {
|
|
22
|
+
const params = {};
|
|
23
|
+
if (obsolete)
|
|
24
|
+
params.obsolete = 1;
|
|
25
|
+
if (inherit)
|
|
26
|
+
params.inherit = 1;
|
|
27
|
+
const result = await client.get(`projects/${projectId}/versions`, params);
|
|
28
|
+
return result.versions ?? [];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Project users. The metadata cache has no access-level filter, so it is
|
|
32
|
+
* used only when access_level is not given; otherwise the API filters.
|
|
33
|
+
*/
|
|
34
|
+
async function fetchUsers(projectId, accessLevel) {
|
|
35
|
+
if (accessLevel === undefined) {
|
|
36
|
+
const cached = cache ? await cache.loadIfValid() : null;
|
|
37
|
+
const cachedUsers = cached?.byProject[projectId]?.users;
|
|
38
|
+
if (cachedUsers)
|
|
39
|
+
return cachedUsers;
|
|
40
|
+
}
|
|
41
|
+
const params = {};
|
|
42
|
+
if (accessLevel !== undefined)
|
|
43
|
+
params.access_level = accessLevel;
|
|
44
|
+
const result = await client.get(`projects/${projectId}/users`, params);
|
|
45
|
+
return result.users ?? [];
|
|
46
|
+
}
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// get_project
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
server.registerTool('get_project', {
|
|
51
|
+
title: 'Get Project',
|
|
52
|
+
description: `Get one MantisBT project: its master data (id, name, description, status, enabled, view_state, access_level, subprojects) and, on request, its categories, users and versions in the same call.
|
|
53
|
+
|
|
54
|
+
- include: any of "categories", "users", "versions" (default: none — master data only).
|
|
55
|
+
- categories: names come without the "[All Projects] " prefix, ready for create_issue / update_issue.
|
|
56
|
+
- users: id, name (login name), real_name, email, access_level. access_level returns only users at or above that level (10=viewer, 25=reporter, 40=updater, 55=developer, 70=manager, 90=administrator) and implies include "users".
|
|
57
|
+
- member_query: case-insensitive substring search in login name, real name and email; returns all matching users. Implies include "users".
|
|
58
|
+
- versions: id, name, released, obsolete, timestamp. obsolete=true adds obsolete versions, inherit=true adds versions of parent projects; either implies include "versions".
|
|
59
|
+
|
|
60
|
+
Users come from the local metadata cache when it is fresh and no access_level is given. Prerequisites: obtain project_id from list_projects.`,
|
|
61
|
+
inputSchema: z.object({
|
|
62
|
+
project_id: z.coerce.number().int().positive().describe('Numeric project ID — use list_projects to discover project IDs'),
|
|
63
|
+
include: z.array(z.enum(PROJECT_INCLUDES)).optional().describe('Related data to add: "categories", "users", "versions"'),
|
|
64
|
+
access_level: z.coerce.number().int().optional().describe('Only users at or above this access level; implies include "users"'),
|
|
65
|
+
member_query: z.string().min(1).optional().describe('Case-insensitive substring to find users by login name, real name or email; implies include "users"'),
|
|
66
|
+
obsolete: z.preprocess(coerceBool, z.boolean().optional()).describe('Also return obsolete versions; implies include "versions"'),
|
|
67
|
+
inherit: z.preprocess(coerceBool, z.boolean().optional()).describe('Also return versions inherited from parent projects; implies include "versions"'),
|
|
68
|
+
}),
|
|
69
|
+
annotations: {
|
|
70
|
+
readOnlyHint: true,
|
|
71
|
+
destructiveHint: false,
|
|
72
|
+
idempotentHint: true,
|
|
73
|
+
},
|
|
74
|
+
}, async ({ project_id, include, access_level, member_query, obsolete, inherit }) => {
|
|
75
|
+
const wants = new Set(include);
|
|
76
|
+
if (member_query !== undefined || access_level !== undefined)
|
|
77
|
+
wants.add('users');
|
|
78
|
+
if (obsolete !== undefined || inherit !== undefined)
|
|
79
|
+
wants.add('versions');
|
|
80
|
+
try {
|
|
81
|
+
// Categories come with the project itself; users and versions need their own requests.
|
|
82
|
+
const [projectResult, users, versions] = await Promise.all([
|
|
83
|
+
client.get(`projects/${project_id}`),
|
|
84
|
+
wants.has('users') ? fetchUsers(project_id, access_level) : undefined,
|
|
85
|
+
wants.has('versions') ? fetchVersions(project_id, obsolete, inherit) : undefined,
|
|
86
|
+
]);
|
|
87
|
+
const raw = projectResult.projects?.[0];
|
|
88
|
+
if (!raw)
|
|
89
|
+
throw new Error(`Project ${project_id} not found or not accessible.`);
|
|
90
|
+
const project = {
|
|
91
|
+
...normalizeProject(raw),
|
|
92
|
+
...(wants.has('categories') && { categories: stripAllProjectsPrefix(raw.categories ?? []) }),
|
|
93
|
+
...(users && { users: member_query !== undefined ? matchMembers(users, member_query) : users }),
|
|
94
|
+
...(versions && { versions }),
|
|
95
|
+
};
|
|
96
|
+
return { content: [{ type: 'text', text: JSON.stringify(project, null, 2) }] };
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
100
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
101
|
+
}
|
|
102
|
+
});
|
|
13
103
|
// ---------------------------------------------------------------------------
|
|
14
104
|
// list_projects
|
|
15
105
|
// ---------------------------------------------------------------------------
|
|
@@ -40,9 +130,9 @@ export function registerProjectTools(server, client, cache) {
|
|
|
40
130
|
// ---------------------------------------------------------------------------
|
|
41
131
|
server.registerTool('get_project_users', {
|
|
42
132
|
title: 'Get Project Users',
|
|
43
|
-
description:
|
|
133
|
+
description: `${deprecationNote('get_project_users', 'get_project with include: ["users"]')}
|
|
44
134
|
|
|
45
|
-
|
|
135
|
+
List all users with access to a specific MantisBT project. Returns an array of user objects, each containing id, name (login name), real_name, email, and access_level fields.
|
|
46
136
|
|
|
47
137
|
Access level IDs: 10=viewer, 25=reporter, 40=updater, 55=developer, 70=manager, 90=administrator.
|
|
48
138
|
|
|
@@ -76,7 +166,9 @@ Prerequisites: obtain project_id from list_projects.`,
|
|
|
76
166
|
// ---------------------------------------------------------------------------
|
|
77
167
|
server.registerTool('get_project_versions', {
|
|
78
168
|
title: 'Get Project Versions',
|
|
79
|
-
description:
|
|
169
|
+
description: `${deprecationNote('get_project_versions', 'get_project with include: ["versions"]')}
|
|
170
|
+
|
|
171
|
+
List all versions defined for a MantisBT project. Returns an array of version objects, each containing id, name, released (boolean), obsolete (boolean), and timestamp (version date). To create, change, release or delete versions use create_version, update_version, release_version and delete_version.
|
|
80
172
|
|
|
81
173
|
Use the returned version names directly when creating or updating issues via create_issue and update_issue (version, target_version, fixed_in_version fields).
|
|
82
174
|
|
|
@@ -95,14 +187,9 @@ Prerequisites: obtain project_id from list_projects.`,
|
|
|
95
187
|
},
|
|
96
188
|
}, async ({ project_id, obsolete, inherit }) => {
|
|
97
189
|
try {
|
|
98
|
-
const
|
|
99
|
-
if (obsolete)
|
|
100
|
-
params.obsolete = 1;
|
|
101
|
-
if (inherit)
|
|
102
|
-
params.inherit = 1;
|
|
103
|
-
const result = await client.get(`projects/${project_id}/versions`, params);
|
|
190
|
+
const versions = await fetchVersions(project_id, obsolete, inherit);
|
|
104
191
|
return {
|
|
105
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
192
|
+
content: [{ type: 'text', text: JSON.stringify(versions, null, 2) }],
|
|
106
193
|
};
|
|
107
194
|
}
|
|
108
195
|
catch (error) {
|
|
@@ -115,7 +202,9 @@ Prerequisites: obtain project_id from list_projects.`,
|
|
|
115
202
|
// ---------------------------------------------------------------------------
|
|
116
203
|
server.registerTool('get_project_categories', {
|
|
117
204
|
title: 'Get Project Categories',
|
|
118
|
-
description:
|
|
205
|
+
description: `${deprecationNote('get_project_categories', 'get_project with include: ["categories"]')}
|
|
206
|
+
|
|
207
|
+
List all categories available for a MantisBT project.
|
|
119
208
|
|
|
120
209
|
Note: The MantisBT API returns global (cross-project) categories with a "[All Projects] " prefix.
|
|
121
210
|
This tool strips that prefix so the returned names can be used directly when creating issues.`,
|
|
@@ -130,13 +219,7 @@ This tool strips that prefix so the returned names can be used directly when cre
|
|
|
130
219
|
}, async ({ project_id }) => {
|
|
131
220
|
try {
|
|
132
221
|
const result = await client.get(`projects/${project_id}`);
|
|
133
|
-
const
|
|
134
|
-
const categories = raw.map((cat) => ({
|
|
135
|
-
...cat,
|
|
136
|
-
name: cat.name.startsWith(ALL_PROJECTS_PREFIX)
|
|
137
|
-
? cat.name.slice(ALL_PROJECTS_PREFIX.length)
|
|
138
|
-
: cat.name,
|
|
139
|
-
}));
|
|
222
|
+
const categories = stripAllProjectsPrefix(result.projects?.[0]?.categories ?? []);
|
|
140
223
|
return {
|
|
141
224
|
content: [{ type: 'text', text: JSON.stringify(categories, null, 2) }],
|
|
142
225
|
};
|
|
@@ -151,7 +234,9 @@ This tool strips that prefix so the returned names can be used directly when cre
|
|
|
151
234
|
// ---------------------------------------------------------------------------
|
|
152
235
|
server.registerTool('find_project_member', {
|
|
153
236
|
title: 'Find Project Member',
|
|
154
|
-
description:
|
|
237
|
+
description: `${deprecationNote('find_project_member', 'get_project with member_query')}
|
|
238
|
+
|
|
239
|
+
Search for users with access to a MantisBT project by name, display name, or email.
|
|
155
240
|
|
|
156
241
|
Returns up to \`limit\` matching users (default: 10, max: 100). Matching is case-insensitive substring search across \`name\`, \`real_name\`, and \`email\` fields. Omit \`query\` to list the first \`limit\` users.
|
|
157
242
|
|
|
@@ -168,21 +253,8 @@ Data is served from the local metadata cache when fresh; falls back to a live AP
|
|
|
168
253
|
},
|
|
169
254
|
}, async ({ project_id, query, limit }) => {
|
|
170
255
|
try {
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
if (cached?.byProject[project_id]) {
|
|
174
|
-
users = cached.byProject[project_id].users;
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
const result = await client.get(`projects/${project_id}/users`);
|
|
178
|
-
users = result.users ?? [];
|
|
179
|
-
}
|
|
180
|
-
if (query) {
|
|
181
|
-
const q = query.toLowerCase();
|
|
182
|
-
users = users.filter((u) => u.name.toLowerCase().includes(q) ||
|
|
183
|
-
(u.real_name?.toLowerCase().includes(q) ?? false) ||
|
|
184
|
-
(u.email?.toLowerCase().includes(q) ?? false));
|
|
185
|
-
}
|
|
256
|
+
const all = await fetchUsers(project_id);
|
|
257
|
+
const users = query ? matchMembers(all, query) : all;
|
|
186
258
|
return {
|
|
187
259
|
content: [{ type: 'text', text: JSON.stringify(users.slice(0, limit), null, 2) }],
|
|
188
260
|
};
|
package/dist/tools/users.js
CHANGED
|
@@ -12,7 +12,9 @@ export function registerUserTools(server, client) {
|
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
server.registerTool('get_current_user', {
|
|
14
14
|
title: 'Get Current User',
|
|
15
|
-
description:
|
|
15
|
+
description: `Retrieve the profile of the user associated with the current API key: id, name (login name), real_name, email, language, timezone, access_level, created_at and the accessible projects.
|
|
16
|
+
|
|
17
|
+
"language" is the MantisBT language of this user (e.g. "german", "english") — use it as the hint for which language to write issue texts and notes in, and expect localized enum labels in that language.`,
|
|
16
18
|
inputSchema: z.object({}),
|
|
17
19
|
annotations: {
|
|
18
20
|
readOnlyHint: true,
|
package/dist/tools/version.js
CHANGED
|
@@ -1,13 +1,68 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { coerceBool } from '../coerce.js';
|
|
2
3
|
import { parseVersion, compareVersions } from '../version-hint.js';
|
|
3
|
-
|
|
4
|
+
import { deprecationNote } from '../deprecation.js';
|
|
4
5
|
export function registerVersionTools(server, client, versionHint, mcpVersion) {
|
|
6
|
+
/**
|
|
7
|
+
* Installed MantisBT version (X-Mantis-Version header), optionally compared
|
|
8
|
+
* with the latest MantisBT release on GitHub.
|
|
9
|
+
*/
|
|
10
|
+
async function mantisVersionInfo(checkLatest) {
|
|
11
|
+
// Start the GitHub lookup first so it overlaps with the MantisBT request.
|
|
12
|
+
if (checkLatest)
|
|
13
|
+
versionHint.triggerLatestVersionFetch();
|
|
14
|
+
const installedRaw = await client.getVersion();
|
|
15
|
+
const result = { installed_version: installedRaw };
|
|
16
|
+
if (!checkLatest)
|
|
17
|
+
return result;
|
|
18
|
+
const latestRaw = await versionHint.waitForLatestVersion(5000);
|
|
19
|
+
result.latest_version = latestRaw;
|
|
20
|
+
if (latestRaw === null) {
|
|
21
|
+
result.status = 'unknown';
|
|
22
|
+
result.github_note = 'Could not fetch latest version from GitHub (timeout or network error).';
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
const installed = parseVersion(installedRaw);
|
|
26
|
+
const latest = parseVersion(latestRaw);
|
|
27
|
+
if (installed && latest) {
|
|
28
|
+
const cmp = compareVersions(installed, latest);
|
|
29
|
+
result.status = cmp === 0 ? 'up-to-date' : cmp > 0 ? 'newer-than-release' : 'update-available';
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
result.status = 'unknown';
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
const checkLatestSchema = z.preprocess(coerceBool, z.boolean().default(true)).describe('Whether to fetch the latest MantisBT release from GitHub and compare (default: true)');
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// get_version
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
server.registerTool('get_version', {
|
|
41
|
+
title: 'Get Versions',
|
|
42
|
+
description: `Return the version of this mantisbt-mcp-server and of the connected MantisBT installation in one response:
|
|
43
|
+
{ mcp_server: { version }, mantis: { installed_version, latest_version?, status?, github_note? } }
|
|
44
|
+
|
|
45
|
+
The MantisBT version is read from the X-Mantis-Version response header. With check_latest (default: true) it is compared against the latest MantisBT release on GitHub (outbound HTTPS request, up to 5 seconds); status is "up-to-date", "update-available", "newer-than-release" or "unknown". If MantisBT cannot be reached, mantis contains "error" instead and mcp_server is still returned.`,
|
|
46
|
+
inputSchema: z.object({ check_latest: checkLatestSchema }),
|
|
47
|
+
annotations: {
|
|
48
|
+
readOnlyHint: true,
|
|
49
|
+
destructiveHint: false,
|
|
50
|
+
idempotentHint: true,
|
|
51
|
+
},
|
|
52
|
+
}, async ({ check_latest }) => {
|
|
53
|
+
const mantis = await mantisVersionInfo(check_latest).catch((error) => ({
|
|
54
|
+
error: error instanceof Error ? error.message : String(error),
|
|
55
|
+
}));
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: 'text', text: JSON.stringify({ mcp_server: { version: mcpVersion }, mantis }, null, 2) }],
|
|
58
|
+
};
|
|
59
|
+
});
|
|
5
60
|
// ---------------------------------------------------------------------------
|
|
6
61
|
// get_mcp_version
|
|
7
62
|
// ---------------------------------------------------------------------------
|
|
8
63
|
server.registerTool('get_mcp_version', {
|
|
9
64
|
title: 'Get MCP Server Version',
|
|
10
|
-
description: '
|
|
65
|
+
description: `${deprecationNote('get_mcp_version', 'get_version')}\n\nReturns the version of this mantisbt-mcp-server instance.`,
|
|
11
66
|
inputSchema: z.object({}),
|
|
12
67
|
annotations: {
|
|
13
68
|
readOnlyHint: true,
|
|
@@ -22,13 +77,10 @@ export function registerVersionTools(server, client, versionHint, mcpVersion) {
|
|
|
22
77
|
// ---------------------------------------------------------------------------
|
|
23
78
|
server.registerTool('get_mantis_version', {
|
|
24
79
|
title: 'Get MantisBT Version',
|
|
25
|
-
description:
|
|
80
|
+
description: `${deprecationNote('get_mantis_version', 'get_version')}
|
|
26
81
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
inputSchema: z.object({
|
|
30
|
-
check_latest: z.preprocess(coerceBool, z.boolean().default(true)).describe('Whether to fetch the latest release from GitHub and compare (default: true)'),
|
|
31
|
-
}),
|
|
82
|
+
Returns the version of the connected MantisBT installation and optionally compares it against the latest official release on GitHub.`,
|
|
83
|
+
inputSchema: z.object({ check_latest: checkLatestSchema }),
|
|
32
84
|
annotations: {
|
|
33
85
|
readOnlyHint: true,
|
|
34
86
|
destructiveHint: false,
|
|
@@ -36,34 +88,8 @@ The GitHub comparison requires an outbound HTTPS request to the GitHub API.`,
|
|
|
36
88
|
},
|
|
37
89
|
}, async ({ check_latest }) => {
|
|
38
90
|
try {
|
|
39
|
-
const installedRaw = await client.getVersion();
|
|
40
|
-
const result = { installed_version: installedRaw };
|
|
41
|
-
if (check_latest) {
|
|
42
|
-
versionHint.triggerLatestVersionFetch();
|
|
43
|
-
const latestRaw = await versionHint.waitForLatestVersion(5000);
|
|
44
|
-
result.latest_version = latestRaw;
|
|
45
|
-
if (latestRaw === null) {
|
|
46
|
-
result.status = 'unknown';
|
|
47
|
-
result.github_note = 'Could not fetch latest version from GitHub (timeout or network error).';
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
const installed = parseVersion(installedRaw);
|
|
51
|
-
const latest = parseVersion(latestRaw);
|
|
52
|
-
if (installed && latest) {
|
|
53
|
-
const cmp = compareVersions(installed, latest);
|
|
54
|
-
result.status = cmp === 0
|
|
55
|
-
? 'up-to-date'
|
|
56
|
-
: cmp > 0
|
|
57
|
-
? 'newer-than-release'
|
|
58
|
-
: 'update-available';
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
result.status = 'unknown';
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
91
|
return {
|
|
66
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
92
|
+
content: [{ type: 'text', text: JSON.stringify(await mantisVersionInfo(check_latest), null, 2) }],
|
|
67
93
|
};
|
|
68
94
|
}
|
|
69
95
|
catch (error) {
|
package/dist/toolsets.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Tool groups for MANTIS_TOOLSETS and MANTIS_DISABLE_DESTRUCTIVE (#8500)
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
export const TOOLSETS = ['issues', 'notes', 'links', 'files', 'projects', 'versions', 'meta', 'search'];
|
|
5
|
+
/**
|
|
6
|
+
* Group of every tool. A deprecated tool belongs to the group of its
|
|
7
|
+
* replacement, so switching a group off hides old and new name together.
|
|
8
|
+
* tests/toolsets.test.ts checks this list against the registered tools in
|
|
9
|
+
* both directions — keep it in sync when adding or removing a tool.
|
|
10
|
+
*/
|
|
11
|
+
export const TOOL_GROUPS = {
|
|
12
|
+
// issues
|
|
13
|
+
get_issue: 'issues',
|
|
14
|
+
get_issues: 'issues', // deprecated → get_issue
|
|
15
|
+
list_issues: 'issues',
|
|
16
|
+
create_issue: 'issues',
|
|
17
|
+
update_issue: 'issues',
|
|
18
|
+
delete_issue: 'issues',
|
|
19
|
+
list_notes: 'issues', // deprecated → get_issue
|
|
20
|
+
list_issue_files: 'issues', // deprecated → get_issue
|
|
21
|
+
add_monitor: 'issues', // deprecated → update_issue
|
|
22
|
+
remove_monitor: 'issues', // deprecated → update_issue
|
|
23
|
+
// notes
|
|
24
|
+
add_note: 'notes',
|
|
25
|
+
delete_note: 'notes',
|
|
26
|
+
// links
|
|
27
|
+
add_relationship: 'links',
|
|
28
|
+
remove_relationship: 'links',
|
|
29
|
+
attach_tags: 'links',
|
|
30
|
+
detach_tag: 'links',
|
|
31
|
+
list_tags: 'links',
|
|
32
|
+
// files
|
|
33
|
+
upload_file: 'files',
|
|
34
|
+
// projects
|
|
35
|
+
list_projects: 'projects',
|
|
36
|
+
get_project: 'projects',
|
|
37
|
+
list_filters: 'projects',
|
|
38
|
+
get_project_categories: 'projects', // deprecated → get_project
|
|
39
|
+
get_project_users: 'projects', // deprecated → get_project
|
|
40
|
+
get_project_versions: 'projects', // deprecated → get_project
|
|
41
|
+
find_project_member: 'projects', // deprecated → get_project
|
|
42
|
+
// versions
|
|
43
|
+
create_version: 'versions',
|
|
44
|
+
update_version: 'versions',
|
|
45
|
+
delete_version: 'versions',
|
|
46
|
+
release_version: 'versions', // deprecated → update_version
|
|
47
|
+
// meta
|
|
48
|
+
get_metadata: 'meta',
|
|
49
|
+
get_metadata_full: 'meta', // deprecated → get_metadata
|
|
50
|
+
sync_metadata: 'meta', // deprecated → get_metadata
|
|
51
|
+
get_search_index_status: 'meta', // deprecated → get_metadata
|
|
52
|
+
get_issue_schema: 'meta',
|
|
53
|
+
get_issue_fields: 'meta', // deprecated → get_issue_schema
|
|
54
|
+
get_issue_enums: 'meta', // deprecated → get_issue_schema
|
|
55
|
+
get_config: 'meta',
|
|
56
|
+
get_version: 'meta',
|
|
57
|
+
get_mcp_version: 'meta', // deprecated → get_version
|
|
58
|
+
get_mantis_version: 'meta', // deprecated → get_version
|
|
59
|
+
get_current_user: 'meta',
|
|
60
|
+
list_languages: 'meta', // deprecated → get_current_user
|
|
61
|
+
// search (additionally requires MANTIS_SEARCH_ENABLED=true)
|
|
62
|
+
search_issues: 'search',
|
|
63
|
+
rebuild_search_index: 'search',
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Parses MANTIS_TOOLSETS: comma-separated group names, case-insensitive.
|
|
67
|
+
* Unset or empty means all groups. An unknown name throws, listing the valid
|
|
68
|
+
* ones, so a typo does not silently hide tools.
|
|
69
|
+
*/
|
|
70
|
+
export function parseToolsets(raw) {
|
|
71
|
+
const names = (raw ?? '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
|
|
72
|
+
if (names.length === 0)
|
|
73
|
+
return new Set(TOOLSETS);
|
|
74
|
+
const unknown = names.find(name => !isToolset(name));
|
|
75
|
+
if (unknown !== undefined) {
|
|
76
|
+
throw new Error(`Unknown MANTIS_TOOLSETS group "${unknown}". Valid groups: ${TOOLSETS.join(', ')}.`);
|
|
77
|
+
}
|
|
78
|
+
return new Set(names.filter(isToolset));
|
|
79
|
+
}
|
|
80
|
+
const isToolset = (name) => TOOLSETS.includes(name);
|
|
81
|
+
/**
|
|
82
|
+
* Returns the server with registerTool filtered: tools of disabled groups and,
|
|
83
|
+
* with disableDestructive, destructive tools are skipped. Everything else
|
|
84
|
+
* (resources, prompts, connect, …) passes through unchanged. The register*Tools
|
|
85
|
+
* functions receive this instead of the server and need no changes.
|
|
86
|
+
*/
|
|
87
|
+
export function filterToolRegistration(server, filter) {
|
|
88
|
+
const registerTool = ((name, config, callback) => {
|
|
89
|
+
const group = TOOL_GROUPS[name];
|
|
90
|
+
if (group === undefined) {
|
|
91
|
+
throw new Error(`Tool "${name}" has no group — add it to TOOL_GROUPS in src/toolsets.ts.`);
|
|
92
|
+
}
|
|
93
|
+
// A skipped tool is never registered, so it is neither listed nor callable.
|
|
94
|
+
// No caller uses registerTool's return value.
|
|
95
|
+
if (!filter.toolsets.has(group))
|
|
96
|
+
return undefined;
|
|
97
|
+
if (filter.disableDestructive && config.annotations?.destructiveHint === true)
|
|
98
|
+
return undefined;
|
|
99
|
+
return server.registerTool(name, config, callback);
|
|
100
|
+
});
|
|
101
|
+
return new Proxy(server, {
|
|
102
|
+
get: (target, prop, receiver) => (prop === 'registerTool' ? registerTool : Reflect.get(target, prop, receiver)),
|
|
103
|
+
});
|
|
104
|
+
}
|