@dpesch/mantisbt-mcp-server 1.16.0 → 1.18.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 +30 -0
- package/README.de.md +8 -6
- package/README.md +8 -6
- package/dist/build-info.js +17 -0
- package/dist/build-info.json +5 -0
- package/dist/deprecation.js +4 -1
- package/dist/index.js +6 -8
- package/dist/search/highlight.js +1 -1
- package/dist/search/lexical.js +72 -0
- package/dist/search/store.js +10 -2
- package/dist/search/tools.js +2 -0
- package/dist/tools/index.js +1 -1
- package/dist/tools/issues.js +62 -16
- package/dist/tools/version.js +6 -4
- package/docs/cookbook.de.md +77 -3
- package/docs/cookbook.md +77 -3
- package/docs/examples.de.md +5 -1
- package/docs/examples.md +5 -1
- package/package.json +4 -3
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,36 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## [1.18.0] – 2026-11-09
|
|
15
|
+
|
|
16
|
+
### Deprecated
|
|
17
|
+
|
|
18
|
+
- The default of `response` in `update_issue` and `create_issue` changes from `"full"` to `"short"` in 2.0.0. Callers that rely on the whole issue in the response should pass `response: "full"` explicitly.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `update_issue` and `create_issue`: optional `response` parameter. `"short"` returns only what confirms the change instead of the whole issue with description, notes and history (#8598) — for `update_issue` `id`, `summary`, `status`, `resolution`, `updated_at`, `view_url` and the changed fields (custom fields and edited notes narrowed to the changed entries, an appended note to `id` and `view_url`); for `create_issue` `id`, `summary`, `project`, `category`, `status`, `handler`, `created_at` and `view_url`. On a fresh demo issue that is 529 instead of 4,046 characters; on an issue with many notes the gap is far larger. The default stays `"full"`.
|
|
23
|
+
- `get_version`: `mcp_server` now also contains `commit` (short hash of the build, `-dirty` with uncommitted changes) and `built_at` (build time), so an outdated build shows even when its version number is right (#8547).
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- The server reported the version from `package.json` at runtime, not the one its code was built from — in the MCP handshake, `get_version`, `get_mcp_version`, the startup log and the HTTP `/health` endpoint. Between a version bump and `npm run build`, a local install therefore claimed the new version while running the old code (#8547). The build now writes `dist/build-info.json`, and the server reads its version from there. Only the release build in CI reports a plain version; every other build — a local `npm run build`, `npm run dev`, code run from `src/` — is marked `-dev`, e.g. `1.18.0-dev`. `npm publish` refuses any build that is not the release build of the package version (`prepublishOnly`). The npm package was not affected: it is always built right before publishing.
|
|
28
|
+
- `list_issues`: the client-side filters `status`, `assigned_to`, `reporter_id`, `updated_after`/`updated_before` and `created_after`/`created_before` returned an empty list when `select` did not contain the field they read (`status`, `handler`, `reporter`, `updated_at`, `created_at`). The `select` projection removed the field before the filter ran, so every issue failed it — silently, without an error (#8597). The fields a filter needs are now fetched as well and removed again after filtering, so the response still holds exactly the selected fields. `group_by` was not affected.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## [1.17.0] – 2026-11-03
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- Search benchmark: an exact-first measure for identifier queries (do all issues that contain the identifier lead the top 10?), a new `compound` query type for identifiers such as `CVE-2026-1234`, and fixed cases from `BENCHMARK_EXACT` that print their ranking.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- Semantic search: `search_issues` lists every issue that contains an identifier from the query as a whole — an order number, a CVE, a version, a code name such as `MANTIS_SEARCH_DIR` or `getEmbeddingSignature` — before all issues that do not. The combined ranking found such issues, but put issues in between that were only close by meaning, often short ones (#8545). A query for `CVE-2026-1234` no longer counts an issue with another CVE of 2026 as a near match, and `1.15` does not match `1.15.2`. Identifiers are recognised by their form (underscore, inner capital, letters with digits, five or more digits, or words joined by `-` `.` `:` `/` with a digit), so plain words and short numbers such as "Android 15" keep the combined ranking; an identifier found in more than 20 issues does as well. Measured on 7,742 issues: for a rare identifier, all issues containing it now lead the top 10 in 97 % of the queries instead of 72 %; for a compound identifier such as `7.0.0-1014-azure` in 98 % instead of 11 %, and such a query finds its issue among the top 10 in 98 % instead of 64 %. Titles, sentences and paraphrases stay level or gain slightly. No index rebuild is needed.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
14
44
|
## [1.16.0] – 2026-11-02
|
|
15
45
|
|
|
16
46
|
### Added
|
package/README.de.md
CHANGED
|
@@ -156,8 +156,8 @@ Veraltete Tools gehören immer zur gleichen Gruppe wie ihr Ersatz – eine Grupp
|
|
|
156
156
|
| `get_issue` | Ein Issue per `id` oder mehrere per `ids` (1–50) in einem Aufruf abrufen – genau eines von beiden angeben; mit `ids` lautet die Antwort `{ issues, requested, found, failed }`, nicht zugängliche IDs liefern `null` an ihrer Position; optionales `select` zur Feldauswahl |
|
|
157
157
|
| `get_issues` | **Veraltet:** stattdessen `get_issue` mit `ids` verwenden – wird in 2.0.0 entfernt |
|
|
158
158
|
| `list_issues` | Issues nach Projekt, Status, Autor u.v.m. filtern; optionales `select` für Feldprojektion und `status` für clientseitige Statusfilterung – kanonische englische Statusnamen (z.B. `"new"`, `"resolved"`) werden per ID abgeglichen und funktionieren damit sprachunabhängig auf lokalisierten Installationen; `group_by` (`"status"`, `"handler"`, `"priority"`, `"category"`) liefert statt einer Issue-Liste Zählwerte pro Gruppe – alle Filter greifen weiterhin, `page`, `page_size` und `select` werden ignoriert |
|
|
159
|
-
| `create_issue` | Neues Issue anlegen; `severity` und `priority` müssen kanonische englische Namen sein (z.B. `minor`, `major`, `normal`, `high`) – `get_issue_schema` aufrufen, um alle gültigen Werte und deren lokalisierte Bezeichnungen zu sehen; optionaler `handler`-Parameter akzeptiert einen Benutzernamen als Alternative zu `handler_id` (wird gegen die Projektmitglieder aufgelöst); optionales `custom_fields` zum Setzen benutzerdefinierter Felder |
|
|
160
|
-
| `update_issue` | Bestehendes Issue bearbeiten; Enum-Felder (`status`, `priority`, `severity`, `resolution`, `reproducibility`) akzeptieren kanonische englische Namen, lokalisierte Namen oder numerische IDs – der Server löst Namen automatisch zu IDs auf; unterstützt `custom_fields` sowie einen optionalen `note`-Parameter, der im selben Aufruf eine Notiz anhängt; `fields` darf entfallen, wenn nur `add_monitors`, `remove_monitors` oder `note` angegeben werden; mit `add_monitors` / `remove_monitors` (Arrays von Login-Namen) können Beobachter hinzugefügt oder entfernt werden – die Antwort enthält einen `monitors`-Schlüssel mit der aktuellen und der geänderten Beobachterliste; mit `fields.notes` lassen sich bestehende Notizen per ID bearbeiten (Notiz-ID aus `get_issue`) |
|
|
159
|
+
| `create_issue` | Neues Issue anlegen; `severity` und `priority` müssen kanonische englische Namen sein (z.B. `minor`, `major`, `normal`, `high`) – `get_issue_schema` aufrufen, um alle gültigen Werte und deren lokalisierte Bezeichnungen zu sehen; optionaler `handler`-Parameter akzeptiert einen Benutzernamen als Alternative zu `handler_id` (wird gegen die Projektmitglieder aufgelöst); optionales `custom_fields` zum Setzen benutzerdefinierter Felder; `response: "short"` liefert statt des ganzen Issues nur dessen Kernfelder (Standard `"full"`, ab 2.0.0 `"short"`) |
|
|
160
|
+
| `update_issue` | Bestehendes Issue bearbeiten; Enum-Felder (`status`, `priority`, `severity`, `resolution`, `reproducibility`) akzeptieren kanonische englische Namen, lokalisierte Namen oder numerische IDs – der Server löst Namen automatisch zu IDs auf; unterstützt `custom_fields` sowie einen optionalen `note`-Parameter, der im selben Aufruf eine Notiz anhängt; `fields` darf entfallen, wenn nur `add_monitors`, `remove_monitors` oder `note` angegeben werden; mit `add_monitors` / `remove_monitors` (Arrays von Login-Namen) können Beobachter hinzugefügt oder entfernt werden – die Antwort enthält einen `monitors`-Schlüssel mit der aktuellen und der geänderten Beobachterliste; mit `fields.notes` lassen sich bestehende Notizen per ID bearbeiten (Notiz-ID aus `get_issue`); `response: "short"` liefert statt des ganzen Issues mit Notizen und Historie nur die Kernfelder und die geänderten Felder (Standard `"full"`, ab 2.0.0 `"short"`) |
|
|
161
161
|
| `delete_issue` | Issue löschen |
|
|
162
162
|
|
|
163
163
|
### Notizen
|
|
@@ -220,13 +220,13 @@ Die semantische Suche versteht die *Bedeutung* einer Anfrage, nicht nur ihre Sti
|
|
|
220
220
|
- *„Performance-Probleme auf der Checkout-Seite"* – liefert verwandte Meldungen unabhängig von der verwendeten Terminologie
|
|
221
221
|
- *„doppelte Einträge in der Rechnungsliste"* – erkennt auch Issues, die als „zweifach angezeigt", „dupliziert" o.ä. beschrieben sind
|
|
222
222
|
|
|
223
|
-
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). Starten mehrere Client-Sitzungen gleichzeitig einen Server, synchronisiert nur einer davon den gemeinsamen Index. Das Modell wird bei der ersten Nutzung geladen und nach 10 Minuten ohne Einbetten wieder entladen, damit ruhende Sitzungen seinen Speicher (~0,9 GB pro Serverprozess) nicht dauerhaft belegen. Lange Beschreibungen werden in bis zu 8 Abschnitte geteilt, damit auch Details weiter unten gefunden werden; der Index enthält dadurch etwa 1,7 Vektoren pro Issue (rund 115 MB bei 7.700 Issues), die jeder Serverprozess mit aktivierter Suche im Speicher hält. Die Rangfolge berücksichtigt zusätzlich exakte Stichworttreffer, sodass eine Suche nach einem Bezeichner, Fehlercode oder einer Versionsnummer die Issues findet, die ihn enthalten.
|
|
223
|
+
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). Starten mehrere Client-Sitzungen gleichzeitig einen Server, synchronisiert nur einer davon den gemeinsamen Index. Das Modell wird bei der ersten Nutzung geladen und nach 10 Minuten ohne Einbetten wieder entladen, damit ruhende Sitzungen seinen Speicher (~0,9 GB pro Serverprozess) nicht dauerhaft belegen. Lange Beschreibungen werden in bis zu 8 Abschnitte geteilt, damit auch Details weiter unten gefunden werden; der Index enthält dadurch etwa 1,7 Vektoren pro Issue (rund 115 MB bei 7.700 Issues), die jeder Serverprozess mit aktivierter Suche im Speicher hält. Die Rangfolge berücksichtigt zusätzlich exakte Stichworttreffer, sodass eine Suche nach einem Bezeichner, Fehlercode oder einer Versionsnummer die Issues findet, die ihn enthalten – und alle Issues, die eine solche Kennung vollständig enthalten, vor allen anderen auflistet, sofern die Kennung nicht in mehr als 20 Issues vorkommt.
|
|
224
224
|
|
|
225
225
|
Aktivierung mit `MANTIS_SEARCH_ENABLED=true`.
|
|
226
226
|
|
|
227
227
|
| Tool | Beschreibung |
|
|
228
228
|
|---|---|
|
|
229
|
-
| `search_issues` | Natürlichsprachige Suche über alle indizierten Issues – kombiniert semantische Ähnlichkeit mit Stichwortsuche (BM25), damit auch exakte Begriffe wie Bezeichner oder Fehlercodes gefunden werden; liefert Top-N-Ergebnisse mit der Cosine-Similarity als `score`, sortiert nach der kombinierten Rangfolge; optionales `select` (kommagetrennte Feldnamen) reichert jedes Ergebnis mit den angeforderten Issue-Feldern an; optionales `highlight` (boolean, Standard `false`) fügt je Ergebnis ein `highlights`-Feld mit keyword-basierten Ausschnitten aus `summary` und `description` hinzu (Treffer werden in `**fett**` dargestellt) |
|
|
229
|
+
| `search_issues` | Natürlichsprachige Suche über alle indizierten Issues – kombiniert semantische Ähnlichkeit mit Stichwortsuche (BM25), damit auch exakte Begriffe wie Bezeichner oder Fehlercodes gefunden werden; Issues, die eine Kennung aus der Anfrage enthalten (Auftragsnummer, CVE, Version, Code-Bezeichner), stehen vorn; liefert Top-N-Ergebnisse mit der Cosine-Similarity als `score`, sortiert nach der kombinierten Rangfolge; optionales `select` (kommagetrennte Feldnamen) reichert jedes Ergebnis mit den angeforderten Issue-Feldern an; optionales `highlight` (boolean, Standard `false`) fügt je Ergebnis ein `highlights`-Feld mit keyword-basierten Ausschnitten aus `summary` und `description` hinzu (Treffer werden in `**fett**` dargestellt) |
|
|
230
230
|
| `rebuild_search_index` | Suchindex aufbauen oder aktualisieren; `full: true` löscht und baut ihn vollständig neu |
|
|
231
231
|
| `get_search_index_status` | **Veraltet:** stattdessen `get_metadata` (Feld `search_index`) verwenden – wird in 2.0.0 entfernt |
|
|
232
232
|
|
|
@@ -251,7 +251,7 @@ npm install sqlite-vec better-sqlite3
|
|
|
251
251
|
| Tool | Beschreibung |
|
|
252
252
|
|---|---|
|
|
253
253
|
| `get_metadata` | Gecachte Metadaten abrufen: `detail: "summary"` (Standard) liefert Projekt-/Tag-Zählwerte und Benutzer-/Versions-/Kategorie-Zählwerte je Projekt; `detail: "full"` liefert den vollständigen rohen Cache; `refresh: true` lädt alle Daten zuerst von MantisBT nach; mit aktivierter semantischer Suche enthält die Antwort zusätzlich einen `search_index`-Block |
|
|
254
|
-
| `get_version` | Version dieses MCP-Servers und der verbundenen MantisBT-Installation in einer Antwort; `check_latest` (Standard `true`) vergleicht MantisBT mit dem neuesten GitHub-Release; `mantis` enthält `error`, wenn MantisBT nicht erreichbar ist |
|
|
254
|
+
| `get_version` | Version dieses MCP-Servers und der verbundenen MantisBT-Installation in einer Antwort; der Server-Teil nennt zusätzlich Commit und Build-Zeitpunkt (Anhang `-dev`, wenn es kein veröffentlichter Release ist); `check_latest` (Standard `true`) vergleicht MantisBT mit dem neuesten GitHub-Release; `mantis` enthält `error`, wenn MantisBT nicht erreichbar ist |
|
|
255
255
|
| `get_issue_schema` | Gültige `select`-Felder (`fields`, `fields_source`) und Enum-Werte (`enums`: severity, status, priority, resolution, reproducibility) für diesen MantisBT-Server in einer Antwort; optionale `project_id` schränkt die Feldermittlung ein – vor `create_issue` / `update_issue` oder beim Aufbau eines `select`-Strings verwenden |
|
|
256
256
|
| `get_issue_fields` | **Veraltet:** stattdessen `get_issue_schema` verwenden – wird in 2.0.0 entfernt |
|
|
257
257
|
| `get_issue_enums` | **Veraltet:** stattdessen `get_issue_schema` verwenden; die Ressource `mantis://enums` bleibt – wird in 2.0.0 entfernt |
|
|
@@ -341,9 +341,11 @@ npm test # Tests ausführen (vitest)
|
|
|
341
341
|
npm run test:watch # Tests im Watch-Modus
|
|
342
342
|
npm run test:coverage # Coverage-Report
|
|
343
343
|
npm run benchmark # Token-Benchmark gegen eine Live-MantisBT-Instanz (siehe Token-Verbrauch oben)
|
|
344
|
-
npm run benchmark:search # Such-Trefferquote auf einem bestehenden Index (MANTIS_SEARCH_DIR), Vektor vs. hybrid
|
|
344
|
+
npm run benchmark:search # Such-Trefferquote und Exakt-Treffer-Reihenfolge auf einem bestehenden Index (MANTIS_SEARCH_DIR), Vektor vs. hybrid
|
|
345
345
|
```
|
|
346
346
|
|
|
347
|
+
`benchmark:search` liest optional handgeschriebene Umschreibungen (`BENCHMARK_PARAPHRASES`) und feste Fälle mit erwarteter Rangfolge (`BENCHMARK_EXACT`); die Dateiformate stehen am Anfang von `scripts/benchmark-search.ts`.
|
|
348
|
+
|
|
347
349
|
## Lizenz
|
|
348
350
|
|
|
349
351
|
MIT – siehe [LICENSE](LICENSE)
|
package/README.md
CHANGED
|
@@ -156,8 +156,8 @@ Deprecated tools always belong to the same group as their replacement — disabl
|
|
|
156
156
|
| `get_issue` | Retrieve an issue by `id`, or several by `ids` (1–50) in one call — pass exactly one; with `ids` returns `{ issues, requested, found, failed }`, missing or inaccessible IDs give `null` at their position; optional `select` for field projection |
|
|
157
157
|
| `get_issues` | **Deprecated:** use `get_issue` with `ids` instead — will be removed in 2.0.0 |
|
|
158
158
|
| `list_issues` | Filter issues by project, status, author, and more; optional `select` for field projection and `status` for client-side status filtering — canonical English status names (e.g. `"new"`, `"resolved"`) are matched by ID, making the filter language-independent on localized installations; `group_by` (`"status"`, `"handler"`, `"priority"`, `"category"`) returns counts per group instead of an issue list — all filters still apply, `page`, `page_size`, and `select` are ignored |
|
|
159
|
-
| `create_issue` | Create a new issue; `severity` and `priority` must be canonical English names (e.g. `minor`, `major`, `normal`, `high`) — call `get_issue_schema` to see all valid values and their localized labels; optional `handler` parameter accepts a username as alternative to `handler_id` (resolved against project members); optional `custom_fields` to set custom field values |
|
|
160
|
-
| `update_issue` | Update an existing issue; enum fields (`status`, `priority`, `severity`, `resolution`, `reproducibility`) accept canonical English names, localized names, or numeric IDs — the server resolves names to IDs automatically; supports `custom_fields` and an optional `note` parameter that appends a note in the same call; `fields` may be omitted when only `add_monitors`, `remove_monitors`, or `note` are given; use `add_monitors` / `remove_monitors` (arrays of login names) to add or remove watchers — the response includes a `monitors` key with the current and changed monitor lists; use `fields.notes` to edit existing notes by ID (note ID from `get_issue`) |
|
|
159
|
+
| `create_issue` | Create a new issue; `severity` and `priority` must be canonical English names (e.g. `minor`, `major`, `normal`, `high`) — call `get_issue_schema` to see all valid values and their localized labels; optional `handler` parameter accepts a username as alternative to `handler_id` (resolved against project members); optional `custom_fields` to set custom field values; `response: "short"` returns only the core fields of the new issue instead of the whole issue (default `"full"`, changes to `"short"` in 2.0.0) |
|
|
160
|
+
| `update_issue` | Update an existing issue; enum fields (`status`, `priority`, `severity`, `resolution`, `reproducibility`) accept canonical English names, localized names, or numeric IDs — the server resolves names to IDs automatically; supports `custom_fields` and an optional `note` parameter that appends a note in the same call; `fields` may be omitted when only `add_monitors`, `remove_monitors`, or `note` are given; use `add_monitors` / `remove_monitors` (arrays of login names) to add or remove watchers — the response includes a `monitors` key with the current and changed monitor lists; use `fields.notes` to edit existing notes by ID (note ID from `get_issue`); `response: "short"` returns only the core fields and the changed ones instead of the whole issue with notes and history (default `"full"`, changes to `"short"` in 2.0.0) |
|
|
161
161
|
| `delete_issue` | Delete an issue |
|
|
162
162
|
|
|
163
163
|
### Notes
|
|
@@ -220,13 +220,13 @@ Semantic search understands the *meaning* behind a query, not just its keywords.
|
|
|
220
220
|
- *"performance problems on the checkout page"* — surfaces related reports regardless of the exact terminology used
|
|
221
221
|
- *"duplicate entries in the invoice list"* — catches issues described as "shown twice", "double records", etc.
|
|
222
222
|
|
|
223
|
-
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). When several client sessions start a server at the same time, only one of them syncs the shared index. The model is loaded on first use and unloaded again after 10 minutes without embedding, so idle sessions do not hold on to its memory (~0.9 GB per server process). Long descriptions are split into up to 8 chunks so that details further down are found as well; the index then holds about 1.7 vectors per issue (around 115 MB for 7,700 issues), which every server process with search enabled keeps in memory. The ranking also takes exact keyword matches into account, so a query for an identifier, error code or version number finds the issues that contain it.
|
|
223
|
+
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). When several client sessions start a server at the same time, only one of them syncs the shared index. The model is loaded on first use and unloaded again after 10 minutes without embedding, so idle sessions do not hold on to its memory (~0.9 GB per server process). Long descriptions are split into up to 8 chunks so that details further down are found as well; the index then holds about 1.7 vectors per issue (around 115 MB for 7,700 issues), which every server process with search enabled keeps in memory. The ranking also takes exact keyword matches into account, so a query for an identifier, error code or version number finds the issues that contain it — and lists all issues containing such an identifier as a whole before any others, unless it occurs in more than 20 issues.
|
|
224
224
|
|
|
225
225
|
Activate with `MANTIS_SEARCH_ENABLED=true`.
|
|
226
226
|
|
|
227
227
|
| Tool | Description |
|
|
228
228
|
|---|---|
|
|
229
|
-
| `search_issues` | Natural language search over all indexed issues — combines semantic similarity with keyword matching (BM25), so exact terms such as identifiers or error codes are found as well; returns top-N results with the cosine similarity as `score`, ordered by the combined ranking; optional `select` (comma-separated field names) enriches each result with the requested issue fields; optional `highlight` (boolean, default `false`) adds a `highlights` field per result with keyword-matched excerpts from `summary` and `description` (matched terms shown in `**bold**`) |
|
|
229
|
+
| `search_issues` | Natural language search over all indexed issues — combines semantic similarity with keyword matching (BM25), so exact terms such as identifiers or error codes are found as well; issues containing an identifier from the query (order number, CVE, version, code name) come first; returns top-N results with the cosine similarity as `score`, ordered by the combined ranking; optional `select` (comma-separated field names) enriches each result with the requested issue fields; optional `highlight` (boolean, default `false`) adds a `highlights` field per result with keyword-matched excerpts from `summary` and `description` (matched terms shown in `**bold**`) |
|
|
230
230
|
| `rebuild_search_index` | Build or update the search index; `full: true` clears and rebuilds from scratch |
|
|
231
231
|
| `get_search_index_status` | **Deprecated:** use `get_metadata` (field `search_index`) instead — will be removed in 2.0.0 |
|
|
232
232
|
|
|
@@ -251,7 +251,7 @@ npm install sqlite-vec better-sqlite3
|
|
|
251
251
|
| Tool | Description |
|
|
252
252
|
|---|---|
|
|
253
253
|
| `get_metadata` | Retrieve cached metadata: `detail: "summary"` (default) returns project/tag counts and per-project user/version/category counts; `detail: "full"` returns the complete raw cache; `refresh: true` re-fetches from MantisBT first; with semantic search enabled the response also contains a `search_index` block |
|
|
254
|
-
| `get_version` | Version of this MCP server and of the connected MantisBT installation in one response; `check_latest` (default `true`) compares MantisBT with the latest GitHub release; `mantis` holds `error` when MantisBT cannot be reached |
|
|
254
|
+
| `get_version` | Version of this MCP server and of the connected MantisBT installation in one response; the server part also names the commit and build time (`-dev` suffix unless it is a published release); `check_latest` (default `true`) compares MantisBT with the latest GitHub release; `mantis` holds `error` when MantisBT cannot be reached |
|
|
255
255
|
| `get_issue_schema` | Valid `select` fields (`fields`, `fields_source`) and enum values (`enums`: severity, status, priority, resolution, reproducibility) for this MantisBT server in one response; optional `project_id` scopes the field discovery — use before `create_issue` / `update_issue` or when building a `select` string |
|
|
256
256
|
| `get_issue_fields` | **Deprecated:** use `get_issue_schema` instead — will be removed in 2.0.0 |
|
|
257
257
|
| `get_issue_enums` | **Deprecated:** use `get_issue_schema` instead; the resource `mantis://enums` stays — will be removed in 2.0.0 |
|
|
@@ -341,9 +341,11 @@ npm test # Run tests (vitest)
|
|
|
341
341
|
npm run test:watch # Run tests in watch mode
|
|
342
342
|
npm run test:coverage # Coverage report
|
|
343
343
|
npm run benchmark # Token benchmark against a live MantisBT (see Token usage above)
|
|
344
|
-
npm run benchmark:search # Search recall on an existing index (MANTIS_SEARCH_DIR), vectors vs. hybrid
|
|
344
|
+
npm run benchmark:search # Search recall and exact-first order on an existing index (MANTIS_SEARCH_DIR), vectors vs. hybrid
|
|
345
345
|
```
|
|
346
346
|
|
|
347
|
+
`benchmark:search` optionally reads hand-written paraphrases (`BENCHMARK_PARAPHRASES`) and fixed cases with an expected ranking (`BENCHMARK_EXACT`); the file formats are described at the top of `scripts/benchmark-search.ts`.
|
|
348
|
+
|
|
347
349
|
## License
|
|
348
350
|
|
|
349
351
|
MIT – see [LICENSE](LICENSE)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Reads build-info.json, which scripts/write-build-info.mjs writes next to the
|
|
5
|
+
* compiled code. The version thus belongs to the code that runs, even when
|
|
6
|
+
* package.json was bumped after the build (#8547). Code run straight from
|
|
7
|
+
* src/ (tests, tsx) has no build: it reports package.json's version as -dev.
|
|
8
|
+
*/
|
|
9
|
+
export function loadBuildInfo(codeDir) {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(readFileSync(join(codeDir, 'build-info.json'), 'utf-8'));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
const pkg = JSON.parse(readFileSync(join(codeDir, '..', 'package.json'), 'utf-8'));
|
|
15
|
+
return { version: `${pkg.version}-dev` };
|
|
16
|
+
}
|
|
17
|
+
}
|
package/dist/deprecation.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Next major version: deprecated tools are removed and announced default
|
|
3
|
+
* changes take effect (see versioning rules in CLAUDE.md).
|
|
4
|
+
*/
|
|
2
5
|
export const REMOVAL_VERSION = '2.0.0';
|
|
3
6
|
/**
|
|
4
7
|
* First line of the description of a deprecated tool. Keeping the wording in
|
package/dist/index.js
CHANGED
|
@@ -3,9 +3,9 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
5
5
|
import { createServer } from 'node:http';
|
|
6
|
-
import { readFileSync } from 'node:fs';
|
|
7
6
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import {
|
|
7
|
+
import { dirname } from 'node:path';
|
|
8
|
+
import { loadBuildInfo } from './build-info.js';
|
|
9
9
|
import { getConfig, getStartupConfig, assertHttpAuthConfigured } from './config.js';
|
|
10
10
|
import { MantisClient } from './client.js';
|
|
11
11
|
import { VersionHintService, setGlobalVersionHint } from './version-hint.js';
|
|
@@ -15,12 +15,10 @@ import { filterToolRegistration } from './toolsets.js';
|
|
|
15
15
|
import { registerPrompts } from './prompts/index.js';
|
|
16
16
|
import { registerResources } from './resources/index.js';
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
|
-
//
|
|
18
|
+
// Version of the running build (dist/build-info.json, see build-info.ts)
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
23
|
-
const version = packageJson.version;
|
|
20
|
+
const build = loadBuildInfo(dirname(fileURLToPath(import.meta.url)));
|
|
21
|
+
const version = build.version;
|
|
24
22
|
// ---------------------------------------------------------------------------
|
|
25
23
|
// Bootstrap
|
|
26
24
|
// ---------------------------------------------------------------------------
|
|
@@ -57,7 +55,7 @@ async function createMcpServer(transport) {
|
|
|
57
55
|
client,
|
|
58
56
|
cache,
|
|
59
57
|
versionHint,
|
|
60
|
-
|
|
58
|
+
build,
|
|
61
59
|
uploadDir: startupConfig.uploadDir,
|
|
62
60
|
transport,
|
|
63
61
|
searchIndexStatus,
|
package/dist/search/highlight.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Note: highlights are keyword-based (lexical), not semantic.
|
|
5
5
|
* A result may have no highlighted terms even if it is semantically relevant.
|
|
6
6
|
*/
|
|
7
|
-
function escapeRegex(term) {
|
|
7
|
+
export function escapeRegex(term) {
|
|
8
8
|
return term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
9
9
|
}
|
|
10
10
|
function combinedPattern(terms, flags) {
|
package/dist/search/lexical.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// keyword ranking. Built in memory from the summaries and descriptions the
|
|
7
7
|
// vector index already stores — no extra API calls.
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
|
+
import { escapeRegex } from './highlight.js';
|
|
9
10
|
// Standard BM25 parameters.
|
|
10
11
|
const K1 = 1.2;
|
|
11
12
|
const B = 0.75;
|
|
@@ -15,7 +16,48 @@ const SUMMARY_WEIGHT = 2;
|
|
|
15
16
|
export function tokenize(text) {
|
|
16
17
|
return (text.toLowerCase().match(/[\p{L}\p{N}_]+/gu) ?? []).filter(word => word.length >= 2);
|
|
17
18
|
}
|
|
19
|
+
// An identifier (#8545) found in more issues than this is no longer specific
|
|
20
|
+
// enough to put all of them first; the fused ranking decides instead. Keeps
|
|
21
|
+
// common tokens such as UTF-8 from taking over a search. The rare identifiers
|
|
22
|
+
// of the search benchmark occur in at most 8 issues, well below the cap.
|
|
23
|
+
const MAX_EXACT_MATCHES = 20;
|
|
24
|
+
// Words, or words joined by - . : / (CVE-2026-1234, 7.0.0-1014-azure).
|
|
25
|
+
const COMPOUND = /[\p{L}\p{N}_]+(?:[-.:/][\p{L}\p{N}_]+)*/gu;
|
|
26
|
+
function looksLikeIdentifier(word) {
|
|
27
|
+
return word.includes('_') // MANTIS_SEARCH_DIR, _resizeTimer
|
|
28
|
+
|| /\p{Ll}\p{Lu}/u.test(word) // ScrollViewHelper
|
|
29
|
+
|| (word.length >= 4 && /\p{L}/u.test(word) && /\p{N}/u.test(word)) // host01
|
|
30
|
+
|| /^\p{N}{5,}$/u.test(word); // 202699999
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Identifiers in a search query: order numbers, CVEs, versions, code names.
|
|
34
|
+
* Plain words, short numbers ("Android 15") and hyphenated words without
|
|
35
|
+
* digits ("Play-Store-Hinweis") are none.
|
|
36
|
+
*/
|
|
37
|
+
export function identifiers(query) {
|
|
38
|
+
const found = new Map();
|
|
39
|
+
for (const match of query.match(COMPOUND) ?? []) {
|
|
40
|
+
// A compound with a digit is one identifier; otherwise each word counts alone.
|
|
41
|
+
const candidates = /[-.:/]/.test(match) && /\p{N}/u.test(match)
|
|
42
|
+
? [match]
|
|
43
|
+
: match.split(/[-.:/]/).filter(looksLikeIdentifier);
|
|
44
|
+
for (const candidate of candidates) {
|
|
45
|
+
if (!found.has(candidate.toLowerCase()))
|
|
46
|
+
found.set(candidate.toLowerCase(), candidate);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return [...found.values()];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Matches the identifier as a whole: not inside a longer word, and not as the
|
|
53
|
+
* start or end of a longer number or version (1.15 is not in 1.15.2), while a
|
|
54
|
+
* word may continue with a hyphen (ScrollViewHelper in ScrollViewHelper-Bridge).
|
|
55
|
+
*/
|
|
56
|
+
export function wholeIdentifier(identifier) {
|
|
57
|
+
return new RegExp(`(?<![\\p{L}\\p{N}_])(?<!\\p{N}[-.:/])${escapeRegex(identifier)}(?![\\p{L}\\p{N}_])(?![-.:/]\\p{N})`, 'iu');
|
|
58
|
+
}
|
|
18
59
|
export class LexicalIndex {
|
|
60
|
+
documents;
|
|
19
61
|
// Postings in compressed sparse rows: the documents of term t are
|
|
20
62
|
// docs[offsets[t] .. offsets[t + 1]], with their term frequencies in tfs.
|
|
21
63
|
// Typed arrays keep 340,000 postings (7,700 issues) at about 7 MB instead of
|
|
@@ -28,6 +70,7 @@ export class LexicalIndex {
|
|
|
28
70
|
lengths;
|
|
29
71
|
avgLength;
|
|
30
72
|
constructor(documents) {
|
|
73
|
+
this.documents = documents;
|
|
31
74
|
this.ids = Uint32Array.from(documents, doc => doc.id);
|
|
32
75
|
this.lengths = new Uint32Array(documents.length);
|
|
33
76
|
const counts = documents.map((doc, index) => {
|
|
@@ -85,4 +128,33 @@ export class LexicalIndex {
|
|
|
85
128
|
}
|
|
86
129
|
return new Map([...byDoc].map(([doc, score]) => [this.ids[doc], score]));
|
|
87
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Per document id, how many identifiers of the query it contains as a whole
|
|
133
|
+
* (#8545); documents without one are absent. Candidates come from the
|
|
134
|
+
* postings of the identifier's rarest word, the text check decides.
|
|
135
|
+
*/
|
|
136
|
+
exactMatches(query) {
|
|
137
|
+
const counts = new Map();
|
|
138
|
+
const frequency = (term) => this.offsets[term + 1] - this.offsets[term];
|
|
139
|
+
for (const identifier of identifiers(query)) {
|
|
140
|
+
const terms = tokenize(identifier).map(word => this.termIds.get(word));
|
|
141
|
+
if (terms.length === 0 || terms.some(term => term === undefined))
|
|
142
|
+
continue;
|
|
143
|
+
const rarest = terms.reduce((a, b) => (frequency(a) <= frequency(b) ? a : b));
|
|
144
|
+
const pattern = wholeIdentifier(identifier);
|
|
145
|
+
const matches = [];
|
|
146
|
+
for (let at = this.offsets[rarest]; at < this.offsets[rarest + 1]; at++) {
|
|
147
|
+
const doc = this.documents[this.docs[at]];
|
|
148
|
+
if (pattern.test(doc.summary) || pattern.test(doc.description ?? ''))
|
|
149
|
+
matches.push(doc.id);
|
|
150
|
+
if (matches.length > MAX_EXACT_MATCHES)
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
if (matches.length > MAX_EXACT_MATCHES)
|
|
154
|
+
continue;
|
|
155
|
+
for (const id of matches)
|
|
156
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
157
|
+
}
|
|
158
|
+
return counts;
|
|
159
|
+
}
|
|
88
160
|
}
|
package/dist/search/store.js
CHANGED
|
@@ -107,11 +107,19 @@ export class VectraStore {
|
|
|
107
107
|
// Reciprocal Rank Fusion of the vector ranking and the keyword ranking.
|
|
108
108
|
this.lexical ??= new LexicalIndex([...this.items.values()].map(item => ({ id: item.id, ...item.metadata })));
|
|
109
109
|
const keywordRank = new Map([...this.lexical.scores(queryText)].sort((a, b) => b[1] - a[1]).map(([id], rank) => [id, rank]));
|
|
110
|
+
// Issues containing an identifier of the query as a whole come first
|
|
111
|
+
// (#8545): a search for an order number or a CVE lists all its issues
|
|
112
|
+
// before any that merely resemble the query.
|
|
113
|
+
const exact = this.lexical.exactMatches(queryText);
|
|
110
114
|
const fused = results.map((result, vectorRank) => {
|
|
111
115
|
const rank = keywordRank.get(result.id);
|
|
112
|
-
return {
|
|
116
|
+
return {
|
|
117
|
+
result,
|
|
118
|
+
exact: exact.get(result.id) ?? 0,
|
|
119
|
+
fused: 1 / (RRF_K + vectorRank) + (rank === undefined ? 0 : 1 / (RRF_K + rank)),
|
|
120
|
+
};
|
|
113
121
|
});
|
|
114
|
-
fused.sort((a, b) => b.fused - a.fused);
|
|
122
|
+
fused.sort((a, b) => b.exact - a.exact || b.fused - a.fused);
|
|
115
123
|
return fused.slice(0, topN).map(f => f.result);
|
|
116
124
|
}
|
|
117
125
|
async getItem(id) {
|
package/dist/search/tools.js
CHANGED
|
@@ -49,6 +49,8 @@ export function registerSearchTools(server, client, store, embedder, lockDir) {
|
|
|
49
49
|
'by combining semantic similarity with keyword matching, so exact terms such as identifiers, ' +
|
|
50
50
|
'error codes or version numbers are found as well. "score" is the semantic similarity; results ' +
|
|
51
51
|
'are ordered by the combined ranking, so a keyword match can precede a higher score. ' +
|
|
52
|
+
'Issues that contain an identifier from the query as a whole (order number, CVE, version, ' +
|
|
53
|
+
'code name such as MANTIS_SEARCH_DIR) are listed before all others, unless the identifier is common. ' +
|
|
52
54
|
'The search index must be populated first via rebuild_search_index.',
|
|
53
55
|
inputSchema: z.object({
|
|
54
56
|
query: z.string().describe('Natural language search query'),
|
package/dist/tools/index.js
CHANGED
|
@@ -30,5 +30,5 @@ export function registerAllTools(server, deps) {
|
|
|
30
30
|
registerConfigTools(server, client, cache);
|
|
31
31
|
registerMetadataTools(server, client, cache, deps.searchIndexStatus);
|
|
32
32
|
registerTagTools(server, client);
|
|
33
|
-
registerVersionTools(server, client, deps.versionHint, deps.
|
|
33
|
+
registerVersionTools(server, client, deps.versionHint, deps.build);
|
|
34
34
|
}
|
package/dist/tools/issues.js
CHANGED
|
@@ -9,7 +9,7 @@ import { fetchIssueEnumsWithCache } from './config.js';
|
|
|
9
9
|
import { postNote, noteEditsSchema, prepareNoteEdits, verifyNoteEdits } from './notes.js';
|
|
10
10
|
import { applyMonitorChanges } from './monitors.js';
|
|
11
11
|
import { firstIssue } from '../issue-envelope.js';
|
|
12
|
-
import { deprecationNote } from '../deprecation.js';
|
|
12
|
+
import { deprecationNote, REMOVAL_VERSION } from '../deprecation.js';
|
|
13
13
|
function errorText(msg) {
|
|
14
14
|
const vh = getVersionHint();
|
|
15
15
|
vh?.triggerLatestVersionFetch();
|
|
@@ -74,6 +74,37 @@ function makeIssueFilter({ status, assigned_to, reporter_id, dateFilter }) {
|
|
|
74
74
|
olderThanUpdatedAfter: (issue) => updatedAfterMs !== undefined && issue.updated_at !== undefined && new Date(issue.updated_at).getTime() <= updatedAfterMs,
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
+
// response: "short" for create_issue / update_issue (#8598) — the fields that
|
|
78
|
+
// confirm what happened, instead of the whole issue with notes and history.
|
|
79
|
+
const responseSchema = z.enum(['short', 'full']).default('full').describe(`"full" (default): the whole issue as MantisBT returns it, including description, notes and history. ` +
|
|
80
|
+
`"short": only the fields that confirm the change — far fewer tokens. The default changes to "short" in ${REMOVAL_VERSION}.`);
|
|
81
|
+
const SHORT_CREATE_FIELDS = ['id', 'summary', 'project', 'category', 'status', 'handler', 'created_at', 'view_url'];
|
|
82
|
+
const SHORT_UPDATE_FIELDS = ['id', 'summary', 'status', 'resolution', 'updated_at', 'view_url'];
|
|
83
|
+
/** Returns only the given top-level fields of an object (those it has). */
|
|
84
|
+
function pickFields(obj, fields) {
|
|
85
|
+
return Object.fromEntries(Object.entries(obj).filter(([key]) => fields.includes(key)));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Short update_issue result: the core fields plus every changed field. Custom
|
|
89
|
+
* fields and notes are narrowed to the entries that were changed.
|
|
90
|
+
*/
|
|
91
|
+
function shortUpdatedIssue(issue, changes) {
|
|
92
|
+
const short = pickFields(issue, [...SHORT_UPDATE_FIELDS, ...Object.keys(changes)]);
|
|
93
|
+
const { custom_fields: changedCustomFields, notes: editedNotes } = changes;
|
|
94
|
+
if (short.custom_fields && changedCustomFields) {
|
|
95
|
+
short.custom_fields = short.custom_fields.filter(({ field }) => changedCustomFields.some(c => c.field.id === field.id || c.field.name === field.name));
|
|
96
|
+
}
|
|
97
|
+
if (short.notes && editedNotes) {
|
|
98
|
+
short.notes = short.notes.filter(n => editedNotes.some(e => e.id === n.id));
|
|
99
|
+
}
|
|
100
|
+
return short;
|
|
101
|
+
}
|
|
102
|
+
/** Returns the issue without the given top-level fields. */
|
|
103
|
+
function omitFields(issue, fields) {
|
|
104
|
+
if (fields.length === 0)
|
|
105
|
+
return issue;
|
|
106
|
+
return Object.fromEntries(Object.entries(issue).filter(([key]) => !fields.includes(key)));
|
|
107
|
+
}
|
|
77
108
|
// Worker-pool: runs `fn` over all `items` with at most `concurrency` in-flight at once.
|
|
78
109
|
// nextIndex is only incremented inside microtasks, so the ++ is safe without a lock.
|
|
79
110
|
async function runWithConcurrency(items, concurrency, fn) {
|
|
@@ -115,14 +146,21 @@ export function registerIssueTools(server, client, cache) {
|
|
|
115
146
|
* the current page once MantisBT's newest-first order shows the rest is
|
|
116
147
|
* older than updated_after. Returns truncated when maxPages ended the scan
|
|
117
148
|
* before either happened.
|
|
149
|
+
*
|
|
150
|
+
* select is the caller's projection. The filter reads its fields from each
|
|
151
|
+
* issue, so a select that omits them would fail every issue (#8597): they
|
|
152
|
+
* are fetched as well and removed again before onMatch sees the issue.
|
|
118
153
|
*/
|
|
119
|
-
async function scanMatchingIssues(params, filter, pageSize, maxPages, onMatch) {
|
|
154
|
+
async function scanMatchingIssues(params, select, filter, pageSize, maxPages, onMatch) {
|
|
155
|
+
const selected = select?.split(',').map(f => f.trim()).filter(Boolean);
|
|
156
|
+
const extraFields = selected ? filter.fields.filter(f => !selected.includes(f)) : [];
|
|
157
|
+
const fetchSelect = selected && [...selected, ...extraFields].join(',');
|
|
120
158
|
for (let page = 1; page <= maxPages; page++) {
|
|
121
|
-
const issues = (await client.get('issues', { ...params, page, page_size: pageSize })).issues ?? [];
|
|
159
|
+
const issues = (await client.get('issues', { ...params, select: fetchSelect, page, page_size: pageSize })).issues ?? [];
|
|
122
160
|
let stopAfterBatch = false;
|
|
123
161
|
for (const issue of issues) {
|
|
124
162
|
if (filter.matches(issue)) {
|
|
125
|
-
if (onMatch(issue) === false)
|
|
163
|
+
if (onMatch(omitFields(issue, extraFields)) === false)
|
|
126
164
|
return { truncated: false };
|
|
127
165
|
}
|
|
128
166
|
else if (filter.olderThanUpdatedAfter(issue)) {
|
|
@@ -139,10 +177,9 @@ export function registerIssueTools(server, client, cache) {
|
|
|
139
177
|
* the fields grouping and filters need.
|
|
140
178
|
*/
|
|
141
179
|
async function groupIssues(groupBy, params, filter) {
|
|
142
|
-
const select = [...new Set(['id', groupBy, ...filter.fields])].join(',');
|
|
143
180
|
const groups = new Map();
|
|
144
181
|
let total = 0;
|
|
145
|
-
const { truncated } = await scanMatchingIssues(
|
|
182
|
+
const { truncated } = await scanMatchingIssues(params, `id,${groupBy}`, filter, GROUP_BY_PAGE_SIZE, GROUP_BY_MAX_PAGES, issue => {
|
|
146
183
|
const ref = issue[groupBy];
|
|
147
184
|
const key = ref?.id ?? null;
|
|
148
185
|
let group = groups.get(key);
|
|
@@ -300,7 +337,7 @@ Use "select" to fetch only some fields, e.g. select: "notes" for just the notes
|
|
|
300
337
|
const MAX_API_PAGES = 10; // hard cap: scan at most 500 issues
|
|
301
338
|
const neededTotal = page * page_size; // need this many matches to serve page N
|
|
302
339
|
const matching = [];
|
|
303
|
-
const { truncated } = await scanMatchingIssues(baseParams, filter, API_PAGE_SIZE, MAX_API_PAGES, issue => {
|
|
340
|
+
const { truncated } = await scanMatchingIssues(baseParams, select, filter, API_PAGE_SIZE, MAX_API_PAGES, issue => {
|
|
304
341
|
matching.push(issue);
|
|
305
342
|
return matching.length < neededTotal;
|
|
306
343
|
});
|
|
@@ -324,7 +361,7 @@ Use "select" to fetch only some fields, e.g. select: "notes" for just the notes
|
|
|
324
361
|
// ---------------------------------------------------------------------------
|
|
325
362
|
server.registerTool('create_issue', {
|
|
326
363
|
title: 'Create Issue',
|
|
327
|
-
description: `Create a new MantisBT issue. Returns the full created issue object including the assigned id, summary, status, priority, severity, category, reporter, created_at, and view_url.
|
|
364
|
+
description: `Create a new MantisBT issue. Returns the full created issue object including the assigned id, summary, status, priority, severity, category, reporter, created_at, and view_url. With response: "short" only id, summary, project, category, status, handler, created_at and view_url (default "full"; it changes to "short" in ${REMOVAL_VERSION}).
|
|
328
365
|
|
|
329
366
|
Required fields: summary, description, project_id, category. All other fields are optional with sensible defaults (priority: "normal", severity: "minor").
|
|
330
367
|
|
|
@@ -353,13 +390,14 @@ For the handler, prefer the username field (resolved server-side) over handler_i
|
|
|
353
390
|
reproducibility: z.string().optional().describe('How reliably the issue reproduces. Canonical English names: always, sometimes, random, have not tried, unable to reproduce, N/A. Use get_issue_schema to see localized labels.'),
|
|
354
391
|
view_state: z.enum(['public', 'private']).optional().describe('Visibility of the issue: "public" (visible to all, default) or "private" (restricted to higher-access users).'),
|
|
355
392
|
custom_fields: z.array(customFieldEntry).optional().describe('Custom field values: [{field: {id|name}, value: "<string>"}]. Use get_issue_schema or get_metadata to discover available custom fields per project.'),
|
|
393
|
+
response: responseSchema,
|
|
356
394
|
}),
|
|
357
395
|
annotations: {
|
|
358
396
|
readOnlyHint: false,
|
|
359
397
|
destructiveHint: false,
|
|
360
398
|
idempotentHint: false,
|
|
361
399
|
},
|
|
362
|
-
}, async ({ summary, description, project_id, category, priority, severity, handler_id, handler, version, target_version, fixed_in_version, steps_to_reproduce, additional_information, reproducibility, view_state, custom_fields }) => {
|
|
400
|
+
}, async ({ summary, description, project_id, category, priority, severity, handler_id, handler, version, target_version, fixed_in_version, steps_to_reproduce, additional_information, reproducibility, view_state, custom_fields, response }) => {
|
|
363
401
|
// Resolve handler username to handler_id when only a name is given
|
|
364
402
|
let resolvedHandlerId = handler_id;
|
|
365
403
|
if (resolvedHandlerId === undefined && handler !== undefined) {
|
|
@@ -425,19 +463,22 @@ For the handler, prefer the username field (resolved server-side) over handler_i
|
|
|
425
463
|
const partial = firstIssue(raw) ?? raw;
|
|
426
464
|
let issue = partial;
|
|
427
465
|
if (!('summary' in partial)) {
|
|
428
|
-
// Older MantisBT returned only { id: N } — fetch the
|
|
466
|
+
// Older MantisBT returned only { id: N } — fetch the issue (only the
|
|
467
|
+
// short fields when those are all that is returned).
|
|
429
468
|
// Suppress GET errors: the issue was already created.
|
|
430
469
|
try {
|
|
431
|
-
const
|
|
470
|
+
const select = response === 'short' ? SHORT_CREATE_FIELDS.filter(f => f !== 'view_url').join(',') : undefined;
|
|
471
|
+
const fetched = await client.get(`issues/${partial.id}`, { select });
|
|
432
472
|
issue = firstIssue(fetched) ?? partial;
|
|
433
473
|
}
|
|
434
474
|
catch {
|
|
435
475
|
// unable to fetch details — return minimal object
|
|
436
476
|
}
|
|
437
477
|
}
|
|
438
|
-
const
|
|
478
|
+
const enriched = enrichIssue(issue, await client.getBaseUrl());
|
|
479
|
+
const result = response === 'short' ? pickFields(enriched, SHORT_CREATE_FIELDS) : enriched;
|
|
439
480
|
return {
|
|
440
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
481
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
441
482
|
};
|
|
442
483
|
}
|
|
443
484
|
catch (error) {
|
|
@@ -495,6 +536,8 @@ Use the optional "note" parameter to append a note in the same call (e.g. the re
|
|
|
495
536
|
|
|
496
537
|
Use "add_monitors" / "remove_monitors" (lists of MantisBT login names) to change who watches the issue — "fields" may be omitted then. The response contains "monitors" with added/removed/not_monitoring and the current monitor list. Adding or removing other users requires the monitor_add_others_bug_threshold / monitor_delete_others_bug_threshold access level.
|
|
497
538
|
|
|
539
|
+
The response is the updated issue. With response: "short" it holds only id, summary, status, resolution, updated_at, view_url and the fields that were changed (custom_fields and notes narrowed to the changed entries; an appended note as { id, view_url }) — use it for routine changes. Default is "full"; it changes to "short" in ${REMOVAL_VERSION}.
|
|
540
|
+
|
|
498
541
|
Steps run in order: fields, monitors, note. Once one step succeeded, a later failure is reported inline ("notes_error" when a note edit was not applied, "monitors_error", "note_error") instead of failing the call — do not retry the steps that went through. Only if every requested step failed is the call an error.`,
|
|
499
542
|
inputSchema: z.object({
|
|
500
543
|
id: z.coerce.number().int().positive().describe('Numeric issue ID to update'),
|
|
@@ -503,6 +546,7 @@ Steps run in order: fields, monitors, note. Once one step succeeded, a later fai
|
|
|
503
546
|
dry_run: z.preprocess(coerceBool, z.boolean().optional()).describe('If true, return the patch payload that would be sent without actually updating the issue. Useful for previewing changes before committing them.'),
|
|
504
547
|
note: z.string().min(1).optional().describe('Optional note text appended after a successful update (e.g. reason for a status change). Replaces a separate add_note call.'),
|
|
505
548
|
note_view_state: z.enum(['public', 'private']).default('public').describe('Visibility of the appended note: "public" (default) or "private". Only used when "note" is set.'),
|
|
549
|
+
response: responseSchema,
|
|
506
550
|
fields: z.preprocess((v) => {
|
|
507
551
|
if (typeof v !== 'string')
|
|
508
552
|
return v;
|
|
@@ -537,7 +581,7 @@ Steps run in order: fields, monitors, note. Once one step succeeded, a later fai
|
|
|
537
581
|
destructiveHint: false,
|
|
538
582
|
idempotentHint: false,
|
|
539
583
|
},
|
|
540
|
-
}, async ({ id, fields, dry_run, note, note_view_state, add_monitors, remove_monitors }) => {
|
|
584
|
+
}, async ({ id, fields, dry_run, note, note_view_state, add_monitors, remove_monitors, response: responseMode }) => {
|
|
541
585
|
const fieldChanges = fields ?? {};
|
|
542
586
|
const hasFields = Object.keys(fieldChanges).length > 0;
|
|
543
587
|
const hasMonitorChanges = add_monitors !== undefined || remove_monitors !== undefined;
|
|
@@ -574,9 +618,10 @@ Steps run in order: fields, monitors, note. Once one step succeeded, a later fai
|
|
|
574
618
|
}],
|
|
575
619
|
};
|
|
576
620
|
}
|
|
621
|
+
const short = responseMode === 'short';
|
|
577
622
|
const patched = hasFields ? await patchIssueFields(id, patchFields) : undefined;
|
|
578
623
|
const response = patched
|
|
579
|
-
? { ...patched }
|
|
624
|
+
? (short ? shortUpdatedIssue(patched, fieldChanges) : { ...patched })
|
|
580
625
|
: { id, view_url: buildIssueViewUrl(await client.getBaseUrl(), id) };
|
|
581
626
|
// MantisBT skips note edits it does not apply without an error, so the
|
|
582
627
|
// response is checked.
|
|
@@ -614,7 +659,8 @@ Steps run in order: fields, monitors, note. Once one step succeeded, a later fai
|
|
|
614
659
|
}
|
|
615
660
|
if (note !== undefined) {
|
|
616
661
|
try {
|
|
617
|
-
|
|
662
|
+
const posted = await postNote(client, id, note, note_view_state);
|
|
663
|
+
response['note'] = short ? pickFields(posted, ['id', 'view_url']) : posted;
|
|
618
664
|
succeeded = true;
|
|
619
665
|
}
|
|
620
666
|
catch (noteError) {
|
package/dist/tools/version.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { coerceBool } from '../coerce.js';
|
|
3
3
|
import { parseVersion, compareVersions } from '../version-hint.js';
|
|
4
4
|
import { deprecationNote } from '../deprecation.js';
|
|
5
|
-
export function registerVersionTools(server, client, versionHint,
|
|
5
|
+
export function registerVersionTools(server, client, versionHint, build) {
|
|
6
6
|
/**
|
|
7
7
|
* Installed MantisBT version (X-Mantis-Version header), optionally compared
|
|
8
8
|
* with the latest MantisBT release on GitHub.
|
|
@@ -40,7 +40,9 @@ export function registerVersionTools(server, client, versionHint, mcpVersion) {
|
|
|
40
40
|
server.registerTool('get_version', {
|
|
41
41
|
title: 'Get Versions',
|
|
42
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? } }
|
|
43
|
+
{ mcp_server: { version, commit?, built_at? }, mantis: { installed_version, latest_version?, status?, github_note? } }
|
|
44
|
+
|
|
45
|
+
mcp_server describes the running build: version ends in "-dev" unless it is a published release; commit is the short hash it was built from ("-dirty" with uncommitted changes), built_at the ISO-8601 build time.
|
|
44
46
|
|
|
45
47
|
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
48
|
inputSchema: z.object({ check_latest: checkLatestSchema }),
|
|
@@ -54,7 +56,7 @@ The MantisBT version is read from the X-Mantis-Version response header. With che
|
|
|
54
56
|
error: error instanceof Error ? error.message : String(error),
|
|
55
57
|
}));
|
|
56
58
|
return {
|
|
57
|
-
content: [{ type: 'text', text: JSON.stringify({ mcp_server:
|
|
59
|
+
content: [{ type: 'text', text: JSON.stringify({ mcp_server: build, mantis }, null, 2) }],
|
|
58
60
|
};
|
|
59
61
|
});
|
|
60
62
|
// ---------------------------------------------------------------------------
|
|
@@ -70,7 +72,7 @@ The MantisBT version is read from the X-Mantis-Version response header. With che
|
|
|
70
72
|
idempotentHint: true,
|
|
71
73
|
},
|
|
72
74
|
}, async () => ({
|
|
73
|
-
content: [{ type: 'text', text: JSON.stringify({ version:
|
|
75
|
+
content: [{ type: 'text', text: JSON.stringify({ version: build.version }, null, 2) }],
|
|
74
76
|
}));
|
|
75
77
|
// ---------------------------------------------------------------------------
|
|
76
78
|
// get_mantis_version
|
package/docs/cookbook.de.md
CHANGED
|
@@ -779,6 +779,47 @@ Löst ein Issue auf und schließt es. **Immer beide Felder** `status` und `resol
|
|
|
779
779
|
|
|
780
780
|
---
|
|
781
781
|
|
|
782
|
+
### Kurze Antwort statt des ganzen Issues
|
|
783
|
+
|
|
784
|
+
Standardmäßig geben `update_issue` und `create_issue` das ganze Issue zurück, mit Beschreibung, Notizen und Historie – bei einem lebhaften Issue mehrere tausend Token. Mit `response: "short"` kommt nur zurück, was die Änderung bestätigt.
|
|
785
|
+
|
|
786
|
+
**Tool:** `update_issue` (bei `create_issue` genauso)
|
|
787
|
+
|
|
788
|
+
**Parameter:**
|
|
789
|
+
- `response` – _(optional)_ `"full"` (Standard) oder `"short"`. In 2.0.0 wird `"short"` zum Standard.
|
|
790
|
+
|
|
791
|
+
**Request:**
|
|
792
|
+
|
|
793
|
+
```json
|
|
794
|
+
{
|
|
795
|
+
"id": 1042,
|
|
796
|
+
"fields": {
|
|
797
|
+
"status": { "name": "resolved" },
|
|
798
|
+
"resolution": { "id": 20 }
|
|
799
|
+
},
|
|
800
|
+
"note": "Behoben in Commit abc123.",
|
|
801
|
+
"response": "short"
|
|
802
|
+
}
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
**Response:**
|
|
806
|
+
|
|
807
|
+
```json
|
|
808
|
+
{
|
|
809
|
+
"id": 1042,
|
|
810
|
+
"summary": "Login schlägt auf iOS fehl",
|
|
811
|
+
"status": { "id": 80, "name": "resolved", "label": "erledigt" },
|
|
812
|
+
"resolution": { "id": 20, "name": "fixed", "label": "erledigt" },
|
|
813
|
+
"updated_at": "2026-11-09T13:21:00+01:00",
|
|
814
|
+
"view_url": "https://mantis.example.com/view.php?id=1042",
|
|
815
|
+
"note": { "id": 4957, "view_url": "https://mantis.example.com/view.php?id=1042#bugnote4957" }
|
|
816
|
+
}
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
`update_issue` liefert `id`, `summary`, `status`, `resolution`, `updated_at`, `view_url` und jedes geänderte Feld; `custom_fields` und `notes` (Änderungen über `fields.notes`) sind auf die geänderten Einträge beschränkt, eine angehängte Notiz auf `id` und `view_url`. `monitors` und die `*_error`-Schlüssel erscheinen wie bei `"full"`. `create_issue` liefert `id`, `summary`, `project`, `category`, `status`, `handler`, `created_at` und `view_url`.
|
|
820
|
+
|
|
821
|
+
---
|
|
822
|
+
|
|
782
823
|
### Benutzerdefinierte Felder setzen
|
|
783
824
|
|
|
784
825
|
Schreibt Custom-Field-Werte beim Anlegen oder Aktualisieren. Dasselbe `custom_fields`-Format funktioniert bei `create_issue` und `update_issue` (dort innerhalb von `fields`).
|
|
@@ -1684,7 +1725,7 @@ Gibt die Anzahl der indizierten Issues, die Gesamtanzahl der Issues und den Zeit
|
|
|
1684
1725
|
|
|
1685
1726
|
### Nach Bedeutung suchen
|
|
1686
1727
|
|
|
1687
|
-
Findet Issues, die einem natürlichsprachigen Suchbegriff semantisch ähnlich sind, kombiniert mit einer Stichwortsuche, damit auch exakte Begriffe (Bezeichner, Fehlercodes, Versionsnummern) gefunden werden. Gibt Issue-IDs und Relevanz-Scores zurück. `score` ist die semantische Ähnlichkeit; die Reihenfolge folgt der kombinierten Rangfolge, ein Ergebnis mit exaktem Stichworttreffer kann also vor einem mit höherem `score` stehen.
|
|
1728
|
+
Findet Issues, die einem natürlichsprachigen Suchbegriff semantisch ähnlich sind, kombiniert mit einer Stichwortsuche, damit auch exakte Begriffe (Bezeichner, Fehlercodes, Versionsnummern) gefunden werden. Gibt Issue-IDs und Relevanz-Scores zurück. `score` ist die semantische Ähnlichkeit; die Reihenfolge folgt der kombinierten Rangfolge, ein Ergebnis mit exaktem Stichworttreffer kann also vor einem mit höherem `score` stehen. Issues, die eine Kennung aus der Anfrage vollständig enthalten – eine Auftragsnummer, eine CVE, eine Version, einen Code-Bezeichner wie `MANTIS_SEARCH_DIR` –, stehen vor allen anderen, es sei denn, die Kennung kommt in mehr als 20 Issues vor.
|
|
1688
1729
|
|
|
1689
1730
|
**Tool:** `search_issues`
|
|
1690
1731
|
|
|
@@ -1712,7 +1753,38 @@ Findet Issues, die einem natürlichsprachigen Suchbegriff semantisch ähnlich si
|
|
|
1712
1753
|
]
|
|
1713
1754
|
```
|
|
1714
1755
|
|
|
1715
|
-
> **Hinweis:** Die semantische Suche gibt die top-N ähnlichsten Issues zurück – vollständige Treffergarantie besteht nicht. Sie eignet sich nicht für Abfragen wie »alle Issues zu Thema X auflisten«.
|
|
1756
|
+
> **Hinweis:** Die semantische Suche gibt die top-N ähnlichsten Issues zurück – vollständige Treffergarantie besteht nicht. Sie eignet sich nicht für Abfragen wie »alle Issues zu Thema X auflisten«. Ausnahme ist die Suche nach einer seltenen Kennung: Die Issues, die sie enthalten, stehen vorn (siehe unten), mit ausreichend großem `top_n` werden also alle aufgelistet.
|
|
1757
|
+
|
|
1758
|
+
---
|
|
1759
|
+
|
|
1760
|
+
### Nach einer Kennung suchen
|
|
1761
|
+
|
|
1762
|
+
Eine Suche nach einer Auftragsnummer, einer CVE, einer Version oder einem Code-Bezeichner listet alle Issues, die sie vollständig enthalten, vor allen anderen Ergebnissen. Kennungen werden an ihrer Form erkannt: ein Unterstrich, ein Großbuchstabe mitten im Wort, Buchstaben mit Ziffern, fünf oder mehr Ziffern oder mit `-` `.` `:` `/` verbundene Teile mit einer Ziffer. `1.15` trifft nicht `1.15.2`, und `CVE-2026-1234` trifft nicht `CVE-2026-12345`. Gewöhnliche Wörter und kurze Zahlen behalten die kombinierte Rangfolge, ebenso eine Kennung, die in mehr als 20 Issues vorkommt.
|
|
1763
|
+
|
|
1764
|
+
**Tool:** `search_issues`
|
|
1765
|
+
|
|
1766
|
+
**Request:**
|
|
1767
|
+
|
|
1768
|
+
```json
|
|
1769
|
+
{
|
|
1770
|
+
"query": "CVE-2026-1234",
|
|
1771
|
+
"top_n": 5
|
|
1772
|
+
}
|
|
1773
|
+
```
|
|
1774
|
+
|
|
1775
|
+
**Response:**
|
|
1776
|
+
|
|
1777
|
+
```json
|
|
1778
|
+
[
|
|
1779
|
+
{ "id": 2210, "score": 0.58, "view_url": "https://mantis.example.com/view.php?id=2210" },
|
|
1780
|
+
{ "id": 2211, "score": 0.47, "view_url": "https://mantis.example.com/view.php?id=2211" },
|
|
1781
|
+
{ "id": 2084, "score": 0.65, "view_url": "https://mantis.example.com/view.php?id=2084" },
|
|
1782
|
+
{ "id": 1977, "score": 0.63, "view_url": "https://mantis.example.com/view.php?id=1977" },
|
|
1783
|
+
{ "id": 2150, "score": 0.61, "view_url": "https://mantis.example.com/view.php?id=2150" }
|
|
1784
|
+
]
|
|
1785
|
+
```
|
|
1786
|
+
|
|
1787
|
+
Die Issues 2210 und 2211 enthalten `CVE-2026-1234` und stehen vorn, obwohl Issue 2084 den höheren `score` hat.
|
|
1716
1788
|
|
|
1717
1789
|
---
|
|
1718
1790
|
|
|
@@ -2313,7 +2385,7 @@ Gibt die Version dieses mantisbt-mcp-servers und der verbundenen MantisBT-Instal
|
|
|
2313
2385
|
|
|
2314
2386
|
```json
|
|
2315
2387
|
{
|
|
2316
|
-
"mcp_server": { "version": "1.
|
|
2388
|
+
"mcp_server": { "version": "1.18.0", "commit": "9ebef4c", "built_at": "2026-11-09T12:00:00.000Z" },
|
|
2317
2389
|
"mantis": {
|
|
2318
2390
|
"installed_version": "2.27.0",
|
|
2319
2391
|
"latest_version": "2.27.1",
|
|
@@ -2324,6 +2396,8 @@ Gibt die Version dieses mantisbt-mcp-servers und der verbundenen MantisBT-Instal
|
|
|
2324
2396
|
|
|
2325
2397
|
Mögliche Werte für `status`: `up-to-date`, `update-available`, `newer-than-release`, `unknown`. Mit `check_latest: false` den GitHub-Vergleich überspringen.
|
|
2326
2398
|
|
|
2399
|
+
`mcp_server` beschreibt den laufenden Build, nicht die aktuelle `package.json`: `version` ist die Version, aus der gebaut wurde, `commit` der kurze Commit-Hash (mit `-dirty`, wenn es nicht committete Änderungen gab), `built_at` der Build-Zeitpunkt. Nur ein veröffentlichter Release meldet eine reine Versionsnummer; ein lokaler Build, `npm run dev` oder Code direkt aus `src/` meldet z. B. `1.18.0-dev`. Ohne Git fehlt `commit`; Code direkt aus `src/` hat weder `commit` noch `built_at`.
|
|
2400
|
+
|
|
2327
2401
|
> **Veraltet:** `get_mcp_version` und `get_mantis_version` werden in 2.0.0 entfernt – stattdessen `get_version` wie oben gezeigt verwenden.
|
|
2328
2402
|
|
|
2329
2403
|
---
|
package/docs/cookbook.md
CHANGED
|
@@ -779,6 +779,47 @@ Changes the status and appends a note explaining the change — in one `update_i
|
|
|
779
779
|
|
|
780
780
|
---
|
|
781
781
|
|
|
782
|
+
### Get a short response instead of the whole issue
|
|
783
|
+
|
|
784
|
+
By default `update_issue` and `create_issue` return the whole issue, including description, notes and history — several thousand tokens on a busy issue. With `response: "short"` you get only what confirms the change.
|
|
785
|
+
|
|
786
|
+
**Tool:** `update_issue` (works the same way for `create_issue`)
|
|
787
|
+
|
|
788
|
+
**Parameters:**
|
|
789
|
+
- `response` — _(optional)_ `"full"` (default) or `"short"`. The default changes to `"short"` in 2.0.0.
|
|
790
|
+
|
|
791
|
+
**Request:**
|
|
792
|
+
|
|
793
|
+
```json
|
|
794
|
+
{
|
|
795
|
+
"id": 1042,
|
|
796
|
+
"fields": {
|
|
797
|
+
"status": { "name": "resolved" },
|
|
798
|
+
"resolution": { "id": 20 }
|
|
799
|
+
},
|
|
800
|
+
"note": "Fixed in commit abc123.",
|
|
801
|
+
"response": "short"
|
|
802
|
+
}
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
**Response:**
|
|
806
|
+
|
|
807
|
+
```json
|
|
808
|
+
{
|
|
809
|
+
"id": 1042,
|
|
810
|
+
"summary": "Login fails on iOS",
|
|
811
|
+
"status": { "id": 80, "name": "resolved", "label": "resolved" },
|
|
812
|
+
"resolution": { "id": 20, "name": "fixed", "label": "fixed" },
|
|
813
|
+
"updated_at": "2026-11-09T13:21:00+01:00",
|
|
814
|
+
"view_url": "https://mantis.example.com/view.php?id=1042",
|
|
815
|
+
"note": { "id": 4957, "view_url": "https://mantis.example.com/view.php?id=1042#bugnote4957" }
|
|
816
|
+
}
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
`update_issue` returns `id`, `summary`, `status`, `resolution`, `updated_at`, `view_url` and every field you changed; `custom_fields` and `notes` (edits via `fields.notes`) are narrowed to the entries you changed, an appended note to `id` and `view_url`. `monitors` and the `*_error` keys appear as with `"full"`. `create_issue` returns `id`, `summary`, `project`, `category`, `status`, `handler`, `created_at` and `view_url`.
|
|
820
|
+
|
|
821
|
+
---
|
|
822
|
+
|
|
782
823
|
### Set custom fields
|
|
783
824
|
|
|
784
825
|
Writes custom field values on create or update. The same `custom_fields` format works for both `create_issue` and `update_issue` (inside `fields`).
|
|
@@ -1684,7 +1725,7 @@ Returns the number of indexed issues, total issue count, and the timestamp of th
|
|
|
1684
1725
|
|
|
1685
1726
|
### Search by meaning
|
|
1686
1727
|
|
|
1687
|
-
Finds issues semantically similar to a natural language query, combined with keyword matching so that exact terms (identifiers, error codes, version numbers) are found as well. Returns issue IDs and relevance scores. `score` is the semantic similarity; the order follows the combined ranking, so a result with an exact keyword match can come before one with a higher `score`.
|
|
1728
|
+
Finds issues semantically similar to a natural language query, combined with keyword matching so that exact terms (identifiers, error codes, version numbers) are found as well. Returns issue IDs and relevance scores. `score` is the semantic similarity; the order follows the combined ranking, so a result with an exact keyword match can come before one with a higher `score`. Issues that contain an identifier from the query as a whole — an order number, a CVE, a version, a code name such as `MANTIS_SEARCH_DIR` — are listed before all others, unless the identifier occurs in more than 20 issues.
|
|
1688
1729
|
|
|
1689
1730
|
**Tool:** `search_issues`
|
|
1690
1731
|
|
|
@@ -1712,7 +1753,38 @@ Finds issues semantically similar to a natural language query, combined with key
|
|
|
1712
1753
|
]
|
|
1713
1754
|
```
|
|
1714
1755
|
|
|
1715
|
-
> **Note:** Semantic search returns the top-N most similar issues — it does not guarantee exhaustive recall. It is not suitable for "find all issues about X" census queries.
|
|
1756
|
+
> **Note:** Semantic search returns the top-N most similar issues — it does not guarantee exhaustive recall. It is not suitable for "find all issues about X" census queries. The exception is a query for a rare identifier: the issues that contain it come first (see below), so with a large enough `top_n` they are all listed.
|
|
1757
|
+
|
|
1758
|
+
---
|
|
1759
|
+
|
|
1760
|
+
### Search for an identifier
|
|
1761
|
+
|
|
1762
|
+
A query for an order number, a CVE, a version or a code name lists every issue that contains it as a whole before all other results. Identifiers are recognised by their form: an underscore, a capital inside the word, letters with digits, five or more digits, or parts joined by `-` `.` `:` `/` with a digit. `1.15` does not match `1.15.2`, and `CVE-2026-1234` does not match `CVE-2026-12345`. Plain words and short numbers keep the combined ranking, as does an identifier that occurs in more than 20 issues.
|
|
1763
|
+
|
|
1764
|
+
**Tool:** `search_issues`
|
|
1765
|
+
|
|
1766
|
+
**Request:**
|
|
1767
|
+
|
|
1768
|
+
```json
|
|
1769
|
+
{
|
|
1770
|
+
"query": "CVE-2026-1234",
|
|
1771
|
+
"top_n": 5
|
|
1772
|
+
}
|
|
1773
|
+
```
|
|
1774
|
+
|
|
1775
|
+
**Response:**
|
|
1776
|
+
|
|
1777
|
+
```json
|
|
1778
|
+
[
|
|
1779
|
+
{ "id": 2210, "score": 0.58, "view_url": "https://mantis.example.com/view.php?id=2210" },
|
|
1780
|
+
{ "id": 2211, "score": 0.47, "view_url": "https://mantis.example.com/view.php?id=2211" },
|
|
1781
|
+
{ "id": 2084, "score": 0.65, "view_url": "https://mantis.example.com/view.php?id=2084" },
|
|
1782
|
+
{ "id": 1977, "score": 0.63, "view_url": "https://mantis.example.com/view.php?id=1977" },
|
|
1783
|
+
{ "id": 2150, "score": 0.61, "view_url": "https://mantis.example.com/view.php?id=2150" }
|
|
1784
|
+
]
|
|
1785
|
+
```
|
|
1786
|
+
|
|
1787
|
+
Issues 2210 and 2211 contain `CVE-2026-1234` and come first, although issue 2084 has the higher `score`.
|
|
1716
1788
|
|
|
1717
1789
|
---
|
|
1718
1790
|
|
|
@@ -2313,7 +2385,7 @@ Returns the version of this mantisbt-mcp-server and of the connected MantisBT in
|
|
|
2313
2385
|
|
|
2314
2386
|
```json
|
|
2315
2387
|
{
|
|
2316
|
-
"mcp_server": { "version": "1.
|
|
2388
|
+
"mcp_server": { "version": "1.18.0", "commit": "9ebef4c", "built_at": "2026-11-09T12:00:00.000Z" },
|
|
2317
2389
|
"mantis": {
|
|
2318
2390
|
"installed_version": "2.27.0",
|
|
2319
2391
|
"latest_version": "2.27.1",
|
|
@@ -2324,6 +2396,8 @@ Returns the version of this mantisbt-mcp-server and of the connected MantisBT in
|
|
|
2324
2396
|
|
|
2325
2397
|
Possible values for `status`: `up-to-date`, `update-available`, `newer-than-release`, `unknown`. Set `check_latest: false` to skip the GitHub comparison.
|
|
2326
2398
|
|
|
2399
|
+
`mcp_server` describes the running build, not the current `package.json`: `version` is the version it was built from, `commit` the short commit hash (with `-dirty` if there were uncommitted changes), `built_at` the build time. Only a published release reports a plain version; a local build, `npm run dev` or code run straight from `src/` reports e.g. `1.18.0-dev`. Without git, `commit` is left out; code run from `src/` has neither `commit` nor `built_at`.
|
|
2400
|
+
|
|
2327
2401
|
> **Deprecated:** `get_mcp_version` and `get_mantis_version` will be removed in 2.0.0 — use `get_version` as shown above.
|
|
2328
2402
|
|
|
2329
2403
|
---
|
package/docs/examples.de.md
CHANGED
|
@@ -201,10 +201,14 @@ Die semantische Suche versteht die *Bedeutung* deiner Frage – nicht nur einzel
|
|
|
201
201
|
|
|
202
202
|
### Exakte Begriffe
|
|
203
203
|
|
|
204
|
-
Die Suche berücksichtigt auch exakte Stichwörter, daher funktionieren Bezeichner, Fehlercodes und Versionsnummern ebenfalls als Suchanfrage.
|
|
204
|
+
Die Suche berücksichtigt auch exakte Stichwörter, daher funktionieren Bezeichner, Fehlercodes und Versionsnummern ebenfalls als Suchanfrage. Issues, die die Kennung enthalten, stehen vorn.
|
|
205
205
|
|
|
206
206
|
> »Welche Issues erwähnen `MANTIS_SEARCH_DIR`?«
|
|
207
207
|
|
|
208
|
+
> »Finde alle Issues zu `CVE-2026-1234`.«
|
|
209
|
+
|
|
210
|
+
Wie Kennungen erkannt werden: [Cookbook – Nach einer Kennung suchen](cookbook.de.md#nach-einer-kennung-suchen).
|
|
211
|
+
|
|
208
212
|
> »Finde Issues zum Fehler `ERR_CERT_DATE_INVALID`.«
|
|
209
213
|
|
|
210
214
|
---
|
package/docs/examples.md
CHANGED
|
@@ -201,10 +201,14 @@ Semantic search understands the *meaning* of your question — not just keywords
|
|
|
201
201
|
|
|
202
202
|
### Exact terms
|
|
203
203
|
|
|
204
|
-
The search also matches exact keywords, so identifiers, error codes and version numbers work as queries too.
|
|
204
|
+
The search also matches exact keywords, so identifiers, error codes and version numbers work as queries too. Issues that contain the identifier come first.
|
|
205
205
|
|
|
206
206
|
> "Which issues mention `MANTIS_SEARCH_DIR`?"
|
|
207
207
|
|
|
208
|
+
> "Find all issues about `CVE-2026-1234`."
|
|
209
|
+
|
|
210
|
+
How identifiers are recognised: [Cookbook — Search for an identifier](cookbook.md#search-for-an-identifier).
|
|
211
|
+
|
|
208
212
|
> "Find issues about the error `ERR_CERT_DATE_INVALID`."
|
|
209
213
|
|
|
210
214
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dpesch/mantisbt-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
4
4
|
"mcpName": "io.github.dpesch/mantisbt-mcp-server",
|
|
5
5
|
"description": "MCP server for MantisBT REST API – read and manage bug tracker issues",
|
|
6
6
|
"author": "Dominik Pesch",
|
|
@@ -32,10 +32,11 @@
|
|
|
32
32
|
},
|
|
33
33
|
"type": "module",
|
|
34
34
|
"scripts": {
|
|
35
|
-
"build": "tsc -p tsconfig.build.json",
|
|
35
|
+
"build": "tsc -p tsconfig.build.json && node scripts/write-build-info.mjs",
|
|
36
36
|
"typecheck": "tsc --noEmit",
|
|
37
37
|
"start": "node dist/index.js",
|
|
38
|
-
"dev": "tsc -p tsconfig.build.json --watch",
|
|
38
|
+
"dev": "node scripts/write-build-info.mjs && tsc -p tsconfig.build.json --watch",
|
|
39
|
+
"prepublishOnly": "node scripts/check-release-build.mjs",
|
|
39
40
|
"dev:start": "npm run typecheck && npm run build && node dist/index.js",
|
|
40
41
|
"test": "vitest run",
|
|
41
42
|
"test:watch": "vitest",
|
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.
|
|
6
|
+
"version": "1.18.0",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"registryType": "npm",
|
|
10
10
|
"identifier": "@dpesch/mantisbt-mcp-server",
|
|
11
|
-
"version": "1.
|
|
11
|
+
"version": "1.18.0",
|
|
12
12
|
"runtimeHint": "npx",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|