@dpesch/mantisbt-mcp-server 1.11.0 → 1.13.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,41 @@ This project adheres to [Semantic Versioning](https://semver.org/).
11
11
 
12
12
  ---
13
13
 
14
+ ## [1.13.0] – 2026-09-27
15
+
16
+ ### Added
17
+
18
+ - `create_version` — create a new version in a MantisBT project; returns the created version object (id, name, description, released, obsolete, timestamp)
19
+ - `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)
20
+ - `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)
21
+ - `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)
22
+ - 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.
23
+ - 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`.
24
+
25
+ ### Changed
26
+
27
+ - `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`)
28
+ - 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.
29
+ - 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.
30
+
31
+ ---
32
+
33
+ ## [1.12.0] – 2026-08-28
34
+
35
+ ### Added
36
+
37
+ - Added `MANTIS_USE_INDEX_PHP=true` for MantisBT installations without URL rewriting. REST requests use `/api/rest/index.php/` instead of `/api/rest/` when enabled. Thanks to [@iantrudell](https://github.com/iantrudell) for the contribution.
38
+ - `MANTIS_USE_INDEX_PHP` is detected automatically when `MANTIS_BASE_URL` ends with `/api/rest/index.php`, so pasting the full REST URL of an installation without URL rewriting is enough. An explicitly set value always wins; a base URL contradicting an explicit `false` is reported on stderr instead of failing silently.
39
+ - New cookbook section "Connect to an installation without URL rewriting" in `docs/cookbook.md` and `docs/cookbook.de.md`.
40
+
41
+ ### Changed
42
+
43
+ - `MANTIS_BASE_URL` suffix normalization is now case-insensitive, so `/API/REST` is stripped like `/api/rest`.
44
+ - `.env.local` is now read once per process instead of once per config path, removing a duplicate file read on every startup.
45
+ - `README.md` and `README.de.md` now link to each other, making the German translation discoverable.
46
+
47
+ ---
48
+
14
49
  ## [1.11.0] – 2026-07-03
15
50
 
16
51
  ### Added
package/README.de.md CHANGED
@@ -6,6 +6,8 @@
6
6
  [![MCP Badge](https://lobehub.com/badge/mcp/dpesch-mantisbt-mcp-server)](https://lobehub.com/mcp/dpesch-mantisbt-mcp-server)
7
7
  [![MantisBT MCP Server](https://glama.ai/mcp/servers/dpesch/mantisbt-mcp-server/badges/card.svg)](https://glama.ai/mcp/servers/dpesch/mantisbt-mcp-server)
8
8
 
9
+ [English](README.md) · **Deutsch**
10
+
9
11
  Ein [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) Server, der die [MantisBT REST API](https://documenter.getpostman.com/view/29959/mantis-bug-tracker-rest-api) in Claude Code und andere MCP-fähige Clients integriert. Issues lesen, erstellen und bearbeiten – direkt aus dem Editor heraus.
10
12
 
11
13
  ## Voraussetzungen
@@ -66,20 +68,21 @@ npm run build
66
68
 
67
69
  | Variable | Pflicht | Standard | Beschreibung |
68
70
  |---|---|---|---|
69
- | `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. |
70
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). |
71
74
  | `MANTIS_CACHE_DIR` | – | `~/.cache/mantisbt-mcp` | Verzeichnis für den Metadaten-Cache |
72
75
  | `MANTIS_CACHE_TTL` | – | `3600` | Cache-Lebensdauer in Sekunden |
73
76
  | `TRANSPORT` | – | `stdio` | Transport-Modus: `stdio` oder `http` |
74
77
  | `PORT` | – | `3000` | Port für HTTP-Modus |
75
- | `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. |
76
- | `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. |
77
80
  | `MANTIS_SEARCH_ENABLED` | – | `false` | Auf `true` setzen, um die semantische Suche zu aktivieren |
78
81
  | `MANTIS_SEARCH_BACKEND` | – | `vectra` | Vektorspeicher: `vectra` (reines JS) oder `sqlite-vec` (manuelle Installation erforderlich) |
79
82
  | `MANTIS_SEARCH_DIR` | – | `{MANTIS_CACHE_DIR}/search` | Verzeichnis für den Suchindex |
80
83
  | `MANTIS_SEARCH_MODEL` | – | `Xenova/paraphrase-multilingual-MiniLM-L12-v2` | Embedding-Modell (wird beim ersten Start einmalig heruntergeladen, ~80 MB) |
81
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. |
82
- | `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. |
83
86
 
84
87
  ## Verfügbare Tools
85
88
 
@@ -89,9 +92,9 @@ npm run build
89
92
  |---|---|
90
93
  | `get_issue` | Ein Issue anhand seiner ID abrufen; optionales `select` zur Feldauswahl reduziert die Antwortgröße |
91
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 |
92
- | `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 |
93
- | `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 |
94
- | `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) |
95
98
  | `delete_issue` | Issue löschen |
96
99
 
97
100
  ### Notizen
@@ -136,20 +139,24 @@ npm run build
136
139
  | Tool | Beschreibung |
137
140
  |---|---|
138
141
  | `list_projects` | Alle zugänglichen Projekte auflisten; gibt normalisierte Projektdaten zurück (konsistent mit dem `sync_metadata`-Cache) |
139
- | `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) |
140
147
  | `get_project_categories` | Kategorien eines Projekts abrufen |
141
148
  | `get_project_users` | Benutzer eines Projekts abrufen |
142
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 |
143
150
 
144
151
  ### Semantische Suche *(optional)*
145
152
 
146
- 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:
147
154
 
148
- - *„Login funktioniert nach Passwort-Reset nicht"* — findet Issues rund um Authentifizierungsgrenzfälle
149
- - *„Performance-Probleme auf der Checkout-Seite"* — liefert verwandte Meldungen unabhängig von der verwendeten Terminologie
150
- - *„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
151
158
 
152
- 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).
153
160
 
154
161
  Aktivierung mit `MANTIS_SEARCH_ENABLED=true`.
155
162
 
@@ -187,13 +194,13 @@ npm install sqlite-vec better-sqlite3
187
194
  | `get_current_user` | Eigenes Benutzerprofil abrufen |
188
195
  | `list_languages` | Verfügbare Sprachen auflisten |
189
196
  | `get_config` | Server-Konfiguration (Basis-URL, Cache-TTL) anzeigen |
190
- | `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 |
191
198
  | `get_mantis_version` | MantisBT-Version abrufen und auf Updates prüfen |
192
199
  | `get_mcp_version` | Version dieser mantisbt-mcp-server-Instanz zurückgeben |
193
200
 
194
201
  ## Verfügbare Ressourcen
195
202
 
196
- 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.
197
204
 
198
205
  | Ressource-URI | Beschreibung |
199
206
  |---|---|
@@ -204,7 +211,7 @@ MCP-Ressourcen sind URI-adressierbare, schreibgeschützte Daten, die Clients dir
204
211
 
205
212
  ## Verfügbare Prompts
206
213
 
207
- 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.
208
215
 
209
216
  | Prompt | Pflichtargumente | Optionale Argumente | Beschreibung |
210
217
  |---|---|---|---|
@@ -215,7 +222,7 @@ MCP-Prompt-Templates sind Gesprächseinstiege, die den LLM anweisen, strukturier
215
222
 
216
223
  ## HTTP-Modus
217
224
 
218
- 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:
219
226
 
220
227
  ```bash
221
228
  MCP_HTTP_TOKEN=secret MANTIS_BASE_URL=... MANTIS_API_KEY=... \
@@ -226,14 +233,14 @@ MCP_HTTP_TOKEN=secret MANTIS_BASE_URL=... MANTIS_API_KEY=... \
226
233
  # TRANSPORT=http PORT=3456 MCP_HTTP_HOST=0.0.0.0 node dist/index.js
227
234
  ```
228
235
 
229
- 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.
230
237
 
231
238
  Healthcheck: `GET http://localhost:3456/health` (immer öffentlich, kein Token erforderlich)
232
239
 
233
240
  ## Dokumentation
234
241
 
235
- - [**Cookbook**](docs/cookbook.de.md) — Tool-orientierte Rezepte mit direkt verwendbaren Parameter-Beispielen für alle registrierten Tools
236
- - [**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)
237
244
 
238
245
  ## Entwicklung
239
246
 
package/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
  [![MCP Badge](https://lobehub.com/badge/mcp/dpesch-mantisbt-mcp-server)](https://lobehub.com/mcp/dpesch-mantisbt-mcp-server)
7
7
  [![MantisBT MCP Server](https://glama.ai/mcp/servers/dpesch/mantisbt-mcp-server/badges/card.svg)](https://glama.ai/mcp/servers/dpesch/mantisbt-mcp-server)
8
8
 
9
+ **English** · [Deutsch](README.de.md)
10
+
9
11
  A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that integrates the [MantisBT REST API](https://documenter.getpostman.com/view/29959/mantis-bug-tracker-rest-api) into Claude Code and other MCP-capable clients. Read, create, and update issues directly from your editor.
10
12
 
11
13
  ## Requirements
@@ -68,6 +70,7 @@ npm run build
68
70
  |---|---|---|---|
69
71
  | `MANTIS_BASE_URL` | ✅ | – | Base URL of your MantisBT installation. Both `https://your-mantis.example.com` and `https://your-mantis.example.com/api/rest` are accepted — the `/api/rest` suffix is normalized automatically. |
70
72
  | `MANTIS_API_KEY` | ✅ | – | API token for authentication |
73
+ | `MANTIS_USE_INDEX_PHP` | – | auto | Set to `true` when URL rewriting is unavailable — REST requests then use `/api/rest/index.php/` instead of `/api/rest/`. Detected automatically when `MANTIS_BASE_URL` ends with `/api/rest/index.php`; an explicit value always wins. See the [cookbook](docs/cookbook.md#connect-to-an-installation-without-url-rewriting). |
71
74
  | `MANTIS_CACHE_DIR` | – | `~/.cache/mantisbt-mcp` | Directory for the metadata cache |
72
75
  | `MANTIS_CACHE_TTL` | – | `3600` | Cache lifetime in seconds |
73
76
  | `TRANSPORT` | – | `stdio` | Transport mode: `stdio` or `http` |
@@ -136,7 +139,11 @@ npm run build
136
139
  | Tool | Description |
137
140
  |---|---|
138
141
  | `list_projects` | List all accessible projects; returns normalized project data (consistent with `sync_metadata` cache) |
139
- | `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) |
140
147
  | `get_project_categories` | Get categories of a project |
141
148
  | `get_project_users` | Get users of a project |
142
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/client.js CHANGED
@@ -2,13 +2,35 @@
2
2
  // Helpers
3
3
  // ---------------------------------------------------------------------------
4
4
  /**
5
- * Normalise a MANTIS_BASE_URL so that it never ends with "/api/rest" or a
6
- * trailing slash. The client always appends "/api/rest/<path>" itself, so
7
- * users who follow README examples that include "/api/rest" in the URL must
8
- * not end up with a doubled prefix.
5
+ * Recognises a MANTIS_BASE_URL that already points at the REST API, with or
6
+ * without the index.php front controller. Single source of truth for both
7
+ * stripping the suffix and detecting the entry point, so the two can never
8
+ * disagree. Case-insensitive: MantisBT is commonly hosted on case-insensitive
9
+ * servers, which is exactly the IIS/Apache-without-rewrite crowd this matters
10
+ * for.
11
+ */
12
+ const REST_API_SUFFIX = /\/api\/rest(\/index\.php)?\/?$/i;
13
+ /**
14
+ * Normalise a MANTIS_BASE_URL so that it never ends with "/api/rest",
15
+ * "/api/rest/index.php", or a trailing slash. The client appends the REST API
16
+ * prefix itself, so URLs that already include it must not produce a doubled
17
+ * prefix.
18
+ *
19
+ * The "/index.php" part is stripped regardless of the useIndexPhp flag — this
20
+ * function only produces the bare host prefix. Whether requests then route
21
+ * through index.php is answered by usesIndexPhpEntryPoint() on the
22
+ * *unnormalised* URL.
9
23
  */
10
24
  export function normalizeBaseUrl(url) {
11
- return url.replace(/\/api\/rest\/?$/, '').replace(/\/$/, '');
25
+ return url.replace(REST_API_SUFFIX, '').replace(/\/$/, '');
26
+ }
27
+ /**
28
+ * True when an unnormalised base URL already routes through the index.php
29
+ * front controller. Used by the config layer to default MANTIS_USE_INDEX_PHP
30
+ * for users who paste their full REST URL.
31
+ */
32
+ export function usesIndexPhpEntryPoint(url) {
33
+ return REST_API_SUFFIX.exec(url)?.[1] !== undefined;
12
34
  }
13
35
  export function buildIssueViewUrl(baseUrl, issueId) {
14
36
  return `${baseUrl}/view.php?id=${issueId}`;
@@ -31,10 +53,17 @@ export class MantisClient {
31
53
  credentialFactory;
32
54
  responseObserver;
33
55
  resolvedCredentials;
34
- constructor(baseUrlOrFactory, apiKeyOrObserver, responseObserver) {
56
+ constructor(baseUrlOrFactory, apiKeyOrObserver, useIndexPhpOrObserver, responseObserver) {
35
57
  if (typeof baseUrlOrFactory === 'string') {
36
- this.resolvedCredentials = { baseUrl: normalizeBaseUrl(baseUrlOrFactory), apiKey: apiKeyOrObserver };
37
- this.responseObserver = responseObserver;
58
+ const useIndexPhp = typeof useIndexPhpOrObserver === 'boolean' ? useIndexPhpOrObserver : false;
59
+ this.resolvedCredentials = {
60
+ baseUrl: normalizeBaseUrl(baseUrlOrFactory),
61
+ apiKey: apiKeyOrObserver,
62
+ useIndexPhp,
63
+ };
64
+ this.responseObserver = typeof useIndexPhpOrObserver === 'function'
65
+ ? useIndexPhpOrObserver
66
+ : responseObserver;
38
67
  }
39
68
  else {
40
69
  this.credentialFactory = baseUrlOrFactory;
@@ -46,8 +75,8 @@ export class MantisClient {
46
75
  // ---------------------------------------------------------------------------
47
76
  async getCredentials() {
48
77
  if (!this.resolvedCredentials) {
49
- const { baseUrl, apiKey } = await this.credentialFactory();
50
- this.resolvedCredentials = { baseUrl: normalizeBaseUrl(baseUrl), apiKey };
78
+ const { baseUrl, apiKey, useIndexPhp = false } = await this.credentialFactory();
79
+ this.resolvedCredentials = { baseUrl: normalizeBaseUrl(baseUrl), apiKey, useIndexPhp };
51
80
  }
52
81
  return this.resolvedCredentials;
53
82
  }
@@ -55,8 +84,9 @@ export class MantisClient {
55
84
  return (await this.getCredentials()).baseUrl;
56
85
  }
57
86
  async buildUrl(path, params) {
58
- const { baseUrl } = await this.getCredentials();
59
- const url = new URL(`${baseUrl}/api/rest/${path}`);
87
+ const { baseUrl, useIndexPhp } = await this.getCredentials();
88
+ const restPrefix = useIndexPhp ? '/api/rest/index.php/' : '/api/rest/';
89
+ const url = new URL(`${baseUrl}${restPrefix}${path}`);
60
90
  if (params) {
61
91
  for (const [key, value] of Object.entries(params)) {
62
92
  if (value !== undefined) {
package/dist/config.js CHANGED
@@ -2,11 +2,23 @@ import { readFile } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
3
  import { join, dirname } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
- import { normalizeBaseUrl } from './client.js';
5
+ import { normalizeBaseUrl, usesIndexPhpEntryPoint } from './client.js';
6
6
  // ---------------------------------------------------------------------------
7
7
  // .env.local loader
8
8
  // ---------------------------------------------------------------------------
9
- async function loadDotEnvLocal() {
9
+ let cachedDotEnvLocal = null;
10
+ /**
11
+ * Loads .env.local at most once per process. Both getStartupConfig() and
12
+ * getConfig() need it, and the HTTP transport calls them on separate paths —
13
+ * without memoising, the file is read twice on every startup. Caching the
14
+ * promise (not a boolean) also makes concurrent callers share one read.
15
+ */
16
+ function loadDotEnvLocal() {
17
+ if (!cachedDotEnvLocal)
18
+ cachedDotEnvLocal = readDotEnvLocal();
19
+ return cachedDotEnvLocal;
20
+ }
21
+ async function readDotEnvLocal() {
10
22
  try {
11
23
  const envPath = join(dirname(fileURLToPath(import.meta.url)), '..', '.env.local');
12
24
  const content = await readFile(envPath, 'utf-8');
@@ -24,6 +36,9 @@ async function loadDotEnvLocal() {
24
36
  // .env.local not present — use environment variables directly
25
37
  }
26
38
  }
39
+ // ---------------------------------------------------------------------------
40
+ // Non-credential config (safe to read at startup without credentials)
41
+ // ---------------------------------------------------------------------------
27
42
  function readNonCredentialConfig() {
28
43
  const defaultCacheDir = join(homedir(), '.cache', 'mantisbt-mcp');
29
44
  const cacheDir = process.env.MANTIS_CACHE_DIR ?? defaultCacheDir;
@@ -57,6 +72,26 @@ function readNonCredentialConfig() {
57
72
  testEnvironment: process.env.MCP_TEST_ENVIRONMENT === 'true',
58
73
  };
59
74
  }
75
+ /**
76
+ * Resolves index.php routing from MANTIS_USE_INDEX_PHP and the shape of
77
+ * MANTIS_BASE_URL. An explicitly set env var always wins; the URL suffix is
78
+ * only consulted when the user said nothing, so copying the full REST URL
79
+ * from a MantisBT installation without URL rewriting just works. A base URL
80
+ * that contradicts an explicit "false" is reported on stderr — silently
81
+ * ignoring it would look like a broken server, not a misconfiguration.
82
+ */
83
+ function resolveUseIndexPhp(rawBaseUrl) {
84
+ const explicit = process.env.MANTIS_USE_INDEX_PHP;
85
+ const urlImpliesIndexPhp = usesIndexPhpEntryPoint(rawBaseUrl);
86
+ if (explicit === undefined)
87
+ return urlImpliesIndexPhp;
88
+ const enabled = explicit === 'true';
89
+ if (urlImpliesIndexPhp && !enabled) {
90
+ process.stderr.write(`[mantisbt-config] MANTIS_BASE_URL ends with /api/rest/index.php, but MANTIS_USE_INDEX_PHP="${explicit}" — ` +
91
+ `using plain /api/rest/ routing as configured. Unset MANTIS_USE_INDEX_PHP to route through index.php.\n`);
92
+ }
93
+ return enabled;
94
+ }
60
95
  /**
61
96
  * Returns all non-credential config values. Never throws, even when
62
97
  * MANTIS_BASE_URL / MANTIS_API_KEY are absent. Use this at server startup
@@ -102,6 +137,7 @@ export async function getConfig() {
102
137
  cachedConfig = {
103
138
  baseUrl: normalizeBaseUrl(baseUrl),
104
139
  apiKey,
140
+ useIndexPhp: resolveUseIndexPhp(baseUrl),
105
141
  ...readNonCredentialConfig(),
106
142
  };
107
143
  return cachedConfig;
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';
@@ -43,7 +44,11 @@ async function createMcpServer(transport) {
43
44
  setGlobalVersionHint(versionHint);
44
45
  const client = new MantisClient(async () => {
45
46
  const config = await getConfig();
46
- return { baseUrl: config.baseUrl, apiKey: config.apiKey };
47
+ return {
48
+ baseUrl: config.baseUrl,
49
+ apiKey: config.apiKey,
50
+ useIndexPhp: config.useIndexPhp,
51
+ };
47
52
  }, (response) => versionHint.onSuccessfulResponse(response));
48
53
  const cache = new MetadataCache(startupConfig.cacheDir, startupConfig.cacheTtl);
49
54
  const server = new McpServer({
@@ -56,6 +61,7 @@ async function createMcpServer(transport) {
56
61
  registerRelationshipTools(server, client);
57
62
  registerMonitorTools(server, client);
58
63
  registerProjectTools(server, client, cache);
64
+ registerProjectVersionTools(server, client, cache);
59
65
  registerUserTools(server, client);
60
66
  registerFilterTools(server, client);
61
67
  registerConfigTools(server, client, cache);
@@ -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