@dpesch/mantisbt-mcp-server 1.2.0 → 1.3.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 +20 -0
- package/README.de.md +25 -4
- package/README.md +25 -4
- package/dist/index.js +2 -2
- package/dist/search/index.js +6 -0
- package/dist/search/store.js +16 -0
- package/dist/search/sync.js +18 -6
- package/dist/search/tools.js +53 -6
- package/dist/tools/config.js +106 -10
- package/dist/tools/files.js +2 -2
- package/dist/tools/issues.js +13 -14
- package/dist/tools/metadata.js +56 -17
- package/dist/tools/monitors.js +2 -2
- package/dist/tools/notes.js +4 -4
- package/dist/tools/projects.js +13 -6
- package/dist/tools/relationships.js +5 -5
- package/dist/tools/tags.js +4 -4
- package/dist/tools/version.js +16 -1
- package/package.json +1 -1
- package/scripts/record-fixtures.ts +1 -1
- package/tests/cache.test.ts +1 -0
- package/tests/fixtures/get_current_user.json +6 -5
- package/tests/fixtures/get_issue.json +43 -83
- package/tests/fixtures/get_issue_fields_sample.json +17 -51
- package/tests/fixtures/get_project_categories.json +99 -2
- package/tests/fixtures/get_project_versions_with_data.json +35 -11
- package/tests/fixtures/list_issues.json +51 -57
- package/tests/fixtures/list_projects.json +45 -45
- package/tests/helpers/mock-server.ts +38 -4
- package/tests/helpers/search-mocks.ts +3 -1
- package/tests/search/sync.test.ts +50 -0
- package/tests/search/tools.test.ts +97 -0
- package/tests/tools/config.test.ts +97 -0
- package/tests/tools/issues.test.ts +51 -4
- package/tests/tools/metadata.test.ts +122 -0
- package/tests/tools/projects.test.ts +31 -0
- package/tests/tools/string-coercion.test.ts +251 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.3.0] – 2026-03-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- New tool `get_search_index_status`: returns the current fill level of the semantic search index — how many issues are indexed vs. total, plus the timestamp of the last sync. Only active when `MANTIS_SEARCH_ENABLED=true`.
|
|
14
|
+
- New tool `get_issue_enums`: returns structured ID/name pairs for all issue enum fields (severity, status, priority, resolution, reproducibility) — ready for direct use in `create_issue` / `update_issue` without requiring knowledge of MantisBT-internal config option names.
|
|
15
|
+
- `sync_metadata` now fetches and caches all tags globally (`tags` field at root level of the cached metadata). When the dedicated `GET /tags` endpoint is unavailable (MantisBT < 2.26), tags are collected by scanning all issues across all projects (`select=id,tags`).
|
|
16
|
+
- New tool `get_mcp_version`: returns the version of the running mantisbt-mcp-server instance.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- `list_tags` now falls back to the metadata cache when `GET /tags` returns 404 instead of returning an error. Run `sync_metadata` first to populate the cache.
|
|
20
|
+
- Numeric ID parameters now accept string inputs (e.g. `"1940"`) — MCP clients that pass IDs as strings no longer receive error -32602.
|
|
21
|
+
- `create_issue` now always sends a `severity` to MantisBT (default: `"minor"`). Previously omitting severity caused MantisBT to store `0`, which was displayed as `@0@`.
|
|
22
|
+
- `get_search_index_status` now correctly reports the total issue count on MantisBT installations that do not return `total_count` in the issues list API. The total is persisted after every sync: `total_count` from the API takes precedence, otherwise the current store size is used as a best-effort estimate. The status tool will therefore no longer show "total unknown" after any sync has completed.
|
|
23
|
+
- `sync_metadata` tags endpoint failure now degrades gracefully to an empty array instead of propagating an error.
|
|
24
|
+
- `sync_metadata` now correctly populates `byProject[id].categories`.
|
|
25
|
+
- `sync_metadata` now fetches all versions including obsolete and inherited ones (`obsolete=1&inherit=1`). Previously only non-obsolete versions were returned, causing the version count in the cache to be far too low.
|
|
26
|
+
- `get_project_versions` now accepts optional `obsolete` and `inherit` boolean parameters to include obsolete and/or inherited versions (both default to `false`). Previously the wrong endpoint (`projects/{id}/categories`) was called, which returned an empty array on many installations. Categories are now read from the project detail response (`projects/{id}`), identical to the source used by `get_project_categories`.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
10
30
|
## [1.2.0] – 2026-03-16
|
|
11
31
|
|
|
12
32
|
### Added
|
package/README.de.md
CHANGED
|
@@ -130,7 +130,7 @@ Falls keine Umgebungsvariablen gesetzt sind, wird `~/.claude/mantis.json` ausgel
|
|
|
130
130
|
|
|
131
131
|
| Tool | Beschreibung |
|
|
132
132
|
|---|---|
|
|
133
|
-
| `list_tags` | Alle verfügbaren Tags auflisten |
|
|
133
|
+
| `list_tags` | Alle verfügbaren Tags auflisten; greift auf den Metadaten-Cache zurück, wenn `GET /tags` mit 404 antwortet (vorher `sync_metadata` ausführen) |
|
|
134
134
|
| `attach_tags` | Tags an ein Issue hängen |
|
|
135
135
|
| `detach_tag` | Tag von einem Issue entfernen |
|
|
136
136
|
|
|
@@ -139,20 +139,39 @@ Falls keine Umgebungsvariablen gesetzt sind, wird `~/.claude/mantis.json` ausgel
|
|
|
139
139
|
| Tool | Beschreibung |
|
|
140
140
|
|---|---|
|
|
141
141
|
| `list_projects` | Alle zugänglichen Projekte auflisten |
|
|
142
|
-
| `get_project_versions` | Versionen eines Projekts abrufen |
|
|
142
|
+
| `get_project_versions` | Versionen eines Projekts abrufen; optionale Booleans `obsolete` und `inherit` für veraltete bzw. vom Elternprojekt geerbte Versionen |
|
|
143
143
|
| `get_project_categories` | Kategorien eines Projekts abrufen |
|
|
144
144
|
| `get_project_users` | Benutzer eines Projekts abrufen |
|
|
145
145
|
|
|
146
146
|
### Semantische Suche *(optional)*
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
Statt einfachem Keyword-Matching versteht die semantische Suche die *Bedeutung* einer Anfrage. Formuliere in natürlicher Sprache — die Suche findet konzeptuell verwandte Issues, auch wenn die genauen Begriffe nicht übereinstimmen:
|
|
149
|
+
|
|
150
|
+
- *„Login funktioniert nach Passwort-Reset nicht"* — findet Issues rund um Authentifizierungsgrenzfälle
|
|
151
|
+
- *„Performance-Probleme auf der Checkout-Seite"* — liefert verwandte Meldungen unabhängig von der verwendeten Terminologie
|
|
152
|
+
- *„doppelte Einträge in der Rechnungsliste"* — erkennt auch Issues, die als „zweifach angezeigt", „dupliziert" o.ä. beschrieben sind
|
|
153
|
+
|
|
154
|
+
Das Embedding-Modell (~80 MB) läuft vollständig **offline** — kein OpenAI-Key, keine externe API. Es wird beim ersten Start einmalig heruntergeladen und lokal gecacht. Issues werden bei jedem Serverstart inkrementell indexiert (nur neue und geänderte Issues werden neu verarbeitet).
|
|
155
|
+
|
|
156
|
+
Aktivierung mit `MANTIS_SEARCH_ENABLED=true`.
|
|
149
157
|
|
|
150
158
|
| Tool | Beschreibung |
|
|
151
159
|
|---|---|
|
|
152
160
|
| `search_issues` | Natürlichsprachige Suche über alle indizierten Issues – liefert Top-N-Ergebnisse mit Cosine-Similarity-Score |
|
|
153
161
|
| `rebuild_search_index` | Suchindex aufbauen oder aktualisieren; `full: true` löscht und baut ihn vollständig neu |
|
|
162
|
+
| `get_search_index_status` | Aktuellen Füllstand des Suchindex zurückgeben: wie viele Issues bereits indiziert sind im Verhältnis zur Gesamtanzahl, plus Zeitstempel der letzten Synchronisation |
|
|
163
|
+
|
|
164
|
+
#### Welches Backend wählen?
|
|
165
|
+
|
|
166
|
+
| | `vectra` *(Standard)* | `sqlite-vec` |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| Abhängigkeiten | Keine (reines JS) | Benötigt native Build-Tools |
|
|
169
|
+
| Installation | Enthalten | `npm install sqlite-vec better-sqlite3` |
|
|
170
|
+
| Geeignet für | Bis ~10.000 Issues | Ab 10.000 Issues |
|
|
171
|
+
| Performance | Für die meisten Instanzen ausreichend | Schneller bei großen Datenmengen |
|
|
172
|
+
|
|
173
|
+
Mit `vectra` starten. Zu `sqlite-vec` wechseln, wenn Indexierungszeiten oder Abfragen spürbar langsam werden.
|
|
154
174
|
|
|
155
|
-
**`sqlite-vec`-Backend** (optional, schneller bei großen Instanzen):
|
|
156
175
|
```bash
|
|
157
176
|
npm install sqlite-vec better-sqlite3
|
|
158
177
|
# dann MANTIS_SEARCH_BACKEND=sqlite-vec setzen
|
|
@@ -169,7 +188,9 @@ npm install sqlite-vec better-sqlite3
|
|
|
169
188
|
| `get_current_user` | Eigenes Benutzerprofil abrufen |
|
|
170
189
|
| `list_languages` | Verfügbare Sprachen auflisten |
|
|
171
190
|
| `get_config` | Server-Konfiguration (Basis-URL, Cache-TTL) anzeigen |
|
|
191
|
+
| `get_issue_enums` | Gültige ID/Name-Paare für alle Enum-Felder zurückgeben (Severity, Status, Priority, Resolution, Reproducibility) — vor `create_issue` / `update_issue` verwenden, um korrekte Werte nachzuschlagen |
|
|
172
192
|
| `get_mantis_version` | MantisBT-Version abrufen und auf Updates prüfen |
|
|
193
|
+
| `get_mcp_version` | Version dieser mantisbt-mcp-server-Instanz zurückgeben |
|
|
173
194
|
|
|
174
195
|
## HTTP-Modus
|
|
175
196
|
|
package/README.md
CHANGED
|
@@ -130,7 +130,7 @@ If no environment variables are set, `~/.claude/mantis.json` is read:
|
|
|
130
130
|
|
|
131
131
|
| Tool | Description |
|
|
132
132
|
|---|---|
|
|
133
|
-
| `list_tags` | List all available tags |
|
|
133
|
+
| `list_tags` | List all available tags; falls back to the metadata cache when `GET /tags` returns 404 (run `sync_metadata` first to populate) |
|
|
134
134
|
| `attach_tags` | Attach tags to an issue |
|
|
135
135
|
| `detach_tag` | Remove a tag from an issue |
|
|
136
136
|
|
|
@@ -139,20 +139,39 @@ If no environment variables are set, `~/.claude/mantis.json` is read:
|
|
|
139
139
|
| Tool | Description |
|
|
140
140
|
|---|---|
|
|
141
141
|
| `list_projects` | List all accessible projects |
|
|
142
|
-
| `get_project_versions` | Get versions of a project |
|
|
142
|
+
| `get_project_versions` | Get versions of a project; optional `obsolete` and `inherit` booleans to include obsolete or parent-inherited versions |
|
|
143
143
|
| `get_project_categories` | Get categories of a project |
|
|
144
144
|
| `get_project_users` | Get users of a project |
|
|
145
145
|
|
|
146
146
|
### Semantic search *(optional)*
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
Instead of exact keyword matching, semantic search understands the *meaning* behind a query. Ask in plain language — the search engine finds conceptually related issues even when the wording doesn't match:
|
|
149
|
+
|
|
150
|
+
- *"login fails after password reset"* — finds issues about authentication edge cases
|
|
151
|
+
- *"performance problems on the checkout page"* — surfaces related reports regardless of the exact terminology used
|
|
152
|
+
- *"duplicate entries in the invoice list"* — catches issues described as "shown twice", "double records", etc.
|
|
153
|
+
|
|
154
|
+
The embedding model (~80 MB) runs entirely **offline** — no OpenAI key, no external API. It is downloaded once on first start and cached locally. Issues are indexed incrementally on every server start (only new and updated issues are re-indexed).
|
|
155
|
+
|
|
156
|
+
Activate with `MANTIS_SEARCH_ENABLED=true`.
|
|
149
157
|
|
|
150
158
|
| Tool | Description |
|
|
151
159
|
|---|---|
|
|
152
160
|
| `search_issues` | Natural language search over all indexed issues — returns top-N results with cosine similarity score |
|
|
153
161
|
| `rebuild_search_index` | Build or update the search index; `full: true` clears and rebuilds from scratch |
|
|
162
|
+
| `get_search_index_status` | Return the current fill level of the search index: how many issues are indexed vs. total, and the timestamp of the last sync |
|
|
163
|
+
|
|
164
|
+
#### Which backend to choose?
|
|
165
|
+
|
|
166
|
+
| | `vectra` *(default)* | `sqlite-vec` |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| Dependencies | None (pure JS) | Requires native build tools |
|
|
169
|
+
| Install | Included | `npm install sqlite-vec better-sqlite3` |
|
|
170
|
+
| Best for | Up to ~10,000 issues | 10,000+ issues |
|
|
171
|
+
| Performance | Fast enough for most setups | Faster for large corpora |
|
|
172
|
+
|
|
173
|
+
Start with `vectra`. Switch to `sqlite-vec` if indexing or query times become noticeably slow.
|
|
154
174
|
|
|
155
|
-
**`sqlite-vec` backend** (optional, faster for large instances):
|
|
156
175
|
```bash
|
|
157
176
|
npm install sqlite-vec better-sqlite3
|
|
158
177
|
# then set MANTIS_SEARCH_BACKEND=sqlite-vec
|
|
@@ -169,7 +188,9 @@ npm install sqlite-vec better-sqlite3
|
|
|
169
188
|
| `get_current_user` | Retrieve your own user profile |
|
|
170
189
|
| `list_languages` | List available languages |
|
|
171
190
|
| `get_config` | Show server configuration (base URL, cache TTL) |
|
|
191
|
+
| `get_issue_enums` | Return valid ID/name pairs for all issue enum fields (severity, status, priority, resolution, reproducibility) — use before `create_issue` / `update_issue` to look up correct values |
|
|
172
192
|
| `get_mantis_version` | Get MantisBT version and check for updates |
|
|
193
|
+
| `get_mcp_version` | Return the version of this mantisbt-mcp-server instance |
|
|
173
194
|
|
|
174
195
|
## HTTP mode
|
|
175
196
|
|
package/dist/index.js
CHANGED
|
@@ -50,10 +50,10 @@ async function createMcpServer() {
|
|
|
50
50
|
registerProjectTools(server, client);
|
|
51
51
|
registerUserTools(server, client);
|
|
52
52
|
registerFilterTools(server, client);
|
|
53
|
-
registerConfigTools(server, client);
|
|
53
|
+
registerConfigTools(server, client, cache);
|
|
54
54
|
registerMetadataTools(server, client, cache);
|
|
55
55
|
registerTagTools(server, client);
|
|
56
|
-
registerVersionTools(server, client, versionHint);
|
|
56
|
+
registerVersionTools(server, client, versionHint, version);
|
|
57
57
|
// Optional: Semantic search module
|
|
58
58
|
if (config.search.enabled) {
|
|
59
59
|
const { initializeSearchModule } = await import('./search/index.js');
|
package/dist/search/index.js
CHANGED
|
@@ -11,6 +11,12 @@ export async function initializeSearchModule(server, client, config) {
|
|
|
11
11
|
const store = createVectorStore(config.backend, config.dir);
|
|
12
12
|
const embedder = new Embedder(config.modelName);
|
|
13
13
|
registerSearchTools(server, client, store, embedder);
|
|
14
|
+
// Pre-initialize lastKnownTotal so get_search_index_status shows a value
|
|
15
|
+
// immediately on startup, even while the background sync is still running.
|
|
16
|
+
const [startupCount, startupTotal] = await Promise.all([store.count(), store.getLastKnownTotal()]);
|
|
17
|
+
if (startupTotal === null && startupCount > 0) {
|
|
18
|
+
await store.setLastKnownTotal(startupCount);
|
|
19
|
+
}
|
|
14
20
|
// Non-blocking background sync on startup
|
|
15
21
|
const syncService = new SearchSyncService(client, store, embedder);
|
|
16
22
|
syncService.sync().catch((err) => {
|
package/dist/search/store.js
CHANGED
|
@@ -7,12 +7,14 @@ export class VectraStore {
|
|
|
7
7
|
dir;
|
|
8
8
|
vectraDir;
|
|
9
9
|
lastSyncFile;
|
|
10
|
+
lastTotalFile;
|
|
10
11
|
items = new Map();
|
|
11
12
|
loaded = false;
|
|
12
13
|
constructor(dir) {
|
|
13
14
|
this.dir = dir;
|
|
14
15
|
this.vectraDir = join(dir, 'vectra');
|
|
15
16
|
this.lastSyncFile = join(dir, 'last_sync.txt');
|
|
17
|
+
this.lastTotalFile = join(dir, 'last_total.txt');
|
|
16
18
|
}
|
|
17
19
|
async ensureLoaded() {
|
|
18
20
|
if (this.loaded)
|
|
@@ -93,6 +95,20 @@ export class VectraStore {
|
|
|
93
95
|
throw err;
|
|
94
96
|
}
|
|
95
97
|
}
|
|
98
|
+
async getLastKnownTotal() {
|
|
99
|
+
try {
|
|
100
|
+
const content = await readFile(this.lastTotalFile, 'utf-8');
|
|
101
|
+
const parsed = parseInt(content.trim(), 10);
|
|
102
|
+
return isNaN(parsed) ? null : parsed;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async setLastKnownTotal(total) {
|
|
109
|
+
await mkdir(this.dir, { recursive: true });
|
|
110
|
+
await writeFile(this.lastTotalFile, String(total), 'utf-8');
|
|
111
|
+
}
|
|
96
112
|
}
|
|
97
113
|
// ---------------------------------------------------------------------------
|
|
98
114
|
// Cosine similarity helper
|
package/dist/search/sync.js
CHANGED
|
@@ -14,7 +14,7 @@ export class SearchSyncService {
|
|
|
14
14
|
}
|
|
15
15
|
async sync(projectId) {
|
|
16
16
|
const lastSyncedAt = await this.store.getLastSyncedAt();
|
|
17
|
-
const allIssues = await this.fetchAllIssues(lastSyncedAt ?? undefined, projectId);
|
|
17
|
+
const { issues: allIssues, totalFromApi } = await this.fetchAllIssues(lastSyncedAt ?? undefined, projectId);
|
|
18
18
|
let indexed = 0;
|
|
19
19
|
let skipped = 0;
|
|
20
20
|
// Process in batches of EMBED_BATCH_SIZE
|
|
@@ -45,10 +45,19 @@ export class SearchSyncService {
|
|
|
45
45
|
indexed += batchItems.length;
|
|
46
46
|
}
|
|
47
47
|
await this.store.setLastSyncedAt(new Date().toISOString());
|
|
48
|
-
|
|
48
|
+
// Persist the best known total for get_search_index_status.
|
|
49
|
+
// Priority: API total_count > full-rebuild count > current store size.
|
|
50
|
+
// The store size fallback handles MantisBT installations that never return
|
|
51
|
+
// total_count and ensures the status tool never shows "total unknown" after
|
|
52
|
+
// any sync has completed.
|
|
53
|
+
const storeCount = await this.store.count();
|
|
54
|
+
const total = totalFromApi ?? (lastSyncedAt === null ? indexed + skipped : storeCount);
|
|
55
|
+
await this.store.setLastKnownTotal(total);
|
|
56
|
+
return { indexed, skipped, total };
|
|
49
57
|
}
|
|
50
58
|
async fetchAllIssues(updatedAfter, projectId) {
|
|
51
59
|
const allIssues = [];
|
|
60
|
+
let totalFromApi = null;
|
|
52
61
|
let page = 1;
|
|
53
62
|
while (true) {
|
|
54
63
|
const params = {
|
|
@@ -67,12 +76,15 @@ export class SearchSyncService {
|
|
|
67
76
|
const response = await this.client.get('issues', params);
|
|
68
77
|
const pageIssues = response.issues ?? [];
|
|
69
78
|
allIssues.push(...pageIssues);
|
|
79
|
+
// Capture total_count from the first page that provides it
|
|
80
|
+
if (totalFromApi === null && response.total_count != null) {
|
|
81
|
+
totalFromApi = response.total_count;
|
|
82
|
+
}
|
|
70
83
|
// Stop when we have fetched all issues:
|
|
71
84
|
// - total_count is provided and reached, or
|
|
72
85
|
// - page returned fewer items than requested (last page)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (allIssues.length >= total)
|
|
86
|
+
if (totalFromApi !== null) {
|
|
87
|
+
if (allIssues.length >= totalFromApi)
|
|
76
88
|
break;
|
|
77
89
|
}
|
|
78
90
|
else if (pageIssues.length < PAGE_SIZE) {
|
|
@@ -80,6 +92,6 @@ export class SearchSyncService {
|
|
|
80
92
|
}
|
|
81
93
|
page++;
|
|
82
94
|
}
|
|
83
|
-
return allIssues;
|
|
95
|
+
return { issues: allIssues, totalFromApi };
|
|
84
96
|
}
|
|
85
97
|
}
|
package/dist/search/tools.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { SearchSyncService } from './sync.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
|
+
}
|
|
3
10
|
// ---------------------------------------------------------------------------
|
|
4
11
|
// registerSearchTools
|
|
5
12
|
// ---------------------------------------------------------------------------
|
|
@@ -14,7 +21,7 @@ export function registerSearchTools(server, client, store, embedder) {
|
|
|
14
21
|
inputSchema: z.object({
|
|
15
22
|
query: z.string().describe('Natural language search query'),
|
|
16
23
|
top_n: z
|
|
17
|
-
.number()
|
|
24
|
+
.coerce.number()
|
|
18
25
|
.int()
|
|
19
26
|
.positive()
|
|
20
27
|
.max(50)
|
|
@@ -48,7 +55,47 @@ export function registerSearchTools(server, client, store, embedder) {
|
|
|
48
55
|
}
|
|
49
56
|
catch (error) {
|
|
50
57
|
const msg = error instanceof Error ? error.message : String(error);
|
|
51
|
-
return { content: [{ type: 'text', text:
|
|
58
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// get_search_index_status
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
server.registerTool('get_search_index_status', {
|
|
65
|
+
title: 'Search Index Status',
|
|
66
|
+
description: 'Returns the current fill level of the semantic search index: how many issues are ' +
|
|
67
|
+
'indexed vs. the total number of issues in MantisBT, plus the timestamp of the last sync.',
|
|
68
|
+
inputSchema: z.object({}),
|
|
69
|
+
annotations: {
|
|
70
|
+
readOnlyHint: true,
|
|
71
|
+
destructiveHint: false,
|
|
72
|
+
idempotentHint: true,
|
|
73
|
+
},
|
|
74
|
+
}, async () => {
|
|
75
|
+
try {
|
|
76
|
+
const [indexed, lastSyncedAt, total] = await Promise.all([
|
|
77
|
+
store.count(),
|
|
78
|
+
store.getLastSyncedAt(),
|
|
79
|
+
store.getLastKnownTotal(),
|
|
80
|
+
]);
|
|
81
|
+
const percent = total !== null
|
|
82
|
+
? (total > 0 ? Math.round((indexed / total) * 100) : 0)
|
|
83
|
+
: null;
|
|
84
|
+
const summary = total !== null
|
|
85
|
+
? `${indexed}/${total} (${percent} %)`
|
|
86
|
+
: `${indexed}/? (total unknown)`;
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: 'text',
|
|
91
|
+
text: JSON.stringify({ summary, indexed, total, percent, last_synced_at: lastSyncedAt }, null, 2),
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
98
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
52
99
|
}
|
|
53
100
|
});
|
|
54
101
|
// ---------------------------------------------------------------------------
|
|
@@ -60,7 +107,7 @@ export function registerSearchTools(server, client, store, embedder) {
|
|
|
60
107
|
'Use full: true to clear the existing index and rebuild from scratch.',
|
|
61
108
|
inputSchema: z.object({
|
|
62
109
|
project_id: z
|
|
63
|
-
.number()
|
|
110
|
+
.coerce.number()
|
|
64
111
|
.int()
|
|
65
112
|
.positive()
|
|
66
113
|
.optional()
|
|
@@ -83,20 +130,20 @@ export function registerSearchTools(server, client, store, embedder) {
|
|
|
83
130
|
}
|
|
84
131
|
const startMs = Date.now();
|
|
85
132
|
const syncService = new SearchSyncService(client, store, embedder);
|
|
86
|
-
const { indexed, skipped } = await syncService.sync(project_id);
|
|
133
|
+
const { indexed, skipped, total } = await syncService.sync(project_id);
|
|
87
134
|
const duration_ms = Date.now() - startMs;
|
|
88
135
|
return {
|
|
89
136
|
content: [
|
|
90
137
|
{
|
|
91
138
|
type: 'text',
|
|
92
|
-
text: JSON.stringify({ indexed, skipped, duration_ms }, null, 2),
|
|
139
|
+
text: JSON.stringify({ indexed, skipped, total, duration_ms }, null, 2),
|
|
93
140
|
},
|
|
94
141
|
],
|
|
95
142
|
};
|
|
96
143
|
}
|
|
97
144
|
catch (error) {
|
|
98
145
|
const msg = error instanceof Error ? error.message : String(error);
|
|
99
|
-
return { content: [{ type: 'text', text:
|
|
146
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
100
147
|
}
|
|
101
148
|
});
|
|
102
149
|
}
|
package/dist/tools/config.js
CHANGED
|
@@ -6,7 +6,21 @@ function errorText(msg) {
|
|
|
6
6
|
const hint = vh?.getUpdateHint();
|
|
7
7
|
return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
// Parse a MantisBT enum string ("10:feature,20:trivial,...") into {id, name}[]
|
|
10
|
+
function parseEnumString(raw) {
|
|
11
|
+
return raw
|
|
12
|
+
.split(',')
|
|
13
|
+
.map((entry) => {
|
|
14
|
+
const colonIdx = entry.indexOf(':');
|
|
15
|
+
if (colonIdx === -1)
|
|
16
|
+
return null;
|
|
17
|
+
const id = parseInt(entry.slice(0, colonIdx), 10);
|
|
18
|
+
const name = entry.slice(colonIdx + 1).trim();
|
|
19
|
+
return isNaN(id) ? null : { id, name };
|
|
20
|
+
})
|
|
21
|
+
.filter((e) => e !== null);
|
|
22
|
+
}
|
|
23
|
+
export function registerConfigTools(server, client, cache) {
|
|
10
24
|
// ---------------------------------------------------------------------------
|
|
11
25
|
// get_config
|
|
12
26
|
// ---------------------------------------------------------------------------
|
|
@@ -47,6 +61,70 @@ Common option names:
|
|
|
47
61
|
}
|
|
48
62
|
});
|
|
49
63
|
// ---------------------------------------------------------------------------
|
|
64
|
+
// get_issue_enums
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
server.registerTool('get_issue_enums', {
|
|
67
|
+
title: 'Get Issue Enum Values',
|
|
68
|
+
description: `Return valid ID and name pairs for all issue enum fields.
|
|
69
|
+
|
|
70
|
+
Use this tool before creating or updating issues to look up the correct value
|
|
71
|
+
for severity, status, priority, resolution, or reproducibility.
|
|
72
|
+
|
|
73
|
+
Example response:
|
|
74
|
+
{
|
|
75
|
+
"severity": [{"id": 10, "name": "feature"}, {"id": 50, "name": "minor"}, ...],
|
|
76
|
+
"status": [{"id": 10, "name": "new"}, {"id": 20, "name": "feedback"}, ...],
|
|
77
|
+
"priority": [{"id": 10, "name": "none"}, {"id": 30, "name": "normal"}, ...],
|
|
78
|
+
"resolution": [{"id": 10, "name": "open"}, {"id": 20, "name": "fixed"}, ...],
|
|
79
|
+
"reproducibility": [{"id": 10, "name": "always"}, {"id": 70, "name": "have not tried"}, ...]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
The "name" field is the value to pass to create_issue or update_issue.`,
|
|
83
|
+
inputSchema: z.object({}),
|
|
84
|
+
annotations: {
|
|
85
|
+
readOnlyHint: true,
|
|
86
|
+
destructiveHint: false,
|
|
87
|
+
idempotentHint: true,
|
|
88
|
+
},
|
|
89
|
+
}, async () => {
|
|
90
|
+
try {
|
|
91
|
+
const enumOptions = [
|
|
92
|
+
'severity_enum_string',
|
|
93
|
+
'status_enum_string',
|
|
94
|
+
'priority_enum_string',
|
|
95
|
+
'resolution_enum_string',
|
|
96
|
+
'reproducibility_enum_string',
|
|
97
|
+
];
|
|
98
|
+
const params = {};
|
|
99
|
+
enumOptions.forEach((opt, i) => {
|
|
100
|
+
params[`option[${i}]`] = opt;
|
|
101
|
+
});
|
|
102
|
+
const result = await client.get('config', params);
|
|
103
|
+
const configs = result.configs ?? [];
|
|
104
|
+
const keyMap = {
|
|
105
|
+
severity_enum_string: 'severity',
|
|
106
|
+
status_enum_string: 'status',
|
|
107
|
+
priority_enum_string: 'priority',
|
|
108
|
+
resolution_enum_string: 'resolution',
|
|
109
|
+
reproducibility_enum_string: 'reproducibility',
|
|
110
|
+
};
|
|
111
|
+
const enums = {};
|
|
112
|
+
for (const { option, value } of configs) {
|
|
113
|
+
const key = keyMap[option];
|
|
114
|
+
if (key && typeof value === 'string') {
|
|
115
|
+
enums[key] = parseEnumString(value);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
content: [{ type: 'text', text: JSON.stringify(enums, 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
|
+
// ---------------------------------------------------------------------------
|
|
50
128
|
// list_languages
|
|
51
129
|
// ---------------------------------------------------------------------------
|
|
52
130
|
server.registerTool('list_languages', {
|
|
@@ -77,12 +155,12 @@ Common option names:
|
|
|
77
155
|
title: 'List Tags',
|
|
78
156
|
description: `List all tags defined in the MantisBT installation.
|
|
79
157
|
|
|
80
|
-
|
|
81
|
-
If
|
|
82
|
-
|
|
158
|
+
The MantisBT REST API exposes a GET /tags endpoint on some installations.
|
|
159
|
+
If that endpoint is not available, this tool falls back to the local metadata
|
|
160
|
+
cache populated by sync_metadata.`,
|
|
83
161
|
inputSchema: z.object({
|
|
84
|
-
page: z.number().int().positive().default(1).describe('Page number (default: 1)'),
|
|
85
|
-
page_size: z.number().int().min(1).max(200).default(50).describe('Tags per page (default: 50)'),
|
|
162
|
+
page: z.coerce.number().int().positive().default(1).describe('Page number (default: 1)'),
|
|
163
|
+
page_size: z.coerce.number().int().min(1).max(200).default(50).describe('Tags per page (default: 50)'),
|
|
86
164
|
}),
|
|
87
165
|
annotations: {
|
|
88
166
|
readOnlyHint: true,
|
|
@@ -98,10 +176,28 @@ In that case, use get_issue to read the tags of a specific issue instead.`,
|
|
|
98
176
|
}
|
|
99
177
|
catch (error) {
|
|
100
178
|
const msg = error instanceof Error ? error.message : String(error);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
179
|
+
if (msg.includes('404')) {
|
|
180
|
+
// GET /tags endpoint not available — fall back to metadata cache
|
|
181
|
+
const metadata = await cache.load();
|
|
182
|
+
if (metadata && Array.isArray(metadata.tags) && metadata.tags.length > 0) {
|
|
183
|
+
const start = (page - 1) * page_size;
|
|
184
|
+
const paginated = metadata.tags.slice(start, start + page_size);
|
|
185
|
+
return {
|
|
186
|
+
content: [{
|
|
187
|
+
type: 'text',
|
|
188
|
+
text: JSON.stringify({ tags: paginated, source: 'cache' }, null, 2),
|
|
189
|
+
}],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
content: [{
|
|
194
|
+
type: 'text',
|
|
195
|
+
text: `Error: ${msg}\n\nThe GET /tags endpoint is not available in this MantisBT installation. No cached tags found either — run sync_metadata to populate the cache if your installation provides this endpoint.`,
|
|
196
|
+
}],
|
|
197
|
+
isError: true,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
105
201
|
}
|
|
106
202
|
});
|
|
107
203
|
}
|
package/dist/tools/files.js
CHANGED
|
@@ -16,7 +16,7 @@ export function registerFileTools(server, client) {
|
|
|
16
16
|
title: 'List Issue File Attachments',
|
|
17
17
|
description: 'List all file attachments of a MantisBT issue.',
|
|
18
18
|
inputSchema: z.object({
|
|
19
|
-
issue_id: z.number().int().positive().describe('Numeric issue ID'),
|
|
19
|
+
issue_id: z.coerce.number().int().positive().describe('Numeric issue ID'),
|
|
20
20
|
}),
|
|
21
21
|
annotations: {
|
|
22
22
|
readOnlyHint: true,
|
|
@@ -49,7 +49,7 @@ Two input modes (exactly one must be provided):
|
|
|
49
49
|
|
|
50
50
|
The optional content_type parameter sets the MIME type (e.g. "image/png"). If omitted, "application/octet-stream" is used.`,
|
|
51
51
|
inputSchema: z.object({
|
|
52
|
-
issue_id: z.number().int().positive().describe('Numeric issue ID'),
|
|
52
|
+
issue_id: z.coerce.number().int().positive().describe('Numeric issue ID'),
|
|
53
53
|
file_path: z.string().min(1).optional().describe('Absolute path to the local file to upload (mutually exclusive with content)'),
|
|
54
54
|
content: z.string().min(1).optional().describe('Base64-encoded file content (mutually exclusive with file_path)'),
|
|
55
55
|
filename: z.string().min(1).optional().describe('File name for the attachment (required when using content; overrides the derived name when using file_path)'),
|
package/dist/tools/issues.js
CHANGED
|
@@ -15,7 +15,7 @@ export function registerIssueTools(server, client) {
|
|
|
15
15
|
title: 'Get Issue',
|
|
16
16
|
description: 'Retrieve a single MantisBT issue by its numeric ID. Returns all issue fields including notes, attachments, and relationships.',
|
|
17
17
|
inputSchema: z.object({
|
|
18
|
-
id: z.number().int().positive().describe('Numeric issue ID'),
|
|
18
|
+
id: z.coerce.number().int().positive().describe('Numeric issue ID'),
|
|
19
19
|
}),
|
|
20
20
|
annotations: {
|
|
21
21
|
readOnlyHint: true,
|
|
@@ -42,12 +42,12 @@ export function registerIssueTools(server, client) {
|
|
|
42
42
|
title: 'List Issues',
|
|
43
43
|
description: 'List MantisBT issues with optional filtering. Returns a paginated list of issues. Use the "select" parameter to limit returned fields and reduce response size significantly.',
|
|
44
44
|
inputSchema: z.object({
|
|
45
|
-
project_id: z.number().int().positive().optional().describe('Filter by project ID'),
|
|
46
|
-
page: z.number().int().positive().default(1).describe('Page number (default: 1)'),
|
|
47
|
-
page_size: z.number().int().min(1).max(50).default(50).describe('Issues per page (default: 50, max: 50)'),
|
|
48
|
-
assigned_to: z.number().int().positive().optional().describe('Filter by handler/assignee user ID'),
|
|
49
|
-
reporter_id: z.number().int().positive().optional().describe('Filter by reporter user ID'),
|
|
50
|
-
filter_id: z.number().int().positive().optional().describe('Use a saved MantisBT filter ID'),
|
|
45
|
+
project_id: z.coerce.number().int().positive().optional().describe('Filter by project ID'),
|
|
46
|
+
page: z.coerce.number().int().positive().default(1).describe('Page number (default: 1)'),
|
|
47
|
+
page_size: z.coerce.number().int().min(1).max(50).default(50).describe('Issues per page (default: 50, max: 50)'),
|
|
48
|
+
assigned_to: z.coerce.number().int().positive().optional().describe('Filter by handler/assignee user ID'),
|
|
49
|
+
reporter_id: z.coerce.number().int().positive().optional().describe('Filter by reporter user ID'),
|
|
50
|
+
filter_id: z.coerce.number().int().positive().optional().describe('Use a saved MantisBT filter ID'),
|
|
51
51
|
sort: z.string().optional().describe('Sort field (e.g. "last_updated", "id")'),
|
|
52
52
|
direction: z.enum(['ASC', 'DESC']).optional().describe('Sort direction'),
|
|
53
53
|
select: z.string().optional().describe('Comma-separated list of fields to include in the response (server-side projection). Significantly reduces response size. Example: "id,summary,status,priority,handler,updated_at"'),
|
|
@@ -100,11 +100,11 @@ export function registerIssueTools(server, client) {
|
|
|
100
100
|
inputSchema: z.object({
|
|
101
101
|
summary: z.string().min(1).describe('Issue summary/title'),
|
|
102
102
|
description: z.string().default('').describe('Detailed issue description'),
|
|
103
|
-
project_id: z.number().int().positive().describe('Project ID the issue belongs to'),
|
|
103
|
+
project_id: z.coerce.number().int().positive().describe('Project ID the issue belongs to'),
|
|
104
104
|
category: z.string().min(1).describe('Category name (use get_project_categories to list available categories)'),
|
|
105
105
|
priority: z.string().optional().describe('Priority name (e.g. "normal", "high", "urgent", "immediate", "low", "none")'),
|
|
106
|
-
severity: z.string().
|
|
107
|
-
handler_id: z.number().int().positive().optional().describe('User ID of the person to assign the issue to'),
|
|
106
|
+
severity: z.string().default('minor').describe('Severity name (e.g. "minor", "major", "crash", "block", "feature", "trivial", "text") — default: "minor"'),
|
|
107
|
+
handler_id: z.coerce.number().int().positive().optional().describe('User ID of the person to assign the issue to'),
|
|
108
108
|
}),
|
|
109
109
|
annotations: {
|
|
110
110
|
readOnlyHint: false,
|
|
@@ -121,8 +121,7 @@ export function registerIssueTools(server, client) {
|
|
|
121
121
|
};
|
|
122
122
|
if (priority)
|
|
123
123
|
body.priority = { name: priority };
|
|
124
|
-
|
|
125
|
-
body.severity = { name: severity };
|
|
124
|
+
body.severity = { name: severity };
|
|
126
125
|
if (handler_id)
|
|
127
126
|
body.handler = { id: handler_id };
|
|
128
127
|
const result = await client.post('issues', body);
|
|
@@ -156,7 +155,7 @@ The "fields" object accepts any combination of:
|
|
|
156
155
|
|
|
157
156
|
Important: when resolving an issue, always set BOTH status and resolution to avoid leaving resolution as "open".`,
|
|
158
157
|
inputSchema: z.object({
|
|
159
|
-
id: z.number().int().positive().describe('Numeric issue ID to update'),
|
|
158
|
+
id: z.coerce.number().int().positive().describe('Numeric issue ID to update'),
|
|
160
159
|
fields: z.record(z.unknown()).describe('Object containing the fields to update (partial update — only provided fields are changed)'),
|
|
161
160
|
}),
|
|
162
161
|
annotations: {
|
|
@@ -183,7 +182,7 @@ Important: when resolving an issue, always set BOTH status and resolution to avo
|
|
|
183
182
|
title: 'Delete Issue',
|
|
184
183
|
description: 'Permanently delete a MantisBT issue. This action is irreversible.',
|
|
185
184
|
inputSchema: z.object({
|
|
186
|
-
id: z.number().int().positive().describe('Numeric issue ID to delete'),
|
|
185
|
+
id: z.coerce.number().int().positive().describe('Numeric issue ID to delete'),
|
|
187
186
|
}),
|
|
188
187
|
annotations: {
|
|
189
188
|
readOnlyHint: false,
|