@dpesch/mantisbt-mcp-server 1.8.1 → 1.8.3
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 +17 -0
- package/dist/client.js +7 -0
- package/dist/config.js +1 -0
- package/dist/index.js +11 -4
- package/dist/resources/index.js +14 -7
- package/package.json +1 -1
- package/scripts/hooks/pre-push.mjs +27 -11
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,23 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## [1.8.3] – 2026-03-28
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- New optional env var `MCP_TEST_ENVIRONMENT=true`. When set, `resources/list` skips live API calls and returns only static resources. Intended for automated inspection environments (e.g. Glama Docker builds) that start the server with placeholder credentials and restricted network access.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- All `MantisClient` fetch calls now use a 30-second timeout via `AbortSignal.timeout()`. Previously, a slow or unreachable MantisBT instance would cause fetch calls to hang indefinitely.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## [1.8.2] – 2026-03-27
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- HTTP transport (`TRANSPORT=http`): race condition when multiple sequential requests arrive quickly (e.g. `initialize` followed immediately by `ping`). The `res.on('close')` handler that calls `transport.close()` fires asynchronously, so the next request could arrive while the previous transport was still registered — causing `connect()` to throw "Already connected". Fixed by calling `await server.close()` before each `server.connect()` call (a no-op when not connected). Also added a `res.headersSent` guard in the error handler to prevent a secondary "Cannot set headers after they are sent" error from closing the socket without a response.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
14
31
|
## [1.8.1] – 2026-03-27
|
|
15
32
|
|
|
16
33
|
### Fixed
|
package/dist/client.js
CHANGED
|
@@ -96,10 +96,12 @@ export class MantisClient {
|
|
|
96
96
|
// ---------------------------------------------------------------------------
|
|
97
97
|
// Public API methods
|
|
98
98
|
// ---------------------------------------------------------------------------
|
|
99
|
+
static TIMEOUT_MS = 30_000;
|
|
99
100
|
async get(path, params) {
|
|
100
101
|
const response = await fetch(await this.buildUrl(path, params), {
|
|
101
102
|
method: 'GET',
|
|
102
103
|
headers: await this.headers(),
|
|
104
|
+
signal: AbortSignal.timeout(MantisClient.TIMEOUT_MS),
|
|
103
105
|
});
|
|
104
106
|
return this.handleResponse(response);
|
|
105
107
|
}
|
|
@@ -108,6 +110,7 @@ export class MantisClient {
|
|
|
108
110
|
method: 'POST',
|
|
109
111
|
headers: await this.headers(),
|
|
110
112
|
body: JSON.stringify(body),
|
|
113
|
+
signal: AbortSignal.timeout(MantisClient.TIMEOUT_MS),
|
|
111
114
|
});
|
|
112
115
|
return this.handleResponse(response);
|
|
113
116
|
}
|
|
@@ -116,6 +119,7 @@ export class MantisClient {
|
|
|
116
119
|
method: 'PATCH',
|
|
117
120
|
headers: await this.headers(),
|
|
118
121
|
body: JSON.stringify(body),
|
|
122
|
+
signal: AbortSignal.timeout(MantisClient.TIMEOUT_MS),
|
|
119
123
|
});
|
|
120
124
|
return this.handleResponse(response);
|
|
121
125
|
}
|
|
@@ -123,6 +127,7 @@ export class MantisClient {
|
|
|
123
127
|
const response = await fetch(await this.buildUrl(path), {
|
|
124
128
|
method: 'DELETE',
|
|
125
129
|
headers: await this.headers(),
|
|
130
|
+
signal: AbortSignal.timeout(MantisClient.TIMEOUT_MS),
|
|
126
131
|
});
|
|
127
132
|
return this.handleResponse(response);
|
|
128
133
|
}
|
|
@@ -137,6 +142,7 @@ export class MantisClient {
|
|
|
137
142
|
'Accept': 'application/json',
|
|
138
143
|
},
|
|
139
144
|
body: formData,
|
|
145
|
+
signal: AbortSignal.timeout(MantisClient.TIMEOUT_MS),
|
|
140
146
|
});
|
|
141
147
|
return this.handleResponse(response);
|
|
142
148
|
}
|
|
@@ -144,6 +150,7 @@ export class MantisClient {
|
|
|
144
150
|
const response = await fetch(await this.buildUrl('users/me'), {
|
|
145
151
|
method: 'GET',
|
|
146
152
|
headers: await this.headers(),
|
|
153
|
+
signal: AbortSignal.timeout(MantisClient.TIMEOUT_MS),
|
|
147
154
|
});
|
|
148
155
|
if (!response.ok) {
|
|
149
156
|
throw new MantisApiError(response.status, response.statusText);
|
package/dist/config.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -63,7 +63,7 @@ async function createMcpServer() {
|
|
|
63
63
|
registerTagTools(server, client);
|
|
64
64
|
registerVersionTools(server, client, versionHint, version);
|
|
65
65
|
registerPrompts(server);
|
|
66
|
-
registerResources(server, client, cache);
|
|
66
|
+
registerResources(server, client, cache, startupConfig.testEnvironment);
|
|
67
67
|
// Optional: Semantic search module
|
|
68
68
|
if (startupConfig.search.enabled) {
|
|
69
69
|
const { initializeSearchModule } = await import('./search/index.js');
|
|
@@ -107,13 +107,20 @@ async function runHttp() {
|
|
|
107
107
|
sessionIdGenerator: undefined,
|
|
108
108
|
enableJsonResponse: true,
|
|
109
109
|
});
|
|
110
|
-
res.on('close', () => transport.close());
|
|
110
|
+
res.on('close', () => { void transport.close(); });
|
|
111
|
+
// Disconnect from any previous transport before connecting the new one.
|
|
112
|
+
// The res.on('close') handler above closes the transport asynchronously,
|
|
113
|
+
// but the next request may arrive before it fires — causing connect() to
|
|
114
|
+
// throw "Already connected". Explicitly closing first avoids that race.
|
|
115
|
+
await server.close();
|
|
111
116
|
await server.connect(transport);
|
|
112
117
|
await transport.handleRequest(req, res, body);
|
|
113
118
|
}
|
|
114
119
|
catch {
|
|
115
|
-
res.
|
|
116
|
-
|
|
120
|
+
if (!res.headersSent) {
|
|
121
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
122
|
+
res.end(JSON.stringify({ error: 'Bad Request' }));
|
|
123
|
+
}
|
|
117
124
|
}
|
|
118
125
|
});
|
|
119
126
|
}
|
package/dist/resources/index.js
CHANGED
|
@@ -7,7 +7,7 @@ function jsonResource(uri, data) {
|
|
|
7
7
|
contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(data) }],
|
|
8
8
|
};
|
|
9
9
|
}
|
|
10
|
-
export function registerResources(server, client, cache) {
|
|
10
|
+
export function registerResources(server, client, cache, testEnvironment = false) {
|
|
11
11
|
async function loadProjects() {
|
|
12
12
|
const cached = await cache.loadIfValid();
|
|
13
13
|
return cached?.projects
|
|
@@ -25,12 +25,19 @@ export function registerResources(server, client, cache) {
|
|
|
25
25
|
mimeType: 'application/json',
|
|
26
26
|
}, async (uri) => jsonResource(uri, await loadProjects()));
|
|
27
27
|
server.registerResource('project-detail', new ResourceTemplate('mantis://projects/{id}', {
|
|
28
|
-
list: async () =>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
list: async () => {
|
|
29
|
+
// In test environments (MCP_TEST_ENVIRONMENT=true, e.g. Glama inspection
|
|
30
|
+
// with placeholder credentials) skip the live API call and return an
|
|
31
|
+
// empty list so resources/list responds immediately without timing out.
|
|
32
|
+
if (testEnvironment)
|
|
33
|
+
return { resources: [] };
|
|
34
|
+
return {
|
|
35
|
+
resources: (await loadProjects()).map((p) => ({
|
|
36
|
+
uri: `mantis://projects/${p.id}`,
|
|
37
|
+
name: p.name,
|
|
38
|
+
})),
|
|
39
|
+
};
|
|
40
|
+
},
|
|
34
41
|
}), {
|
|
35
42
|
title: 'Project Detail',
|
|
36
43
|
description: 'Combined project view: fields (status, view_state, access_level, description) plus all associated users, versions, and categories. Served from local cache when fresh; falls back to live API fetch. Refresh via the sync_metadata tool.',
|
package/package.json
CHANGED
|
@@ -111,6 +111,16 @@ rl.on('close', () => {
|
|
|
111
111
|
const branches = lines.filter(l => l.split(' ')[2]?.startsWith('refs/heads/'));
|
|
112
112
|
const others = lines.filter(l => !l.split(' ')[2]?.startsWith('refs/heads/'));
|
|
113
113
|
|
|
114
|
+
// Batch-fetch all tags from Codeberg (single ls-remote call, ref → sha).
|
|
115
|
+
const remoteTagsRaw = gitOptional(`ls-remote --tags "${remoteUrl}"`);
|
|
116
|
+
const remoteTagMap = new Map(
|
|
117
|
+
remoteTagsRaw
|
|
118
|
+
? remoteTagsRaw.split('\n').filter(Boolean)
|
|
119
|
+
.map(l => { const [sha, ref] = l.split(/\s+/); return [ref, sha]; })
|
|
120
|
+
.filter(([ref]) => ref)
|
|
121
|
+
: []
|
|
122
|
+
);
|
|
123
|
+
|
|
114
124
|
// localSha → filteredSha, accumulated across all refs in this push.
|
|
115
125
|
const shaMap = {};
|
|
116
126
|
let pushed = 0;
|
|
@@ -124,6 +134,17 @@ rl.on('close', () => {
|
|
|
124
134
|
|
|
125
135
|
const label = remoteRef.replace('refs/heads/', '').replace('refs/tags/', '');
|
|
126
136
|
|
|
137
|
+
if (remoteRef.startsWith('refs/tags/')) {
|
|
138
|
+
if (remoteTagMap.has(remoteRef)) {
|
|
139
|
+
console.log(` ↷ ${label} already on Codeberg — skipping`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (!gitOptional(`rev-parse --verify "${localSha}"`)) {
|
|
143
|
+
console.warn(` ⚠ ${label} — commit ${localSha.slice(0, 8)} not found locally, skipping`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
127
148
|
// If this commit was already filtered as part of a branch push, reuse it.
|
|
128
149
|
if (localSha in shaMap) {
|
|
129
150
|
try {
|
|
@@ -137,9 +158,11 @@ rl.on('close', () => {
|
|
|
137
158
|
continue;
|
|
138
159
|
}
|
|
139
160
|
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
const actualRemoteSha =
|
|
161
|
+
// Tags: existing ones were skipped above, so only new tags reach here — no remote anchor.
|
|
162
|
+
// Branches: query individually to find the incremental anchor for filtered chain building.
|
|
163
|
+
const actualRemoteSha = remoteRef.startsWith('refs/tags/')
|
|
164
|
+
? ''
|
|
165
|
+
: (gitOptional(`ls-remote "${remoteUrl}" "${remoteRef}"`)?.split(/\s+/)[0] ?? '');
|
|
143
166
|
|
|
144
167
|
if (!actualRemoteSha) {
|
|
145
168
|
// New ref on Codeberg — filter the tip with no parent.
|
|
@@ -167,15 +190,8 @@ rl.on('close', () => {
|
|
|
167
190
|
shaMap[sha] = makeFilteredCommit(sha, filteredTree, mappedParents);
|
|
168
191
|
}
|
|
169
192
|
} else {
|
|
170
|
-
// Fallback: anchor not found.
|
|
171
|
-
// If the remote SHA doesn't exist locally (filtered commit from a prior push),
|
|
172
|
-
// tags can be safely skipped (immutable — already correct on Codeberg).
|
|
173
|
-
// Branches fall back to orphan filtering (best effort).
|
|
193
|
+
// Fallback: anchor not found — branch only (tags exit early above).
|
|
174
194
|
const remoteExists = !!gitOptional(`rev-parse --verify "${actualRemoteSha}"`);
|
|
175
|
-
if (!remoteExists && remoteRef.startsWith('refs/tags/')) {
|
|
176
|
-
console.log(` ↷ ${label} already on Codeberg — skipping`);
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
195
|
console.warn(` ⚠ Could not find local base for ${label}, filtering tip only`);
|
|
180
196
|
shaMap[localSha] = makeFilteredCommit(localSha, filterTree(localSha), remoteExists ? [actualRemoteSha] : []);
|
|
181
197
|
}
|
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.8.
|
|
6
|
+
"version": "1.8.3",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"registryType": "npm",
|
|
10
10
|
"identifier": "@dpesch/mantisbt-mcp-server",
|
|
11
|
-
"version": "1.8.
|
|
11
|
+
"version": "1.8.3",
|
|
12
12
|
"runtimeHint": "npx",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|