@dpesch/mantisbt-mcp-server 1.12.0 → 1.14.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,42 @@ This project adheres to [Semantic Versioning](https://semver.org/).
11
11
 
12
12
  ---
13
13
 
14
+ ## [1.14.0] – 2026-09-27
15
+
16
+ ### Fixed
17
+
18
+ - `create_issue` / `update_issue`: a `custom_fields` entry without `value` is now rejected with a validation error. Previously it was accepted and the literal string `"undefined"` was written to the custom field.
19
+
20
+ ### Changed
21
+
22
+ - **Minimum Node.js version is now 22** (`engines: >=22`). Node.js 18 and 20 are end-of-life and no longer receive security updates; the test toolchain (vitest 5) no longer runs on them, so they could not be tested anymore. CI now runs on Node.js 22. Users on Node.js 20 or older should upgrade — npm only warns about the `engines` mismatch, but these versions are no longer tested.
23
+ - Dependencies: `zod` 4 (the MCP SDK supports `^3.25 || ^4`). Tool parameters, types and defaults are unchanged. The published JSON schemas differ slightly: nested `$ref`s are inlined, integer IDs carry an explicit `maximum`, and objects no longer declare `additionalProperties: false` (unknown keys were and still are ignored).
24
+ - Dependencies: `@huggingface/transformers` 4 for the optional semantic search. Embeddings are numerically identical to version 3 (verified with the default model), so existing search indexes stay valid — no rebuild needed. This also resolves the `sharp` security advisories reported by `npm audit`.
25
+ - Dependencies: `@modelcontextprotocol/sdk` minimum raised to ^1.30.1; transitive dependencies updated to resolve all `npm audit` advisories (including a critical one in `tar`).
26
+ - Dev dependencies: vitest and `@vitest/coverage-v8` 5, `@types/node` 22 (matching the minimum Node.js version).
27
+ - CI: the publish job now checks `NPM_TOKEN` and `GH_RELEASE_TOKEN` before `npm publish`, so an empty or invalid token stops the release before anything is published. Each publish step (npm, Codeberg release, GitHub release) is skipped when its target already exists, so re-running a failed job only catches up on what is missing.
28
+
29
+ ---
30
+
31
+ ## [1.13.0] – 2026-09-27
32
+
33
+ ### Added
34
+
35
+ - `create_version` — create a new version in a MantisBT project; returns the created version object (id, name, description, released, obsolete, timestamp)
36
+ - `update_version` — update an existing version; only passed fields change (at least one required); renaming rewrites version/target_version/fixed_in_version on all referencing issues; requires `manage_project_threshold` (default: manager)
37
+ - `release_version` — mark a version as released (sets its date, default now) and optionally create a follow-up version in the same step via `next_version`; requires `manage_project_threshold` (default: manager)
38
+ - `delete_version` — permanently delete a version; irreversible — MantisBT clears version/target_version/fixed_in_version on all referencing issues; requires `manage_project_threshold` (default: manager)
39
+ - Version writes are mirrored into the local metadata cache without resetting its timestamp, so `get_metadata` and the project resources stay current without extending the cache TTL.
40
+ - New cookbook recipes (EN/DE) for creating, updating, releasing and deleting versions, plus matching prompt examples in `docs/examples.md` and `docs/examples.de.md`.
41
+
42
+ ### Changed
43
+
44
+ - `get_project_versions` description now documents the `timestamp` field returned per version and cross-references the new version write tools (`create_version`, `update_version`, `release_version`, `delete_version`)
45
+ - German documentation (`README.de.md`, `docs/cookbook.de.md`, `docs/examples.de.md`) now uses the en dash (–) as Gedankenstrich instead of the em dash.
46
+ - CI: the release job checks the npm token before publishing, and failing Codeberg/GitHub release API calls now fail the job instead of passing silently.
47
+
48
+ ---
49
+
14
50
  ## [1.12.0] – 2026-08-28
15
51
 
16
52
  ### Added
package/README.de.md CHANGED
@@ -12,7 +12,7 @@ Ein [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) Server, der
12
12
 
13
13
  ## Voraussetzungen
14
14
 
15
- - Node.js ≥ 18
15
+ - Node.js ≥ 22
16
16
  - MantisBT-Installation mit aktivierter REST-API (ab Version 2.23)
17
17
  - MantisBT API-Token (unter *Mein Konto → API-Token* erstellen)
18
18
 
@@ -68,21 +68,21 @@ npm run build
68
68
 
69
69
  | Variable | Pflicht | Standard | Beschreibung |
70
70
  |---|---|---|---|
71
- | `MANTIS_BASE_URL` | ✅ | – | Basis-URL der MantisBT-Installation. Beide Formate werden akzeptiert: `https://deine-mantis-instanz.example.com` und `https://deine-mantis-instanz.example.com/api/rest` — das `/api/rest`-Suffix wird automatisch normalisiert. |
71
+ | `MANTIS_BASE_URL` | ✅ | – | Basis-URL der MantisBT-Installation. Beide Formate werden akzeptiert: `https://deine-mantis-instanz.example.com` und `https://deine-mantis-instanz.example.com/api/rest` – das `/api/rest`-Suffix wird automatisch normalisiert. |
72
72
  | `MANTIS_API_KEY` | ✅ | – | API-Token für die Authentifizierung |
73
- | `MANTIS_USE_INDEX_PHP` | – | auto | Auf `true` setzen, wenn URL-Rewriting nicht verfügbar ist — REST-Anfragen verwenden dann `/api/rest/index.php/` statt `/api/rest/`. Wird automatisch erkannt, wenn `MANTIS_BASE_URL` auf `/api/rest/index.php` endet; ein explizit gesetzter Wert hat immer Vorrang. Siehe [Cookbook](docs/cookbook.de.md#installation-ohne-url-rewriting-anbinden). |
73
+ | `MANTIS_USE_INDEX_PHP` | – | auto | Auf `true` setzen, wenn URL-Rewriting nicht verfügbar ist – REST-Anfragen verwenden dann `/api/rest/index.php/` statt `/api/rest/`. Wird automatisch erkannt, wenn `MANTIS_BASE_URL` auf `/api/rest/index.php` endet; ein explizit gesetzter Wert hat immer Vorrang. Siehe [Cookbook](docs/cookbook.de.md#installation-ohne-url-rewriting-anbinden). |
74
74
  | `MANTIS_CACHE_DIR` | – | `~/.cache/mantisbt-mcp` | Verzeichnis für den Metadaten-Cache |
75
75
  | `MANTIS_CACHE_TTL` | – | `3600` | Cache-Lebensdauer in Sekunden |
76
76
  | `TRANSPORT` | – | `stdio` | Transport-Modus: `stdio` oder `http` |
77
77
  | `PORT` | – | `3000` | Port für HTTP-Modus |
78
- | `MCP_HTTP_HOST` | – | `127.0.0.1` | Bind-Adresse für HTTP-Modus. **Geändert von `0.0.0.0` auf `127.0.0.1`** — der Server horcht standardmäßig nur auf localhost. Für Docker oder Remote-Zugriff `0.0.0.0` setzen. |
79
- | `MCP_HTTP_TOKEN` | ✅ (HTTP-Modus) | – | Bearer-Token für den `/mcp`-Endpunkt (`Authorization: Bearer <token>`). **Pflicht bei `TRANSPORT=http`** — der Server startet im HTTP-Modus ohne diesen Wert nicht, damit Tools nie unauthentifiziert erreichbar sind. Im stdio-Modus ohne Bedeutung. `/health` ist immer öffentlich. |
78
+ | `MCP_HTTP_HOST` | – | `127.0.0.1` | Bind-Adresse für HTTP-Modus. **Geändert von `0.0.0.0` auf `127.0.0.1`** – der Server horcht standardmäßig nur auf localhost. Für Docker oder Remote-Zugriff `0.0.0.0` setzen. |
79
+ | `MCP_HTTP_TOKEN` | ✅ (HTTP-Modus) | – | Bearer-Token für den `/mcp`-Endpunkt (`Authorization: Bearer <token>`). **Pflicht bei `TRANSPORT=http`** – der Server startet im HTTP-Modus ohne diesen Wert nicht, damit Tools nie unauthentifiziert erreichbar sind. Im stdio-Modus ohne Bedeutung. `/health` ist immer öffentlich. |
80
80
  | `MANTIS_SEARCH_ENABLED` | – | `false` | Auf `true` setzen, um die semantische Suche zu aktivieren |
81
81
  | `MANTIS_SEARCH_BACKEND` | – | `vectra` | Vektorspeicher: `vectra` (reines JS) oder `sqlite-vec` (manuelle Installation erforderlich) |
82
82
  | `MANTIS_SEARCH_DIR` | – | `{MANTIS_CACHE_DIR}/search` | Verzeichnis für den Suchindex |
83
83
  | `MANTIS_SEARCH_MODEL` | – | `Xenova/paraphrase-multilingual-MiniLM-L12-v2` | Embedding-Modell (wird beim ersten Start einmalig heruntergeladen, ~80 MB) |
84
84
  | `MANTIS_SEARCH_THREADS` | – | `1` | Anzahl der ONNX-Intra-Op-Threads für das Embedding-Modell. Standard ist 1, um CPU-Sättigung auf Mehrkernsystemen und in WSL zu verhindern. Nur erhöhen, wenn die Indexierungsgeschwindigkeit kritisch ist und der Host ausschließlich für diese Last vorgesehen ist. |
85
- | `MANTIS_UPLOAD_DIR` | – | – | Schränkt den `file_path` von `upload_file` auf dieses Verzeichnis ein (Pfad-Traversal via `../` wird blockiert). Im **stdio-Modus** ist `file_path` ohne diese Variable unbeschränkt. Im **HTTP-Modus** liest `file_path` aus dem Dateisystem des Servers und ist deshalb **deaktiviert, sofern diese Variable nicht gesetzt ist** — HTTP-Clients sollten stattdessen den Parameter `content` (Base64) verwenden. |
85
+ | `MANTIS_UPLOAD_DIR` | – | – | Schränkt den `file_path` von `upload_file` auf dieses Verzeichnis ein (Pfad-Traversal via `../` wird blockiert). Im **stdio-Modus** ist `file_path` ohne diese Variable unbeschränkt. Im **HTTP-Modus** liest `file_path` aus dem Dateisystem des Servers und ist deshalb **deaktiviert, sofern diese Variable nicht gesetzt ist** – HTTP-Clients sollten stattdessen den Parameter `content` (Base64) verwenden. |
86
86
 
87
87
  ## Verfügbare Tools
88
88
 
@@ -92,9 +92,9 @@ npm run build
92
92
  |---|---|
93
93
  | `get_issue` | Ein Issue anhand seiner ID abrufen; optionales `select` zur Feldauswahl reduziert die Antwortgröße |
94
94
  | `get_issues` | Mehrere Issues per ID in einem Aufruf abrufen (1–50 IDs); nicht zugängliche IDs liefern `null` an ihrer Position, statt den gesamten Aufruf abzubrechen |
95
- | `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 |
96
- | `create_issue` | Neues Issue anlegen; `severity` und `priority` müssen kanonische englische Namen sein (z.B. `minor`, `major`, `normal`, `high`) — `get_issue_enums` 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 |
97
- | `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 (z.B. die Begründung eines Status-Wechsels) |
95
+ | `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 |
96
+ | `create_issue` | Neues Issue anlegen; `severity` und `priority` müssen kanonische englische Namen sein (z.B. `minor`, `major`, `normal`, `high`) – `get_issue_enums` 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 |
97
+ | `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 (z.B. die Begründung eines Status-Wechsels) |
98
98
  | `delete_issue` | Issue löschen |
99
99
 
100
100
  ### Notizen
@@ -139,20 +139,24 @@ npm run build
139
139
  | Tool | Beschreibung |
140
140
  |---|---|
141
141
  | `list_projects` | Alle zugänglichen Projekte auflisten; gibt normalisierte Projektdaten zurück (konsistent mit dem `sync_metadata`-Cache) |
142
- | `get_project_versions` | Versionen eines Projekts abrufen; optionale Booleans `obsolete` und `inherit` für veraltete bzw. vom Elternprojekt geerbte Versionen |
142
+ | `get_project_versions` | Versionen eines Projekts abrufen; optionale Booleans `obsolete` und `inherit` für veraltete bzw. vom Elternprojekt geerbte Versionen; jede Version enthält ein `timestamp`-Feld (Versionsdatum); zum Ändern siehe `create_version`, `update_version`, `release_version`, `delete_version` |
143
+ | `create_version` | Neue Version in einem Projekt erstellen; gibt das erstellte Versionsobjekt zurück (id, name, description, released, obsolete, timestamp); erfordert `manage_project_threshold` (Standard: Manager) |
144
+ | `update_version` | Bestehende Version aktualisieren; nur übergebene Felder werden geändert (mindestens eines erforderlich); Umbenennen schreibt version/target_version/fixed_in_version aller referenzierenden Issues neu; erfordert `manage_project_threshold` (Standard: Manager) |
145
+ | `release_version` | Version als veröffentlicht markieren und Datum setzen (Standard: jetzt); optional eine Folgeversion im gleichen Schritt anlegen via `next_version`; erfordert `manage_project_threshold` (Standard: Manager) |
146
+ | `delete_version` | Version dauerhaft löschen – nicht umkehrbar, MantisBT leert version/target_version/fixed_in_version aller referenzierenden Issues; `update_version` mit `obsolete=true` als nicht-destruktive Alternative bevorzugen; erfordert `manage_project_threshold` (Standard: Manager) |
143
147
  | `get_project_categories` | Kategorien eines Projekts abrufen |
144
148
  | `get_project_users` | Benutzer eines Projekts abrufen |
145
149
  | `find_project_member` | Projektmitglieder nach Name, Realname oder E-Mail suchen (Teilstring-Suche, Groß-/Kleinschreibung ignoriert); optionale Parameter `query` und `limit` (Standard 10, max. 100); Cache-first |
146
150
 
147
151
  ### Semantische Suche *(optional)*
148
152
 
149
- Statt einfachem Keyword-Matching versteht die semantische Suche die *Bedeutung* einer Anfrage. Formuliere in natürlicher Sprache — die Suche findet konzeptuell verwandte Issues, auch wenn die genauen Begriffe nicht übereinstimmen:
153
+ Statt einfachem Keyword-Matching versteht die semantische Suche die *Bedeutung* einer Anfrage. Formuliere in natürlicher Sprache – die Suche findet konzeptuell verwandte Issues, auch wenn die genauen Begriffe nicht übereinstimmen:
150
154
 
151
- - *„Login funktioniert nach Passwort-Reset nicht"* — findet Issues rund um Authentifizierungsgrenzfälle
152
- - *„Performance-Probleme auf der Checkout-Seite"* — liefert verwandte Meldungen unabhängig von der verwendeten Terminologie
153
- - *„doppelte Einträge in der Rechnungsliste"* — erkennt auch Issues, die als „zweifach angezeigt", „dupliziert" o.ä. beschrieben sind
155
+ - *„Login funktioniert nach Passwort-Reset nicht"* – findet Issues rund um Authentifizierungsgrenzfälle
156
+ - *„Performance-Probleme auf der Checkout-Seite"* – liefert verwandte Meldungen unabhängig von der verwendeten Terminologie
157
+ - *„doppelte Einträge in der Rechnungsliste"* – erkennt auch Issues, die als „zweifach angezeigt", „dupliziert" o.ä. beschrieben sind
154
158
 
155
- 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).
159
+ 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).
156
160
 
157
161
  Aktivierung mit `MANTIS_SEARCH_ENABLED=true`.
158
162
 
@@ -190,13 +194,13 @@ npm install sqlite-vec better-sqlite3
190
194
  | `get_current_user` | Eigenes Benutzerprofil abrufen |
191
195
  | `list_languages` | Verfügbare Sprachen auflisten |
192
196
  | `get_config` | Server-Konfiguration (Basis-URL, Cache-TTL) anzeigen |
193
- | `get_issue_enums` | Gültige ID/Name-Paare für alle Enum-Felder zurückgeben (Severity, Status, Priority, Resolution, Reproducibility) — vor `create_issue` / `update_issue` verwenden, um korrekte Werte nachzuschlagen; auf lokalisierten Installationen kann jeder Eintrag ein `canonical_name`-Feld mit dem englischen Standard-API-Namen enthalten |
197
+ | `get_issue_enums` | Gültige ID/Name-Paare für alle Enum-Felder zurückgeben (Severity, Status, Priority, Resolution, Reproducibility) – vor `create_issue` / `update_issue` verwenden, um korrekte Werte nachzuschlagen; auf lokalisierten Installationen kann jeder Eintrag ein `canonical_name`-Feld mit dem englischen Standard-API-Namen enthalten |
194
198
  | `get_mantis_version` | MantisBT-Version abrufen und auf Updates prüfen |
195
199
  | `get_mcp_version` | Version dieser mantisbt-mcp-server-Instanz zurückgeben |
196
200
 
197
201
  ## Verfügbare Ressourcen
198
202
 
199
- MCP-Ressourcen sind URI-adressierbare, schreibgeschützte Daten, die Clients direkt abrufen können, ohne ein Tool aufzurufen. Sie sind das dritte MCP-Primitiv neben Tools und Prompts. Hinweis: Ressourcen werden von MCP-Clients weniger breit unterstützt als Tools — bitte die Dokumentation des jeweiligen Clients prüfen.
203
+ MCP-Ressourcen sind URI-adressierbare, schreibgeschützte Daten, die Clients direkt abrufen können, ohne ein Tool aufzurufen. Sie sind das dritte MCP-Primitiv neben Tools und Prompts. Hinweis: Ressourcen werden von MCP-Clients weniger breit unterstützt als Tools – bitte die Dokumentation des jeweiligen Clients prüfen.
200
204
 
201
205
  | Ressource-URI | Beschreibung |
202
206
  |---|---|
@@ -207,7 +211,7 @@ MCP-Ressourcen sind URI-adressierbare, schreibgeschützte Daten, die Clients dir
207
211
 
208
212
  ## Verfügbare Prompts
209
213
 
210
- MCP-Prompt-Templates sind Gesprächseinstiege, die den LLM anweisen, strukturierte Eingaben zu sammeln und dann das passende Tool aufzurufen. Es handelt sich nicht um Tools — sie starten einen geführten Arbeitsablauf.
214
+ MCP-Prompt-Templates sind Gesprächseinstiege, die den LLM anweisen, strukturierte Eingaben zu sammeln und dann das passende Tool aufzurufen. Es handelt sich nicht um Tools – sie starten einen geführten Arbeitsablauf.
211
215
 
212
216
  | Prompt | Pflichtargumente | Optionale Argumente | Beschreibung |
213
217
  |---|---|---|---|
@@ -218,7 +222,7 @@ MCP-Prompt-Templates sind Gesprächseinstiege, die den LLM anweisen, strukturier
218
222
 
219
223
  ## HTTP-Modus
220
224
 
221
- Für den Einsatz als eigenständiger Server (z.B. in Remote-Setups). `MCP_HTTP_TOKEN` ist im HTTP-Modus **Pflicht** — der Server startet ohne diesen Wert nicht:
225
+ Für den Einsatz als eigenständiger Server (z.B. in Remote-Setups). `MCP_HTTP_TOKEN` ist im HTTP-Modus **Pflicht** – der Server startet ohne diesen Wert nicht:
222
226
 
223
227
  ```bash
224
228
  MCP_HTTP_TOKEN=secret MANTIS_BASE_URL=... MANTIS_API_KEY=... \
@@ -229,14 +233,14 @@ MCP_HTTP_TOKEN=secret MANTIS_BASE_URL=... MANTIS_API_KEY=... \
229
233
  # TRANSPORT=http PORT=3456 MCP_HTTP_HOST=0.0.0.0 node dist/index.js
230
234
  ```
231
235
 
232
- Jede `/mcp`-Anfrage muss `Authorization: Bearer <token>` senden. Im HTTP-Modus ist der `file_path` von `upload_file` deaktiviert, sofern `MANTIS_UPLOAD_DIR` nicht gesetzt ist — stattdessen den Parameter `content` (Base64) verwenden.
236
+ Jede `/mcp`-Anfrage muss `Authorization: Bearer <token>` senden. Im HTTP-Modus ist der `file_path` von `upload_file` deaktiviert, sofern `MANTIS_UPLOAD_DIR` nicht gesetzt ist – stattdessen den Parameter `content` (Base64) verwenden.
233
237
 
234
238
  Healthcheck: `GET http://localhost:3456/health` (immer öffentlich, kein Token erforderlich)
235
239
 
236
240
  ## Dokumentation
237
241
 
238
- - [**Cookbook**](docs/cookbook.de.md) — Tool-orientierte Rezepte mit direkt verwendbaren Parameter-Beispielen für alle registrierten Tools
239
- - [**Anwendungsbeispiele**](docs/examples.de.md) — Beispiele in natürlicher Sprache für alltägliche Anwendungsfälle (keine Tool-Namen erforderlich)
242
+ - [**Cookbook**](docs/cookbook.de.md) – Tool-orientierte Rezepte mit direkt verwendbaren Parameter-Beispielen für alle registrierten Tools
243
+ - [**Anwendungsbeispiele**](docs/examples.de.md) – Beispiele in natürlicher Sprache für alltägliche Anwendungsfälle (keine Tool-Namen erforderlich)
240
244
 
241
245
  ## Entwicklung
242
246
 
package/README.md CHANGED
@@ -12,7 +12,7 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that i
12
12
 
13
13
  ## Requirements
14
14
 
15
- - Node.js ≥ 18
15
+ - Node.js ≥ 22
16
16
  - MantisBT installation with REST API enabled (version 2.23+)
17
17
  - MantisBT API token (create under *My Account → API Tokens*)
18
18
 
@@ -139,7 +139,11 @@ npm run build
139
139
  | Tool | Description |
140
140
  |---|---|
141
141
  | `list_projects` | List all accessible projects; returns normalized project data (consistent with `sync_metadata` cache) |
142
- | `get_project_versions` | Get versions of a project; optional `obsolete` and `inherit` booleans to include obsolete or parent-inherited versions |
142
+ | `get_project_versions` | Get versions of a project; optional `obsolete` and `inherit` booleans to include obsolete or parent-inherited versions; each version includes a `timestamp` field (version date); see `create_version`, `update_version`, `release_version`, `delete_version` to modify versions |
143
+ | `create_version` | Create a new version in a project; returns the created version object (id, name, description, released, obsolete, timestamp); requires `manage_project_threshold` (default: manager) |
144
+ | `update_version` | Update an existing version; only the fields you pass are changed (at least one required); renaming rewrites version/target_version/fixed_in_version on all referencing issues; requires `manage_project_threshold` (default: manager) |
145
+ | `release_version` | Mark a version as released and set its date (default: now); optionally create a follow-up version in the same step via `next_version`; requires `manage_project_threshold` (default: manager) |
146
+ | `delete_version` | Permanently delete a version — irreversible, MantisBT clears version/target_version/fixed_in_version on all referencing issues; prefer `update_version` with `obsolete=true` as the non-destructive alternative; requires `manage_project_threshold` (default: manager) |
143
147
  | `get_project_categories` | Get categories of a project |
144
148
  | `get_project_users` | Get users of a project |
145
149
  | `find_project_member` | Search project members by name, real name, or email (case-insensitive substring match); optional `query` and `limit` (default 10, max 100); cache-first |
package/dist/cache.js CHANGED
@@ -56,6 +56,24 @@ export class MetadataCache {
56
56
  };
57
57
  await writeFile(this.filePath, JSON.stringify(file, null, 2), 'utf-8');
58
58
  }
59
+ /**
60
+ * Applies `mutate` to the cached version list of every project and writes the
61
+ * result back with the original timestamp — a partial update must not extend
62
+ * the TTL of the remaining metadata. No-op when no cache file exists.
63
+ */
64
+ async patchVersions(mutate) {
65
+ let file;
66
+ try {
67
+ file = JSON.parse(await readFile(this.filePath, 'utf-8'));
68
+ }
69
+ catch {
70
+ return;
71
+ }
72
+ for (const [projectId, meta] of Object.entries(file.data.byProject)) {
73
+ meta.versions = mutate(Number(projectId), meta.versions);
74
+ }
75
+ await writeFile(this.filePath, JSON.stringify(file, null, 2), 'utf-8');
76
+ }
59
77
  async invalidate() {
60
78
  try {
61
79
  await unlink(this.filePath);
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ import { registerFileTools } from './tools/files.js';
16
16
  import { registerRelationshipTools } from './tools/relationships.js';
17
17
  import { registerMonitorTools } from './tools/monitors.js';
18
18
  import { registerProjectTools } from './tools/projects.js';
19
+ import { registerProjectVersionTools } from './tools/project-versions.js';
19
20
  import { registerUserTools } from './tools/users.js';
20
21
  import { registerFilterTools } from './tools/filters.js';
21
22
  import { registerConfigTools } from './tools/config.js';
@@ -60,6 +61,7 @@ async function createMcpServer(transport) {
60
61
  registerRelationshipTools(server, client);
61
62
  registerMonitorTools(server, client);
62
63
  registerProjectTools(server, client, cache);
64
+ registerProjectVersionTools(server, client, cache);
63
65
  registerUserTools(server, client);
64
66
  registerFilterTools(server, client);
65
67
  registerConfigTools(server, client, cache);
@@ -47,8 +47,7 @@ export function registerSearchTools(server, client, store, embedder) {
47
47
  'When provided, each matching issue is fetched from MantisBT and enriched with the requested fields. ' +
48
48
  'The relevance score is always included. Without this parameter only id and score are returned.'),
49
49
  highlight: z
50
- .preprocess(coerceBool, z.boolean())
51
- .default(false)
50
+ .preprocess(coerceBool, z.boolean().default(false))
52
51
  .describe('If true, adds a "highlights" field per result with query terms bolded (**term**) ' +
53
52
  'in the issue summary and a short description snippet. ' +
54
53
  'Note: highlights are keyword-based, not semantic — some results may have no highlighted terms.'),
@@ -0,0 +1,207 @@
1
+ import { z } from 'zod';
2
+ import { getVersionHint } from '../version-hint.js';
3
+ function errorText(msg) {
4
+ const vh = getVersionHint();
5
+ vh?.triggerLatestVersionFetch();
6
+ const hint = vh?.getUpdateHint();
7
+ return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
8
+ }
9
+ const coerceBool = (val) => val === 'true' ? true : val === 'false' ? false : val;
10
+ /** Drops undefined values so PATCH only touches the fields the caller set. */
11
+ function definedFields(fields) {
12
+ return Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined));
13
+ }
14
+ const PERMISSION_NOTE = 'Requires the manage_project_threshold access level in the project (MantisBT default: manager).';
15
+ const OWNING_PROJECT_NOTE = 'The version must belong to project_id itself: versions inherited from a parent project can only be changed via the parent\'s project_id (otherwise MantisBT answers "Version not found"). get_project_versions with inherit=false (the default) lists the project\'s own versions.';
16
+ const projectIdSchema = z.coerce.number().int().positive().describe('Numeric ID of the project the version belongs to — use list_projects to discover project IDs');
17
+ const versionIdSchema = z.coerce.number().int().positive().describe('Numeric version ID — use get_project_versions to discover version IDs');
18
+ const timestampSchema = z.string().min(1).describe('Version date as ISO 8601 string, e.g. "2026-09-27" or "2026-09-27T10:00:00+02:00". Determines the order in roadmap and changelog.');
19
+ const boolFlag = z.preprocess(coerceBool, z.boolean());
20
+ export function registerProjectVersionTools(server, client, cache) {
21
+ // A failing cache update must never turn a successful API write into an error.
22
+ async function patchCachedVersions(mutate) {
23
+ try {
24
+ await cache?.patchVersions(mutate);
25
+ }
26
+ catch {
27
+ // Cache is a best-effort mirror; sync_metadata repairs it
28
+ }
29
+ }
30
+ async function createVersion(projectId, fields) {
31
+ const result = await client.post(`projects/${projectId}/versions`, definedFields(fields));
32
+ // Only the owning project gets the new entry — whether subprojects inherit it depends on server config
33
+ await patchCachedVersions((pid, versions) => (pid === projectId ? [...versions, result.version] : versions));
34
+ return result.version;
35
+ }
36
+ async function updateVersion(projectId, versionId, fields) {
37
+ const result = await client.patch(`projects/${projectId}/versions/${versionId}`, definedFields(fields));
38
+ // Version IDs are global, so inherited copies in subprojects are updated as well
39
+ await patchCachedVersions((_pid, versions) => versions.map((v) => (v.id === versionId ? result.version : v)));
40
+ return result.version;
41
+ }
42
+ // ---------------------------------------------------------------------------
43
+ // create_version
44
+ // ---------------------------------------------------------------------------
45
+ server.registerTool('create_version', {
46
+ title: 'Create Version',
47
+ description: `Create a new version in a MantisBT project. Returns the created version object (id, name, description, released, obsolete, timestamp).
48
+
49
+ Version names must be unique within the project. Without timestamp, MantisBT uses the current date.
50
+
51
+ ${PERMISSION_NOTE}`,
52
+ inputSchema: z.object({
53
+ project_id: projectIdSchema,
54
+ name: z.string().trim().min(1).describe('Version name, e.g. "1.2.0"'),
55
+ description: z.string().optional().describe('Optional version description'),
56
+ released: boolFlag.optional().describe('Mark the version as released. MantisBT default: false'),
57
+ obsolete: boolFlag.optional().describe('Mark the version as obsolete. MantisBT default: false'),
58
+ timestamp: timestampSchema.optional(),
59
+ }),
60
+ annotations: {
61
+ readOnlyHint: false,
62
+ destructiveHint: false,
63
+ idempotentHint: false,
64
+ },
65
+ }, async ({ project_id, ...fields }) => {
66
+ try {
67
+ const created = await createVersion(project_id, fields);
68
+ return {
69
+ content: [{ type: 'text', text: JSON.stringify(created, null, 2) }],
70
+ };
71
+ }
72
+ catch (error) {
73
+ const msg = error instanceof Error ? error.message : String(error);
74
+ return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
75
+ }
76
+ });
77
+ // ---------------------------------------------------------------------------
78
+ // update_version
79
+ // ---------------------------------------------------------------------------
80
+ server.registerTool('update_version', {
81
+ title: 'Update Version',
82
+ description: `Update an existing version of a MantisBT project. Only the fields you pass are changed. Returns the updated version object.
83
+
84
+ Renaming a version also rewrites the version, target_version and fixed_in_version fields of all issues that reference it (including subprojects if versions are inherited).
85
+
86
+ Setting released=true does not change the version date — pass timestamp as well, or use release_version, which does both.
87
+
88
+ ${OWNING_PROJECT_NOTE}
89
+
90
+ ${PERMISSION_NOTE}`,
91
+ inputSchema: z.object({
92
+ project_id: projectIdSchema,
93
+ version_id: versionIdSchema,
94
+ name: z.string().trim().min(1).optional().describe('New version name (must be unique within the project)'),
95
+ description: z.string().optional().describe('New version description'),
96
+ released: boolFlag.optional().describe('Released flag'),
97
+ obsolete: boolFlag.optional().describe('Obsolete flag'),
98
+ timestamp: timestampSchema.optional(),
99
+ }),
100
+ annotations: {
101
+ readOnlyHint: false,
102
+ destructiveHint: false,
103
+ idempotentHint: true,
104
+ },
105
+ }, async ({ project_id, version_id, ...fields }) => {
106
+ if (Object.keys(definedFields(fields)).length === 0) {
107
+ return {
108
+ content: [{ type: 'text', text: errorText('No fields to update — pass at least one of name, description, released, obsolete, timestamp.') }],
109
+ isError: true,
110
+ };
111
+ }
112
+ try {
113
+ const updated = await updateVersion(project_id, version_id, fields);
114
+ return {
115
+ content: [{ type: 'text', text: JSON.stringify(updated, null, 2) }],
116
+ };
117
+ }
118
+ catch (error) {
119
+ const msg = error instanceof Error ? error.message : String(error);
120
+ return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
121
+ }
122
+ });
123
+ // ---------------------------------------------------------------------------
124
+ // release_version
125
+ // ---------------------------------------------------------------------------
126
+ server.registerTool('release_version', {
127
+ title: 'Release Version',
128
+ description: `Mark a version as released and set its date (default: now). Optionally create a follow-up version in the same step.
129
+
130
+ The follow-up version is only created when next_version is given; its name is used as-is — no automatic numbering.
131
+
132
+ Returns { released: <version> } plus either next_version: <version> or next_version_error: <message>. The two steps are separate API calls: if creating the follow-up version fails, the release itself stays in effect and the error is reported in next_version_error.
133
+
134
+ ${OWNING_PROJECT_NOTE}
135
+
136
+ ${PERMISSION_NOTE}`,
137
+ inputSchema: z.object({
138
+ project_id: projectIdSchema,
139
+ version_id: versionIdSchema,
140
+ timestamp: timestampSchema.optional().describe('Release date as ISO 8601 string. Default: now.'),
141
+ next_version: z.string().trim().min(1).optional().describe('Name of a follow-up version to create as unreleased placeholder, e.g. "1.2.1". Omit to only release.'),
142
+ }),
143
+ annotations: {
144
+ readOnlyHint: false,
145
+ destructiveHint: false,
146
+ idempotentHint: false,
147
+ },
148
+ }, async ({ project_id, version_id, timestamp, next_version }) => {
149
+ let released;
150
+ try {
151
+ released = await updateVersion(project_id, version_id, {
152
+ released: true,
153
+ timestamp: timestamp ?? new Date().toISOString(),
154
+ });
155
+ }
156
+ catch (error) {
157
+ const msg = error instanceof Error ? error.message : String(error);
158
+ return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
159
+ }
160
+ const result = { released };
161
+ if (next_version !== undefined) {
162
+ try {
163
+ result.next_version = await createVersion(project_id, { name: next_version });
164
+ }
165
+ catch (error) {
166
+ result.next_version_error = error instanceof Error ? error.message : String(error);
167
+ }
168
+ }
169
+ return {
170
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
171
+ };
172
+ });
173
+ // ---------------------------------------------------------------------------
174
+ // delete_version
175
+ // ---------------------------------------------------------------------------
176
+ server.registerTool('delete_version', {
177
+ title: 'Delete Version',
178
+ description: `Permanently delete a version of a MantisBT project. This action is irreversible.
179
+
180
+ MantisBT clears the version, target_version and fixed_in_version fields of all issues that reference the deleted version (including subprojects if versions are inherited). To retire a version without touching issues, set obsolete=true via update_version instead.
181
+
182
+ ${OWNING_PROJECT_NOTE}
183
+
184
+ ${PERMISSION_NOTE}`,
185
+ inputSchema: z.object({
186
+ project_id: projectIdSchema,
187
+ version_id: versionIdSchema,
188
+ }),
189
+ annotations: {
190
+ readOnlyHint: false,
191
+ destructiveHint: true,
192
+ idempotentHint: true,
193
+ },
194
+ }, async ({ project_id, version_id }) => {
195
+ try {
196
+ await client.delete(`projects/${project_id}/versions/${version_id}`);
197
+ await patchCachedVersions((_pid, versions) => versions.filter((v) => v.id !== version_id));
198
+ return {
199
+ content: [{ type: 'text', text: `Version ${version_id} deleted successfully.` }],
200
+ };
201
+ }
202
+ catch (error) {
203
+ const msg = error instanceof Error ? error.message : String(error);
204
+ return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
205
+ }
206
+ });
207
+ }
@@ -76,7 +76,7 @@ Prerequisites: obtain project_id from list_projects.`,
76
76
  // ---------------------------------------------------------------------------
77
77
  server.registerTool('get_project_versions', {
78
78
  title: 'Get Project Versions',
79
- description: `List all versions defined for a MantisBT project. Returns an array of version objects, each containing id, name, released (boolean), obsolete (boolean), and optionally a date field.
79
+ description: `List all versions defined for a MantisBT project. Returns an array of version objects, each containing id, name, released (boolean), obsolete (boolean), and timestamp (version date). To create, change, release or delete versions use create_version, update_version, release_version and delete_version.
80
80
 
81
81
  Use the returned version names directly when creating or updating issues via create_issue and update_issue (version, target_version, fixed_in_version fields).
82
82
 
@@ -85,8 +85,8 @@ By default, obsolete and inherited parent-project versions are excluded. Set obs
85
85
  Prerequisites: obtain project_id from list_projects.`,
86
86
  inputSchema: z.object({
87
87
  project_id: z.coerce.number().int().positive().describe('Numeric project ID — use list_projects to discover project IDs'),
88
- obsolete: z.preprocess(coerceBool, z.boolean()).default(false).describe('Include obsolete (deprecated) versions in the response. Default: false. Set to true to see all versions including those no longer actively used.'),
89
- inherit: z.preprocess(coerceBool, z.boolean()).default(false).describe('Include versions inherited from parent projects. Default: false. Set to true for sub-projects that share versions with a parent project.'),
88
+ obsolete: z.preprocess(coerceBool, z.boolean().default(false)).describe('Include obsolete (deprecated) versions in the response. Default: false. Set to true to see all versions including those no longer actively used.'),
89
+ inherit: z.preprocess(coerceBool, z.boolean().default(false)).describe('Include versions inherited from parent projects. Default: false. Set to true for sub-projects that share versions with a parent project.'),
90
90
  }),
91
91
  annotations: {
92
92
  readOnlyHint: true,
@@ -27,7 +27,7 @@ export function registerVersionTools(server, client, versionHint, mcpVersion) {
27
27
  The version is read from the X-Mantis-Version response header sent by every API call.
28
28
  The GitHub comparison requires an outbound HTTPS request to the GitHub API.`,
29
29
  inputSchema: z.object({
30
- check_latest: z.preprocess(coerceBool, z.boolean()).default(true).describe('Whether to fetch the latest release from GitHub and compare (default: true)'),
30
+ check_latest: z.preprocess(coerceBool, z.boolean().default(true)).describe('Whether to fetch the latest release from GitHub and compare (default: true)'),
31
31
  }),
32
32
  annotations: {
33
33
  readOnlyHint: true,