@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 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
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "version": "1.18.0",
3
+ "commit": "65fcaba",
4
+ "built_at": "2026-11-09T13:22:17.570Z"
5
+ }
@@ -1,4 +1,7 @@
1
- /** Major version in which deprecated tools are removed (see versioning rules in CLAUDE.md). */
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 { join, dirname } from 'node:path';
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
- // Read version from package.json
18
+ // Version of the running build (dist/build-info.json, see build-info.ts)
19
19
  // ---------------------------------------------------------------------------
20
- const __filename = fileURLToPath(import.meta.url);
21
- const __dirname = dirname(__filename);
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
- version,
58
+ build,
61
59
  uploadDir: startupConfig.uploadDir,
62
60
  transport,
63
61
  searchIndexStatus,
@@ -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) {
@@ -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
  }
@@ -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 { result, fused: 1 / (RRF_K + vectorRank) + (rank === undefined ? 0 : 1 / (RRF_K + rank)) };
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) {
@@ -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'),
@@ -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.version);
33
+ registerVersionTools(server, client, deps.versionHint, deps.build);
34
34
  }
@@ -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({ ...params, select }, filter, GROUP_BY_PAGE_SIZE, GROUP_BY_MAX_PAGES, issue => {
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 full issue.
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 fetched = await client.get(`issues/${partial.id}`);
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 baseUrl = await client.getBaseUrl();
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(enrichIssue(issue, baseUrl), null, 2) }],
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
- response['note'] = await postNote(client, id, note, note_view_state);
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) {
@@ -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, mcpVersion) {
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: { version: mcpVersion }, mantis }, null, 2) }],
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: mcpVersion }, null, 2) }],
75
+ content: [{ type: 'text', text: JSON.stringify({ version: build.version }, null, 2) }],
74
76
  }));
75
77
  // ---------------------------------------------------------------------------
76
78
  // get_mantis_version
@@ -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.15.0" },
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.15.0" },
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
  ---
@@ -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.16.0",
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.16.0",
6
+ "version": "1.18.0",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "identifier": "@dpesch/mantisbt-mcp-server",
11
- "version": "1.16.0",
11
+ "version": "1.18.0",
12
12
  "runtimeHint": "npx",
13
13
  "transport": {
14
14
  "type": "stdio"