@dpesch/mantisbt-mcp-server 1.18.0 → 1.18.1
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 +9 -0
- package/dist/build-info.json +3 -3
- package/dist/resources/index.js +4 -3
- package/dist/tools/metadata.js +21 -6
- package/dist/tools/projects.js +4 -12
- package/dist/versions.js +82 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,15 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## [1.18.1] – 2026-11-10
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `get_project` and `get_project_versions`: `obsolete: true` returned no obsolete versions, and `inherit: true` no versions of parent projects (#8614). Without the IDs of obsolete versions, `update_version` could not reach them. The cause lies in MantisBT: the REST handler of `GET /projects/{id}/versions` drops these query parameters, so the endpoint always returns the project's own, active versions (known upstream as [mantisbt #34578](https://mantisbt.org/bugs/view.php?id=34578), unfixed as of MantisBT 2.28.4; fix proposed in [mantisbt PR #2313](https://github.com/mantisbt/mantisbt/pull/2313)). With `obsolete` or `inherit` set, the server now also reads `GET /projects`, which embeds every version of a project and gives the project hierarchy; without `inherit`, versions of parent projects are left out — unless the API user cannot see the parent project, in which case they cannot be told apart and stay in. The metadata cache (`get_metadata`) and the `mantis://projects/{id}` resource held the same gap and now contain obsolete and inherited versions too; refresh an existing cache with `get_metadata(refresh: true)`. The cache sync now takes the versions from `GET /projects` and no longer sends one `/versions` request per project. Without either parameter, `get_project` and `get_project_versions` behave as before and make no extra request.
|
|
19
|
+
- `list_projects`, `get_project` and the `mantis://projects` resources never returned `subprojects`, although `get_project` lists the field (#8616). MantisBT sends it in camelCase as `subProjects`, which the server did not read. `subprojects` now holds the direct subprojects as `{ id, name }` references, the form MantisBT provides; projects without subprojects keep omitting the field. Refresh an existing metadata cache with `get_metadata(refresh: true)`.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
14
23
|
## [1.18.0] – 2026-11-09
|
|
15
24
|
|
|
16
25
|
### Deprecated
|
package/dist/build-info.json
CHANGED
package/dist/resources/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { fetchIssueEnums } from '../tools/config.js';
|
|
3
3
|
import { normalizeProject, stripAllProjectsPrefix } from '../tools/metadata.js';
|
|
4
|
+
import { fetchProjectVersions } from '../versions.js';
|
|
4
5
|
function jsonResource(uri, data) {
|
|
5
6
|
return {
|
|
6
7
|
contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(data) }],
|
|
@@ -56,15 +57,15 @@ export function registerResources(server, client, cache, testEnvironment = false
|
|
|
56
57
|
categories = meta?.categories ?? [];
|
|
57
58
|
}
|
|
58
59
|
else {
|
|
59
|
-
const [projectResult, usersResult,
|
|
60
|
+
const [projectResult, usersResult, fetchedVersions] = await Promise.all([
|
|
60
61
|
client.get(`projects/${numId}`),
|
|
61
62
|
client.get(`projects/${numId}/users`),
|
|
62
|
-
client
|
|
63
|
+
fetchProjectVersions(client, numId, { obsolete: true, inherit: true }),
|
|
63
64
|
]);
|
|
64
65
|
const raw = projectResult.projects?.[0];
|
|
65
66
|
project = raw ? normalizeProject(raw) : undefined;
|
|
66
67
|
users = usersResult.users ?? [];
|
|
67
|
-
versions =
|
|
68
|
+
versions = fetchedVersions;
|
|
68
69
|
categories = stripAllProjectsPrefix(raw?.categories ?? []);
|
|
69
70
|
}
|
|
70
71
|
if (!project) {
|
package/dist/tools/metadata.js
CHANGED
|
@@ -4,6 +4,7 @@ import { getVersionHint } from '../version-hint.js';
|
|
|
4
4
|
import { ALL_PROJECTS_PREFIX } from '../constants.js';
|
|
5
5
|
import { deprecationNote } from '../deprecation.js';
|
|
6
6
|
import { fetchIssueEnumsWithCache } from './config.js';
|
|
7
|
+
import { collectVersions } from '../versions.js';
|
|
7
8
|
// Fields MantisBT strips from issue responses when the array is empty.
|
|
8
9
|
// They are always valid 'select' values even if absent in a sample issue.
|
|
9
10
|
const EMPTY_STRIPPED_FIELDS = [
|
|
@@ -108,28 +109,42 @@ export function normalizeProject(p) {
|
|
|
108
109
|
...(p.enabled !== undefined && { enabled: p.enabled }),
|
|
109
110
|
...(p.view_state !== undefined && { view_state: normalizeIdName(p.view_state) }),
|
|
110
111
|
...(p.access_level !== undefined && { access_level: { id: p.access_level.id, label: p.access_level.label } }),
|
|
111
|
-
|
|
112
|
+
// MantisBT sends the direct subprojects as subProjects: [{ id, name }] (#8616)
|
|
113
|
+
...(p.subProjects !== undefined && {
|
|
114
|
+
subprojects: p.subProjects.map((s) => ({ id: s.id, name: s.name })),
|
|
115
|
+
}),
|
|
112
116
|
};
|
|
113
117
|
}
|
|
114
118
|
async function fetchAndCacheMetadata(client, cache) {
|
|
115
119
|
// Fetch all projects
|
|
116
120
|
const projectResult = await client.get('projects');
|
|
117
|
-
const
|
|
121
|
+
const rawProjects = projectResult.projects ?? [];
|
|
122
|
+
const projects = rawProjects.map(normalizeProject);
|
|
118
123
|
const byProject = {};
|
|
124
|
+
// GET /projects embeds every version of a project, obsolete ones included;
|
|
125
|
+
// /versions ignores obsolete and inherit (#8614) and is only asked when a
|
|
126
|
+
// project arrives without embedded versions.
|
|
127
|
+
const embedsVersions = new Set(rawProjects.filter(p => p.versions !== undefined).map(p => p.id));
|
|
119
128
|
// For each project, fetch users, versions, categories in parallel
|
|
120
129
|
await Promise.all(projects.map(async (project) => {
|
|
121
130
|
const [usersResult, versionsResult, projectDetailResult] = await Promise.allSettled([
|
|
122
131
|
client.get(`projects/${project.id}/users`),
|
|
123
|
-
|
|
132
|
+
embedsVersions.has(project.id)
|
|
133
|
+
? { versions: [] }
|
|
134
|
+
: client.get(`projects/${project.id}/versions`, { obsolete: 1, inherit: 1 }),
|
|
124
135
|
// Categories are embedded in the project detail response — same source as get_project_categories
|
|
125
136
|
client.get(`projects/${project.id}`),
|
|
126
137
|
]);
|
|
127
138
|
const users = usersResult.status === 'fulfilled'
|
|
128
139
|
? (usersResult.value.users ?? [])
|
|
129
140
|
: [];
|
|
130
|
-
const versions =
|
|
131
|
-
? (versionsResult.value.versions ?? [])
|
|
132
|
-
:
|
|
141
|
+
const versions = collectVersions({
|
|
142
|
+
own: versionsResult.status === 'fulfilled' ? (versionsResult.value.versions ?? []) : [],
|
|
143
|
+
projects: rawProjects,
|
|
144
|
+
projectId: project.id,
|
|
145
|
+
obsolete: true,
|
|
146
|
+
inherit: true,
|
|
147
|
+
});
|
|
133
148
|
const rawCategories = projectDetailResult.status === 'fulfilled'
|
|
134
149
|
? (projectDetailResult.value.projects?.[0]?.categories ?? [])
|
|
135
150
|
: [];
|
package/dist/tools/projects.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { coerceBool } from '../coerce.js';
|
|
3
|
+
import { fetchProjectVersions } from '../versions.js';
|
|
3
4
|
import { getVersionHint } from '../version-hint.js';
|
|
4
5
|
import { normalizeProject, stripAllProjectsPrefix } from './metadata.js';
|
|
5
6
|
import { deprecationNote } from '../deprecation.js';
|
|
@@ -18,15 +19,6 @@ const matchMembers = (users, query) => {
|
|
|
18
19
|
};
|
|
19
20
|
const PROJECT_INCLUDES = ['categories', 'users', 'versions'];
|
|
20
21
|
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
22
|
/**
|
|
31
23
|
* Project users. The metadata cache has no access-level filter, so it is
|
|
32
24
|
* used only when access_level is not given; otherwise the API filters.
|
|
@@ -49,7 +41,7 @@ export function registerProjectTools(server, client, cache) {
|
|
|
49
41
|
// ---------------------------------------------------------------------------
|
|
50
42
|
server.registerTool('get_project', {
|
|
51
43
|
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.
|
|
44
|
+
description: `Get one MantisBT project: its master data (id, name, description, status, enabled, view_state, access_level, subprojects as id and name of the direct subprojects) and, on request, its categories, users and versions in the same call.
|
|
53
45
|
|
|
54
46
|
- include: any of "categories", "users", "versions" (default: none — master data only).
|
|
55
47
|
- categories: names come without the "[All Projects] " prefix, ready for create_issue / update_issue.
|
|
@@ -82,7 +74,7 @@ Users come from the local metadata cache when it is fresh and no access_level is
|
|
|
82
74
|
const [projectResult, users, versions] = await Promise.all([
|
|
83
75
|
client.get(`projects/${project_id}`),
|
|
84
76
|
wants.has('users') ? fetchUsers(project_id, access_level) : undefined,
|
|
85
|
-
wants.has('versions') ?
|
|
77
|
+
wants.has('versions') ? fetchProjectVersions(client, project_id, { obsolete, inherit }) : undefined,
|
|
86
78
|
]);
|
|
87
79
|
const raw = projectResult.projects?.[0];
|
|
88
80
|
if (!raw)
|
|
@@ -187,7 +179,7 @@ Prerequisites: obtain project_id from list_projects.`,
|
|
|
187
179
|
},
|
|
188
180
|
}, async ({ project_id, obsolete, inherit }) => {
|
|
189
181
|
try {
|
|
190
|
-
const versions = await
|
|
182
|
+
const versions = await fetchProjectVersions(client, project_id, { obsolete, inherit });
|
|
191
183
|
return {
|
|
192
184
|
content: [{ type: 'text', text: JSON.stringify(versions, null, 2) }],
|
|
193
185
|
};
|
package/dist/versions.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** All projects that have the given project as a direct or indirect subproject. */
|
|
2
|
+
export function ancestorIds(projects, projectId) {
|
|
3
|
+
const parentsOf = new Map();
|
|
4
|
+
for (const project of projects) {
|
|
5
|
+
for (const sub of project.subProjects ?? []) {
|
|
6
|
+
const parents = parentsOf.get(sub.id);
|
|
7
|
+
if (parents)
|
|
8
|
+
parents.push(project.id);
|
|
9
|
+
else
|
|
10
|
+
parentsOf.set(sub.id, [project.id]);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const found = new Set();
|
|
14
|
+
const queue = [...(parentsOf.get(projectId) ?? [])];
|
|
15
|
+
while (queue.length > 0) {
|
|
16
|
+
const id = queue.shift();
|
|
17
|
+
if (id === projectId || found.has(id))
|
|
18
|
+
continue;
|
|
19
|
+
found.add(id);
|
|
20
|
+
queue.push(...(parentsOf.get(id) ?? []));
|
|
21
|
+
}
|
|
22
|
+
return [...found];
|
|
23
|
+
}
|
|
24
|
+
/** /versions omits a blank description; GET /projects always sends one. */
|
|
25
|
+
function normalizeVersion(version) {
|
|
26
|
+
if (version.description === undefined || version.description.trim() !== '')
|
|
27
|
+
return version;
|
|
28
|
+
const { description: _blank, ...rest } = version;
|
|
29
|
+
return rest;
|
|
30
|
+
}
|
|
31
|
+
const timeOf = (version) => {
|
|
32
|
+
const time = version.timestamp ? Date.parse(version.timestamp) : NaN;
|
|
33
|
+
return Number.isNaN(time) ? -Infinity : time;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Combines the /versions response with the versions embedded in GET /projects.
|
|
37
|
+
* Should a MantisBT release honour the parameters, both sources overlap; the
|
|
38
|
+
* union by id keeps the result free of duplicates either way.
|
|
39
|
+
*/
|
|
40
|
+
export function collectVersions(input) {
|
|
41
|
+
const { own, projects, projectId, obsolete = false, inherit = false } = input;
|
|
42
|
+
const versionsById = new Map(projects.map(p => [p.id, p.versions ?? []]));
|
|
43
|
+
const versionsOf = (id) => versionsById.get(id) ?? [];
|
|
44
|
+
const parentVersions = ancestorIds(projects, projectId).flatMap(versionsOf);
|
|
45
|
+
const parentIds = new Set(parentVersions.map(version => version.id));
|
|
46
|
+
const byId = new Map();
|
|
47
|
+
for (const version of [own, versionsOf(projectId), inherit ? parentVersions : []].flat()) {
|
|
48
|
+
if (byId.has(version.id))
|
|
49
|
+
continue;
|
|
50
|
+
if (!inherit && parentIds.has(version.id))
|
|
51
|
+
continue;
|
|
52
|
+
if (!obsolete && version.obsolete)
|
|
53
|
+
continue;
|
|
54
|
+
byId.set(version.id, normalizeVersion(version));
|
|
55
|
+
}
|
|
56
|
+
return [...byId.values()].sort((a, b) => timeOf(b) - timeOf(a));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Versions of one project. Without obsolete/inherit the /versions endpoint is
|
|
60
|
+
* accurate on its own; otherwise GET /projects fills in what it drops.
|
|
61
|
+
*/
|
|
62
|
+
export async function fetchProjectVersions(client, projectId, { obsolete = false, inherit = false } = {}) {
|
|
63
|
+
const params = {};
|
|
64
|
+
if (obsolete)
|
|
65
|
+
params.obsolete = 1;
|
|
66
|
+
if (inherit)
|
|
67
|
+
params.inherit = 1;
|
|
68
|
+
const ownRequest = client.get(`projects/${projectId}/versions`, params);
|
|
69
|
+
if (!obsolete && !inherit)
|
|
70
|
+
return (await ownRequest).versions ?? [];
|
|
71
|
+
const [ownResult, projectsResult] = await Promise.all([
|
|
72
|
+
ownRequest,
|
|
73
|
+
client.get('projects'),
|
|
74
|
+
]);
|
|
75
|
+
return collectVersions({
|
|
76
|
+
own: ownResult.versions ?? [],
|
|
77
|
+
projects: projectsResult.projects ?? [],
|
|
78
|
+
projectId,
|
|
79
|
+
obsolete,
|
|
80
|
+
inherit,
|
|
81
|
+
});
|
|
82
|
+
}
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"name": "io.github.dpesch/mantisbt-mcp-server",
|
|
4
4
|
"title": "MantisBT MCP Server",
|
|
5
5
|
"description": "MantisBT MCP server – manage issues, notes, files, tags, and relationships. With semantic search.",
|
|
6
|
-
"version": "1.18.
|
|
6
|
+
"version": "1.18.1",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"registryType": "npm",
|
|
10
10
|
"identifier": "@dpesch/mantisbt-mcp-server",
|
|
11
|
-
"version": "1.18.
|
|
11
|
+
"version": "1.18.1",
|
|
12
12
|
"runtimeHint": "npx",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|