@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
package/dist/search/sync.js
CHANGED
|
@@ -41,12 +41,24 @@ export class SearchSyncService {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
async syncUnlocked(projectId, full) {
|
|
44
|
-
|
|
44
|
+
// Vectors made with another model or token limit are not comparable with
|
|
45
|
+
// new query vectors, and an incremental sync never replaces them — their
|
|
46
|
+
// issues are unchanged. Such an index is rebuilt, for all projects, since
|
|
47
|
+
// it is useless as it stands. An empty index only takes the signature.
|
|
48
|
+
const signature = this.embedder.signature;
|
|
49
|
+
const mismatch = (await this.store.getEmbeddingSignature()) !== signature;
|
|
50
|
+
const outdated = mismatch && (await this.store.count()) > 0;
|
|
51
|
+
const scope = outdated ? undefined : projectId;
|
|
52
|
+
if (full || outdated) {
|
|
45
53
|
await this.store.clear();
|
|
46
54
|
await this.store.resetLastSyncedAt();
|
|
47
55
|
}
|
|
56
|
+
// Stored before indexing: an interrupted rebuild then resumes on the next
|
|
57
|
+
// sync instead of starting over.
|
|
58
|
+
if (full || mismatch)
|
|
59
|
+
await this.store.setEmbeddingSignature(signature);
|
|
48
60
|
const lastSyncedAt = await this.store.getLastSyncedAt();
|
|
49
|
-
const { issues: allIssues, totalFromApi } = await this.fetchAllIssues(lastSyncedAt ?? undefined,
|
|
61
|
+
const { issues: allIssues, totalFromApi } = await this.fetchAllIssues(lastSyncedAt ?? undefined, scope);
|
|
50
62
|
let indexed = 0;
|
|
51
63
|
let skipped = 0;
|
|
52
64
|
let indexedSinceCheckpoint = 0;
|
|
@@ -62,21 +74,20 @@ export class SearchSyncService {
|
|
|
62
74
|
if (issue.updated_at && (await this.store.getItem(issue.id))?.metadata.updated_at === issue.updated_at) {
|
|
63
75
|
continue; // already indexed in this exact version
|
|
64
76
|
}
|
|
65
|
-
|
|
66
|
-
toEmbed.push({ issue, text });
|
|
77
|
+
toEmbed.push({ issue, doc: { summary: issue.summary, description: issue.description } });
|
|
67
78
|
}
|
|
68
79
|
if (toEmbed.length === 0)
|
|
69
80
|
continue;
|
|
70
|
-
const vectors = await this.embedder.
|
|
71
|
-
const batchItems = toEmbed.map((
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
updated_at:
|
|
78
|
-
}
|
|
79
|
-
})
|
|
81
|
+
const vectors = await this.embedder.embedDocuments(toEmbed.map(e => e.doc));
|
|
82
|
+
const batchItems = toEmbed.map(({ issue, doc }, j) => {
|
|
83
|
+
const [vector, ...chunks] = vectors[j];
|
|
84
|
+
return {
|
|
85
|
+
id: issue.id,
|
|
86
|
+
vector: vector,
|
|
87
|
+
...(chunks.length > 0 && { chunks }),
|
|
88
|
+
metadata: { ...doc, updated_at: issue.updated_at },
|
|
89
|
+
};
|
|
90
|
+
});
|
|
80
91
|
await this.store.addBatch(batchItems);
|
|
81
92
|
indexed += batchItems.length;
|
|
82
93
|
indexedSinceCheckpoint += batchItems.length;
|
|
@@ -96,7 +107,7 @@ export class SearchSyncService {
|
|
|
96
107
|
// changes for good. A project-scoped sync must not move the shared watermark,
|
|
97
108
|
// since changes in the other projects were not looked at.
|
|
98
109
|
const newestUpdatedAt = allIssues.find(issue => issue.updated_at)?.updated_at;
|
|
99
|
-
if (
|
|
110
|
+
if (scope === undefined && newestUpdatedAt && newestUpdatedAt !== lastSyncedAt) {
|
|
100
111
|
await this.store.setLastSyncedAt(newestUpdatedAt);
|
|
101
112
|
}
|
|
102
113
|
// Persist the best known total for get_search_index_status.
|
package/dist/search/tools.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { coerceBool } from '../coerce.js';
|
|
2
3
|
import { buildIssueViewUrl } from '../client.js';
|
|
3
4
|
import { SearchSyncService } from './sync.js';
|
|
4
5
|
import { getVersionHint } from '../version-hint.js';
|
|
6
|
+
import { firstIssue } from '../issue-envelope.js';
|
|
7
|
+
import { deprecationNote } from '../deprecation.js';
|
|
5
8
|
import { dateFilterSchema, matchesDateFilter, hasDateFilter } from '../date-filter.js';
|
|
6
9
|
import { extractTerms, highlightText, extractSnippet, hasTermMatch } from './highlight.js';
|
|
7
10
|
function buildHighlights(summary, description, terms) {
|
|
@@ -25,7 +28,17 @@ function errorText(msg) {
|
|
|
25
28
|
// ---------------------------------------------------------------------------
|
|
26
29
|
// registerSearchTools
|
|
27
30
|
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
31
|
+
/** Fill level of the search index (shared by get_metadata and get_search_index_status). */
|
|
32
|
+
export async function getSearchIndexStatus(store) {
|
|
33
|
+
const [indexed, lastSyncedAt, total] = await Promise.all([
|
|
34
|
+
store.count(),
|
|
35
|
+
store.getLastSyncedAt(),
|
|
36
|
+
store.getLastKnownTotal(),
|
|
37
|
+
]);
|
|
38
|
+
const percent = total !== null ? (total > 0 ? Math.round((indexed / total) * 100) : 0) : null;
|
|
39
|
+
const summary = total !== null ? `${indexed}/${total} (${percent} %)` : `${indexed}/? (total unknown)`;
|
|
40
|
+
return { summary, indexed, total, percent, last_synced_at: lastSyncedAt };
|
|
41
|
+
}
|
|
29
42
|
export function registerSearchTools(server, client, store, embedder, lockDir) {
|
|
30
43
|
// ---------------------------------------------------------------------------
|
|
31
44
|
// search_issues
|
|
@@ -33,7 +46,10 @@ export function registerSearchTools(server, client, store, embedder, lockDir) {
|
|
|
33
46
|
server.registerTool('search_issues', {
|
|
34
47
|
title: 'Semantic Issue Search',
|
|
35
48
|
description: 'Search MantisBT issues using natural language. Returns the most relevant issues ' +
|
|
36
|
-
'by semantic similarity
|
|
49
|
+
'by combining semantic similarity with keyword matching, so exact terms such as identifiers, ' +
|
|
50
|
+
'error codes or version numbers are found as well. "score" is the semantic similarity; results ' +
|
|
51
|
+
'are ordered by the combined ranking, so a keyword match can precede a higher score. ' +
|
|
52
|
+
'The search index must be populated first via rebuild_search_index.',
|
|
37
53
|
inputSchema: z.object({
|
|
38
54
|
query: z.string().describe('Natural language search query'),
|
|
39
55
|
top_n: z
|
|
@@ -79,7 +95,7 @@ export function registerSearchTools(server, client, store, embedder, lockDir) {
|
|
|
79
95
|
embedder.embed(query),
|
|
80
96
|
client.getBaseUrl(),
|
|
81
97
|
]);
|
|
82
|
-
const results = await store.search(queryVector, top_n);
|
|
98
|
+
const results = await store.search(queryVector, top_n, query);
|
|
83
99
|
if (!select) {
|
|
84
100
|
// For filtering or highlighting we need store metadata per result
|
|
85
101
|
if (!filterActive && !terms.length) {
|
|
@@ -107,7 +123,7 @@ export function registerSearchTools(server, client, store, embedder, lockDir) {
|
|
|
107
123
|
const enriched = await Promise.all(results.map(async ({ id, score }) => {
|
|
108
124
|
try {
|
|
109
125
|
const issueResult = await client.get(`issues/${id}`);
|
|
110
|
-
const issue = issueResult
|
|
126
|
+
const issue = (firstIssue(issueResult) ?? {});
|
|
111
127
|
if (filterActive && !matchesDateFilter(issue, dateFilter)) {
|
|
112
128
|
return null;
|
|
113
129
|
}
|
|
@@ -153,7 +169,8 @@ export function registerSearchTools(server, client, store, embedder, lockDir) {
|
|
|
153
169
|
// ---------------------------------------------------------------------------
|
|
154
170
|
server.registerTool('get_search_index_status', {
|
|
155
171
|
title: 'Search Index Status',
|
|
156
|
-
description: '
|
|
172
|
+
description: `${deprecationNote('get_search_index_status', 'get_metadata')}\n\n` +
|
|
173
|
+
'Returns the current fill level of the semantic search index: how many issues are ' +
|
|
157
174
|
'indexed vs. the total number of issues in MantisBT, plus the timestamp of the last sync.',
|
|
158
175
|
inputSchema: z.object({}),
|
|
159
176
|
annotations: {
|
|
@@ -163,24 +180,8 @@ export function registerSearchTools(server, client, store, embedder, lockDir) {
|
|
|
163
180
|
},
|
|
164
181
|
}, async () => {
|
|
165
182
|
try {
|
|
166
|
-
const [indexed, lastSyncedAt, total] = await Promise.all([
|
|
167
|
-
store.count(),
|
|
168
|
-
store.getLastSyncedAt(),
|
|
169
|
-
store.getLastKnownTotal(),
|
|
170
|
-
]);
|
|
171
|
-
const percent = total !== null
|
|
172
|
-
? (total > 0 ? Math.round((indexed / total) * 100) : 0)
|
|
173
|
-
: null;
|
|
174
|
-
const summary = total !== null
|
|
175
|
-
? `${indexed}/${total} (${percent} %)`
|
|
176
|
-
: `${indexed}/? (total unknown)`;
|
|
177
183
|
return {
|
|
178
|
-
content: [
|
|
179
|
-
{
|
|
180
|
-
type: 'text',
|
|
181
|
-
text: JSON.stringify({ summary, indexed, total, percent, last_synced_at: lastSyncedAt }, null, 2),
|
|
182
|
-
},
|
|
183
|
-
],
|
|
184
|
+
content: [{ type: 'text', text: JSON.stringify(await getSearchIndexStatus(store), null, 2) }],
|
|
184
185
|
};
|
|
185
186
|
}
|
|
186
187
|
catch (error) {
|
package/dist/tools/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ISSUE_ENUM_OPTIONS, MANTIS_CANONICAL_ENUM_NAMES } from '../constants.js';
|
|
3
3
|
import { getVersionHint } from '../version-hint.js';
|
|
4
|
+
import { deprecationNote } from '../deprecation.js';
|
|
4
5
|
function errorText(msg) {
|
|
5
6
|
const vh = getVersionHint();
|
|
6
7
|
vh?.triggerLatestVersionFetch();
|
|
@@ -127,7 +128,9 @@ Common option names:
|
|
|
127
128
|
// ---------------------------------------------------------------------------
|
|
128
129
|
server.registerTool('get_issue_enums', {
|
|
129
130
|
title: 'Get Issue Enum Values',
|
|
130
|
-
description:
|
|
131
|
+
description: `${deprecationNote('get_issue_enums', 'get_issue_schema')}
|
|
132
|
+
|
|
133
|
+
Return valid ID, name, and (if available) localized label for all issue enum fields.
|
|
131
134
|
|
|
132
135
|
Use this tool before creating or updating issues to look up the correct value
|
|
133
136
|
for severity, status, priority, resolution, or reproducibility.
|
|
@@ -189,7 +192,9 @@ because there is no separate English original.`,
|
|
|
189
192
|
// ---------------------------------------------------------------------------
|
|
190
193
|
server.registerTool('list_languages', {
|
|
191
194
|
title: 'List Supported Languages',
|
|
192
|
-
description: '
|
|
195
|
+
description: `${deprecationNote('list_languages', 'get_current_user (field "language")')}
|
|
196
|
+
|
|
197
|
+
Despite its name, MantisBT's GET /lang returns only the language of the current user ({ strings, language }), not a list of all languages.`,
|
|
193
198
|
inputSchema: z.object({}),
|
|
194
199
|
annotations: {
|
|
195
200
|
readOnlyHint: true,
|
|
@@ -217,7 +222,7 @@ because there is no separate English original.`,
|
|
|
217
222
|
|
|
218
223
|
The MantisBT REST API exposes a GET /tags endpoint on some installations.
|
|
219
224
|
If that endpoint is not available, this tool falls back to the local metadata
|
|
220
|
-
cache populated by
|
|
225
|
+
cache populated by get_metadata.`,
|
|
221
226
|
inputSchema: z.object({
|
|
222
227
|
page: z.coerce.number().int().positive().default(1).describe('Page number (default: 1)'),
|
|
223
228
|
page_size: z.coerce.number().int().min(1).max(200).default(50).describe('Tags per page (default: 50)'),
|
|
@@ -252,7 +257,7 @@ cache populated by sync_metadata.`,
|
|
|
252
257
|
return {
|
|
253
258
|
content: [{
|
|
254
259
|
type: 'text',
|
|
255
|
-
text: `Error: ${msg}\n\nThe GET /tags endpoint is not available in this MantisBT installation. No cached tags found either — run
|
|
260
|
+
text: `Error: ${msg}\n\nThe GET /tags endpoint is not available in this MantisBT installation. No cached tags found either — run get_metadata with refresh: true to populate the cache if your installation provides this endpoint.`,
|
|
256
261
|
}],
|
|
257
262
|
isError: true,
|
|
258
263
|
};
|
package/dist/tools/files.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { basename, resolve, sep } from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import { enrichIssue } from '../issue-enrich.js';
|
|
4
5
|
import { getVersionHint } from '../version-hint.js';
|
|
6
|
+
import { firstIssue } from '../issue-envelope.js';
|
|
7
|
+
import { deprecationNote } from '../deprecation.js';
|
|
5
8
|
function errorText(msg) {
|
|
6
9
|
const vh = getVersionHint();
|
|
7
10
|
vh?.triggerLatestVersionFetch();
|
|
@@ -21,9 +24,9 @@ export function registerFileTools(server, client, uploadDir, transport = 'stdio'
|
|
|
21
24
|
// ---------------------------------------------------------------------------
|
|
22
25
|
server.registerTool('list_issue_files', {
|
|
23
26
|
title: 'List Issue File Attachments',
|
|
24
|
-
description:
|
|
27
|
+
description: `${deprecationNote('list_issue_files', 'get_issue with select: "attachments"')}
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
List all file attachments of a MantisBT issue. Returns an array of attachment objects, each containing id, filename, size in bytes, content_type, and download_url. Returns an empty array if the issue has no attachments.`,
|
|
27
30
|
inputSchema: z.object({
|
|
28
31
|
issue_id: z.coerce.number().int().positive().describe('Numeric issue ID'),
|
|
29
32
|
}),
|
|
@@ -34,8 +37,12 @@ Use this tool when you need to inspect or enumerate files attached to an issue.
|
|
|
34
37
|
},
|
|
35
38
|
}, async ({ issue_id }) => {
|
|
36
39
|
try {
|
|
37
|
-
const result = await
|
|
38
|
-
|
|
40
|
+
const [result, baseUrl] = await Promise.all([
|
|
41
|
+
client.get(`issues/${issue_id}`, { select: 'id,attachments' }),
|
|
42
|
+
client.getBaseUrl(),
|
|
43
|
+
]);
|
|
44
|
+
const issue = firstIssue(result);
|
|
45
|
+
const attachments = (issue && enrichIssue(issue, baseUrl, issue_id).attachments) ?? [];
|
|
39
46
|
return {
|
|
40
47
|
content: [{ type: 'text', text: JSON.stringify(attachments, null, 2) }],
|
|
41
48
|
};
|
|
@@ -58,7 +65,7 @@ Provide exactly one of the two input modes:
|
|
|
58
65
|
|
|
59
66
|
The optional content_type sets the MIME type (e.g. "image/png"); defaults to "application/octet-stream". Use the optional description to annotate the attachment.
|
|
60
67
|
|
|
61
|
-
Use this tool to attach files such as logs, screenshots, or patches to an existing issue. To list existing attachments, use
|
|
68
|
+
Use this tool to attach files such as logs, screenshots, or patches to an existing issue. To list existing attachments, use get_issue with select: "attachments".`,
|
|
62
69
|
inputSchema: z.object({
|
|
63
70
|
issue_id: z.coerce.number().int().positive().describe('Numeric issue ID'),
|
|
64
71
|
file_path: z.string().min(1).optional().describe('Preferred: absolute path to the local file to upload — use this whenever the file exists on disk (mutually exclusive with content)'),
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { registerIssueTools } from './issues.js';
|
|
2
|
+
import { registerNoteTools } from './notes.js';
|
|
3
|
+
import { registerFileTools } from './files.js';
|
|
4
|
+
import { registerRelationshipTools } from './relationships.js';
|
|
5
|
+
import { registerMonitorTools } from './monitors.js';
|
|
6
|
+
import { registerProjectTools } from './projects.js';
|
|
7
|
+
import { registerProjectVersionTools } from './project-versions.js';
|
|
8
|
+
import { registerUserTools } from './users.js';
|
|
9
|
+
import { registerFilterTools } from './filters.js';
|
|
10
|
+
import { registerConfigTools } from './config.js';
|
|
11
|
+
import { registerMetadataTools } from './metadata.js';
|
|
12
|
+
import { registerTagTools } from './tags.js';
|
|
13
|
+
import { registerVersionTools } from './version.js';
|
|
14
|
+
/**
|
|
15
|
+
* Registers every tool except the optional search tools, which the search
|
|
16
|
+
* module registers itself. Shared by createMcpServer and the test that checks
|
|
17
|
+
* TOOL_GROUPS against the registered tools, so both see the same wiring.
|
|
18
|
+
*/
|
|
19
|
+
export function registerAllTools(server, deps) {
|
|
20
|
+
const { client, cache } = deps;
|
|
21
|
+
registerIssueTools(server, client, cache);
|
|
22
|
+
registerNoteTools(server, client);
|
|
23
|
+
registerFileTools(server, client, deps.uploadDir, deps.transport);
|
|
24
|
+
registerRelationshipTools(server, client);
|
|
25
|
+
registerMonitorTools(server, client);
|
|
26
|
+
registerProjectTools(server, client, cache);
|
|
27
|
+
registerProjectVersionTools(server, client, cache);
|
|
28
|
+
registerUserTools(server, client);
|
|
29
|
+
registerFilterTools(server, client);
|
|
30
|
+
registerConfigTools(server, client, cache);
|
|
31
|
+
registerMetadataTools(server, client, cache, deps.searchIndexStatus);
|
|
32
|
+
registerTagTools(server, client);
|
|
33
|
+
registerVersionTools(server, client, deps.versionHint, deps.version);
|
|
34
|
+
}
|