@dpesch/mantisbt-mcp-server 1.18.1 → 1.18.3

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.18.3] – 2026-11-12
15
+
16
+ ### Documentation
17
+
18
+ - mcprush directory badge added to README.md and README.de.md
19
+ - New "Security considerations" section in the README (#8636): issue and note texts are untrusted input that a prompt injection can use to drive write tools. `MANTIS_UPLOAD_DIR` is now recommended in stdio mode as well and appears in the installation examples, because without it `upload_file`'s `file_path` can read any file the server process can read. The section also points to `MANTIS_TOOLSETS`, `MANTIS_DISABLE_DESTRUCTIVE` and the tool annotations.
20
+
21
+ ### Deprecated
22
+
23
+ - `upload_file`'s `file_path` without `MANTIS_UPLOAD_DIR` in stdio mode: starting with 2.0.0, `file_path` is disabled in stdio mode too unless `MANTIS_UPLOAD_DIR` is set (#8637). Uploads via `content` (Base64) are not affected.
24
+
25
+ ### Security
26
+
27
+ - Updated dependencies with known vulnerabilities: `@modelcontextprotocol/sdk` 1.32.2 and `zod` 4.6.7, and through the SDK `proxy-addr` 2.0.8, `body-parser`, `hono`, `@hono/node-server`, `fast-uri`, `ip-address` and `qs`. `npm audit` reports no vulnerabilities.
28
+
29
+ ---
30
+
31
+ ## [1.18.2] – 2026-11-11
32
+
33
+ ### Added
34
+
35
+ - `MCP_HTTP_MAX_BODY_SIZE` sets the largest accepted `/mcp` request body in bytes, default 64 MB (#8631). An empty value counts as unset. In HTTP mode, any other value that is not a positive whole number stops the server at startup; stdio ignores it.
36
+
37
+ ### Security
38
+
39
+ - `search_issues` with `select`: when MantisBT refused an issue (401, 403) or no longer had it (404), the result still came back with `highlights` taken from the local search index — text of an issue the API user may no longer read (#8617). Such results are now left out. Other errors (network, 5xx) keep the previous fallback to the indexed text.
40
+ - `upload_file` with `MANTIS_UPLOAD_DIR`: the directory check looked only at the path as written, while the read followed symbolic links (#8618). A link inside the upload directory could therefore upload a file from anywhere the server can read. The check now resolves links and compares the real target, and the server reads that target. A link swapped in between check and read is still not caught — keep the upload directory unwritable for less trusted users.
41
+ - `MANTIS_DISABLE_DESTRUCTIVE=true` hid `remove_monitor`, but `update_issue` could still remove monitors through `remove_monitors` (#8621). `update_issue` now rejects `remove_monitors` in that mode, before anything is changed; `add_monitors` keeps working.
42
+ - `update_issue` with `fields.notes`: the check for repeated note IDs took quadratic time and ran before any access check, so a request with many note edits could block the server (#8622). It now takes a single pass, and each repeated ID appears once in the error message.
43
+ - `search_issues`: the exact-match ranking scanned issue texts once per identifier in the query, with no limit, so a long query full of identifiers could block the server (#8623). It now looks at no more than 32 identifiers, checks no more than 10,000 issue texts per query and starts with the rarest identifier. The limits sit well above what real queries need, so their results do not change.
44
+ - HTTP transport: `POST /mcp` read the whole request body into memory, however large (#8624). Bodies above 64 MB are now refused with `413 Payload Too Large` and the connection is closed. The limit leaves room for `upload_file` with `content`: files up to about 48 MB still fit as Base64.
45
+ - Metadata cache and search index were shared by every configuration using the same directory, so one account could see projects, members and issues cached for another (#8625). Both now live in a subdirectory per base URL and API key (host name plus hash, never the key itself), also under an explicit `MANTIS_SEARCH_DIR`; see the README. The server logs the paths on startup. **The search index is rebuilt once after the update**, and again after an API key change. Old files directly in `MANTIS_CACHE_DIR` or `MANTIS_SEARCH_DIR` are no longer read: delete them, or move them into the new subdirectory to skip the rebuild if only one configuration ever used them. After a key change, the folder of the old key can be deleted as well.
46
+ - Search index: the incremental sync only added and updated issues, so issues deleted in MantisBT or no longer visible to the account stayed in the index for good, and `search_issues` kept returning them (#8626). A sync over all projects now lists the issue IDs the account can see (about 1 s for 7,800 issues) and removes the others; `rebuild_search_index` reports them as `removed`. A sync limited to `project_id` removes nothing, and neither does a listing that does not finish.
47
+
48
+ ---
49
+
14
50
  ## [1.18.1] – 2026-11-10
15
51
 
16
52
  ### Fixed
package/README.de.md CHANGED
@@ -4,6 +4,7 @@
4
4
  [![license](https://img.shields.io/npm/l/@dpesch/mantisbt-mcp-server)](LICENSE)
5
5
  [![MCP compatible](https://img.shields.io/badge/MCP-compatible-green.svg?style=flat-square)](https://modelcontextprotocol.io/)
6
6
  [![MCP Badge](https://lobehub.com/badge/mcp/dpesch-mantisbt-mcp-server)](https://lobehub.com/mcp/dpesch-mantisbt-mcp-server)
7
+ [![Mantisbt MCP on mcprush](https://mcprush.com/badge/mantisbt-mcp/score.svg)](https://mcprush.com/to/mantisbt-mcp?ref=badge)
7
8
  [![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
9
 
9
10
  [English](README.md) · **Deutsch**
@@ -31,7 +32,8 @@ In `~/.claude/claude_desktop_config.json` (Claude Desktop) oder der lokalen
31
32
  "args": ["-y", "@dpesch/mantisbt-mcp-server"],
32
33
  "env": {
33
34
  "MANTIS_BASE_URL": "https://deine-mantis-instanz.example.com/api/rest",
34
- "MANTIS_API_KEY": "dein-api-token"
35
+ "MANTIS_API_KEY": "dein-api-token",
36
+ "MANTIS_UPLOAD_DIR": "/pfad/zu/mantis-uploads"
35
37
  }
36
38
  }
37
39
  }
@@ -55,13 +57,16 @@ npm run build
55
57
  "args": ["/pfad/zum/mantisbt-mcp-server/dist/index.js"],
56
58
  "env": {
57
59
  "MANTIS_BASE_URL": "https://deine-mantis-instanz.example.com/api/rest",
58
- "MANTIS_API_KEY": "dein-api-token"
60
+ "MANTIS_API_KEY": "dein-api-token",
61
+ "MANTIS_UPLOAD_DIR": "/pfad/zu/mantis-uploads"
59
62
  }
60
63
  }
61
64
  }
62
65
  }
63
66
  ```
64
67
 
68
+ `MANTIS_UPLOAD_DIR` ist optional, wird aber **auch im stdio-Modus empfohlen**: Die Variable beschränkt `upload_file` auf Dateien in diesem Verzeichnis, sodass eine Prompt Injection im Tickettext das Modell nicht dazu bringen kann, beliebige lokale Dateien anzuhängen. Ohne sie kann `file_path` jede Datei lesen, die der Server-Prozess lesen darf. Ab 2.0.0 ist `file_path` deaktiviert, solange die Variable nicht gesetzt ist. Siehe [Sicherheitshinweise](#sicherheitshinweise).
69
+
65
70
  ## Konfiguration
66
71
 
67
72
  ### Umgebungsvariablen
@@ -71,20 +76,21 @@ npm run build
71
76
  | `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
77
  | `MANTIS_API_KEY` | ✅ | – | API-Token für die Authentifizierung |
73
78
  | `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
- | `MANTIS_CACHE_DIR` | – | `~/.cache/mantisbt-mcp` | Verzeichnis für den Metadaten-Cache |
79
+ | `MANTIS_CACHE_DIR` | – | `~/.cache/mantisbt-mcp` | Basisverzeichnis für den Metadaten-Cache. Jede MantisBT-Instanz und jedes Konto (Basis-URL plus API-Key) bekommt ein eigenes Unterverzeichnis aus Hostname und Hash; der Server gibt dessen Pfad beim Start aus |
75
80
  | `MANTIS_CACHE_TTL` | – | `3600` | Cache-Lebensdauer in Sekunden |
76
81
  | `TRANSPORT` | – | `stdio` | Transport-Modus: `stdio` oder `http` |
77
82
  | `PORT` | – | `3000` | Port für HTTP-Modus |
78
83
  | `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
84
  | `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. |
85
+ | `MCP_HTTP_MAX_BODY_SIZE` | – | `67108864` (64 MB) | Größter angenommener Request-Body für `/mcp` in Bytes; größere bekommen `413 Payload Too Large`. Leer oder nicht gesetzt gilt der Standardwert; im HTTP-Modus muss jeder andere Wert eine positive ganze Zahl sein, sonst startet der Server nicht. Im stdio-Modus ohne Bedeutung. |
80
86
  | `MANTIS_SEARCH_ENABLED` | – | `false` | Auf `true` setzen, um die semantische Suche zu aktivieren |
81
87
  | `MANTIS_SEARCH_BACKEND` | – | `vectra` | Vektorspeicher: `vectra` (reines JS) oder `sqlite-vec` (manuelle Installation erforderlich) |
82
- | `MANTIS_SEARCH_DIR` | – | `{MANTIS_CACHE_DIR}/search` | Verzeichnis für den Suchindex |
88
+ | `MANTIS_SEARCH_DIR` | – | `{MANTIS_CACHE_DIR}/<instanz>/search` | Basisverzeichnis für den Suchindex; auch ein ausdrücklich gesetzter Wert bekommt ein Unterverzeichnis pro Instanz und Konto (`{MANTIS_SEARCH_DIR}/<instanz>`) |
83
89
  | `MANTIS_SEARCH_MODEL` | – | `Xenova/paraphrase-multilingual-MiniLM-L12-v2` | Embedding-Modell (wird beim ersten Start einmalig heruntergeladen, ~80 MB). Nach einem Wechsel baut der nächste Sync den Index von selbst neu auf. |
84
90
  | `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. Auf Rechnern mit 4 oder mehr Kernen ist `4` ein guter Wert: Ein vollständiger Neuaufbau von 7.700 Issues dauerte damit 160 s statt 377 s mit 1 Thread. Die Einstellung beschleunigt vor allem das Indexieren; eine Suchanfrage ist ein einzelner kurzer Text und profitiert kaum. Wirksam ab 1.16.0 – frühere Versionen haben die Einstellung ignoriert und immer alle Kerne genutzt. |
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. |
91
+ | `MANTIS_UPLOAD_DIR` | – | – | Schränkt den `file_path` von `upload_file` auf dieses Verzeichnis ein (Pfad-Traversal via `../` und symbolische Links, die nach außen führen, werden blockiert; das Verzeichnis sollte für weniger vertrauenswürdige Nutzer nicht beschreibbar sein, denn ein Link, der zwischen Prüfung und Lesen ausgetauscht wird, fällt nicht auf). Im **stdio-Modus** ist `file_path` ohne diese Variable unbeschränkt – **das Setzen wird auch im stdio-Modus empfohlen** (siehe [Sicherheitshinweise](#sicherheitshinweise)); ab 2.0.0 ist `file_path` auch im stdio-Modus deaktiviert, solange sie nicht gesetzt ist. 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
92
  | `MANTIS_TOOLSETS` | – | – (alle) | Kommagetrennte Liste der anzubietenden Tool-Gruppen: `issues`, `notes`, `links`, `files`, `projects`, `versions`, `meta`, `search` (Groß-/Kleinschreibung egal). Nicht gesetzt oder leer bedeutet alle Gruppen. Die Gruppe `search` erfordert zusätzlich `MANTIS_SEARCH_ENABLED=true`; ohne die Gruppe wird das Such-Modul gar nicht gestartet. Ein unbekannter Gruppenname stoppt den Server beim Start mit einer Meldung, die die gültigen Namen auflistet. Siehe [Angebotene Tools einschränken](#angebotene-tools-einschränken) und das [Cookbook](docs/cookbook.de.md#nur-bestimmte-tool-gruppen-anbieten). |
87
- | `MANTIS_DISABLE_DESTRUCTIVE` | – | `false` | Auf `true` setzen, um alle destruktiven Tools auszublenden – `delete_issue`, `delete_note`, `delete_version`, `remove_relationship`, `remove_monitor` – unabhängig von den aktiven Gruppen. Ausgeblendete Tools werden nicht aufgelistet und liefern »Tool … not found«, wenn sie aufgerufen werden. |
93
+ | `MANTIS_DISABLE_DESTRUCTIVE` | – | `false` | Auf `true` setzen, um alle destruktiven Tools auszublenden – `delete_issue`, `delete_note`, `delete_version`, `remove_relationship`, `remove_monitor` – unabhängig von den aktiven Gruppen. Ausgeblendete Tools werden nicht aufgelistet und liefern »Tool … not found«, wenn sie aufgerufen werden. `update_issue` lehnt dann auch `remove_monitors` ab. |
88
94
 
89
95
  ### Angebotene Tools einschränken
90
96
 
@@ -321,10 +327,18 @@ MCP_HTTP_TOKEN=secret MANTIS_BASE_URL=... MANTIS_API_KEY=... \
321
327
  # TRANSPORT=http PORT=3456 MCP_HTTP_HOST=0.0.0.0 node dist/index.js
322
328
  ```
323
329
 
324
- 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.
330
+ 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. Ein Request-Body darf standardmäßig höchstens 64 MB groß sein (`MCP_HTTP_MAX_BODY_SIZE`) – größere bekommen `413 Payload Too Large` –, über `content` passen also Dateien bis etwa 48 MB.
325
331
 
326
332
  Healthcheck: `GET http://localhost:3456/health` (immer öffentlich, kein Token erforderlich)
327
333
 
334
+ ## Sicherheitshinweise
335
+
336
+ Ticketbeschreibungen, Notizen und Anhänge schreibt, wer in deinem MantisBT melden oder kommentieren darf – und das Modell liest sie. Behandle sie als **nicht vertrauenswürdige Eingaben**: Eine in einem Ticket versteckte Prompt Injection kann versuchen, das Modell zu schreibenden Tools wie `create_issue`, `update_issue` oder `upload_file` zu bewegen.
337
+
338
+ - **`MANTIS_UPLOAD_DIR` setzen – auch im stdio-Modus.** Ohne die Variable liest `upload_file` per `file_path` jede lokale Datei, die der Server-Prozess lesen darf. Eine eingeschleuste Anweisung könnte das Modell dazu bringen, etwa einen privaten SSH-Schlüssel an ein Ticket anzuhängen – sichtbar für alle, die das Ticket lesen können. Mit `MANTIS_UPLOAD_DIR` ist `file_path` auf dieses Verzeichnis beschränkt. Über HTTP ist `file_path` ohne die Variable bereits deaktiviert; ab 2.0.0 gilt das auch für stdio.
339
+ - **Nur die nötigen Tools anbieten.** `MANTIS_TOOLSETS` beschränkt die angebotenen Gruppen – `files` weglassen, wenn nie hochgeladen wird, oder für eine überwiegend lesende Triage nur `issues,meta` verwenden. `MANTIS_DISABLE_DESTRUCTIVE=true` blendet alle Lösch- und Entfern-Tools aus. Beides lohnt sich besonders dort, wo auch Personen außerhalb des eigenen Teams Tickets schreiben. Siehe [Angebotene Tools einschränken](#angebotene-tools-einschränken).
340
+ - **Schreibende Aufrufe vom Client bestätigen lassen.** Alle Tools tragen MCP-Annotationen (`readOnlyHint`, `destructiveHint`), sodass Clients, die sie unterstützen, vor ändernden Aufrufen nachfragen können.
341
+
328
342
  ## Dokumentation
329
343
 
330
344
  - [**Cookbook**](docs/cookbook.de.md) – Tool-orientierte Rezepte mit direkt verwendbaren Parameter-Beispielen für alle registrierten Tools
@@ -341,7 +355,7 @@ npm test # Tests ausführen (vitest)
341
355
  npm run test:watch # Tests im Watch-Modus
342
356
  npm run test:coverage # Coverage-Report
343
357
  npm run benchmark # Token-Benchmark gegen eine Live-MantisBT-Instanz (siehe Token-Verbrauch oben)
344
- npm run benchmark:search # Such-Trefferquote und Exakt-Treffer-Reihenfolge auf einem bestehenden Index (MANTIS_SEARCH_DIR), Vektor vs. hybrid
358
+ npm run benchmark:search # Such-Trefferquote und Exakt-Treffer-Reihenfolge auf einem bestehenden Index (gefunden wie vom Server: MANTIS_SEARCH_DIR, MANTIS_BASE_URL, MANTIS_API_KEY), Vektor vs. hybrid
345
359
  ```
346
360
 
347
361
  `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`.
package/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
  [![license](https://img.shields.io/npm/l/@dpesch/mantisbt-mcp-server)](LICENSE)
5
5
  [![MCP compatible](https://img.shields.io/badge/MCP-compatible-green.svg?style=flat-square)](https://modelcontextprotocol.io/)
6
6
  [![MCP Badge](https://lobehub.com/badge/mcp/dpesch-mantisbt-mcp-server)](https://lobehub.com/mcp/dpesch-mantisbt-mcp-server)
7
+ [![Mantisbt MCP on mcprush](https://mcprush.com/badge/mantisbt-mcp/score.svg)](https://mcprush.com/to/mantisbt-mcp?ref=badge)
7
8
  [![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
9
 
9
10
  **English** · [Deutsch](README.de.md)
@@ -31,7 +32,8 @@ Add to `~/.claude/claude_desktop_config.json` (Claude Desktop) or your local
31
32
  "args": ["-y", "@dpesch/mantisbt-mcp-server"],
32
33
  "env": {
33
34
  "MANTIS_BASE_URL": "https://your-mantis.example.com/api/rest",
34
- "MANTIS_API_KEY": "your-api-token"
35
+ "MANTIS_API_KEY": "your-api-token",
36
+ "MANTIS_UPLOAD_DIR": "/path/to/mantis-uploads"
35
37
  }
36
38
  }
37
39
  }
@@ -55,13 +57,16 @@ npm run build
55
57
  "args": ["/path/to/mantisbt-mcp-server/dist/index.js"],
56
58
  "env": {
57
59
  "MANTIS_BASE_URL": "https://your-mantis.example.com/api/rest",
58
- "MANTIS_API_KEY": "your-api-token"
60
+ "MANTIS_API_KEY": "your-api-token",
61
+ "MANTIS_UPLOAD_DIR": "/path/to/mantis-uploads"
59
62
  }
60
63
  }
61
64
  }
62
65
  }
63
66
  ```
64
67
 
68
+ `MANTIS_UPLOAD_DIR` is optional, but **recommended in stdio mode as well**: it limits `upload_file` to files in that directory, so a prompt injection in issue text cannot make the model attach arbitrary local files. Without it, `file_path` can read any file the server process can read. Starting with 2.0.0, `file_path` will be disabled unless the variable is set. See [Security considerations](#security-considerations).
69
+
65
70
  ## Configuration
66
71
 
67
72
  ### Environment variables
@@ -71,20 +76,21 @@ npm run build
71
76
  | `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. |
72
77
  | `MANTIS_API_KEY` | ✅ | – | API token for authentication |
73
78
  | `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). |
74
- | `MANTIS_CACHE_DIR` | – | `~/.cache/mantisbt-mcp` | Directory for the metadata cache |
79
+ | `MANTIS_CACHE_DIR` | – | `~/.cache/mantisbt-mcp` | Base directory for the metadata cache. Each MantisBT instance and account (base URL plus API key) gets a subdirectory of its own, named after the host plus a hash; the server logs its path on startup |
75
80
  | `MANTIS_CACHE_TTL` | – | `3600` | Cache lifetime in seconds |
76
81
  | `TRANSPORT` | – | `stdio` | Transport mode: `stdio` or `http` |
77
82
  | `PORT` | – | `3000` | Port for HTTP mode |
78
83
  | `MCP_HTTP_HOST` | – | `127.0.0.1` | Bind address for HTTP mode. **Changed from `0.0.0.0` to `127.0.0.1`** — the server now listens on localhost only by default. Set to `0.0.0.0` for Docker or remote access. |
79
84
  | `MCP_HTTP_TOKEN` | ✅ (HTTP mode) | – | Bearer token for the `/mcp` endpoint (`Authorization: Bearer <token>`). **Required when `TRANSPORT=http`** — the server refuses to start in HTTP mode without it, so tools are never exposed unauthenticated. Ignored in stdio mode. The `/health` endpoint is always public. |
85
+ | `MCP_HTTP_MAX_BODY_SIZE` | – | `67108864` (64 MB) | Largest accepted `/mcp` request body in bytes; larger ones get `413 Payload Too Large`. Empty or unset means the default; in HTTP mode any other value must be a positive whole number, otherwise the server does not start. Ignored in stdio mode. |
80
86
  | `MANTIS_SEARCH_ENABLED` | – | `false` | Set to `true` to enable semantic search |
81
87
  | `MANTIS_SEARCH_BACKEND` | – | `vectra` | Vector store backend: `vectra` (pure JS) or `sqlite-vec` (requires manual install) |
82
- | `MANTIS_SEARCH_DIR` | – | `{MANTIS_CACHE_DIR}/search` | Directory for the search index |
88
+ | `MANTIS_SEARCH_DIR` | – | `{MANTIS_CACHE_DIR}/<instance>/search` | Base directory for the search index; an explicit value also gets a subdirectory per instance and account (`{MANTIS_SEARCH_DIR}/<instance>`) |
83
89
  | `MANTIS_SEARCH_MODEL` | – | `Xenova/paraphrase-multilingual-MiniLM-L12-v2` | Embedding model name (downloaded once on first use, ~80 MB). After a change the index is rebuilt automatically on the next sync. |
84
90
  | `MANTIS_SEARCH_THREADS` | – | `1` | Number of ONNX intra-op threads for the embedding model. Default is 1 to prevent CPU saturation on multi-core machines and WSL. On a machine with 4 or more cores, `4` is a good value: a full rebuild of 7,700 issues took 160 s instead of 377 s with 1 thread. The setting mainly speeds up indexing; a search query is a single short text and hardly benefits. Effective since 1.16.0 — earlier versions ignored this setting and always used every core. |
85
- | `MANTIS_UPLOAD_DIR` | – | – | Restrict `upload_file`'s `file_path` to files within this directory (path traversal via `../` is blocked). In **stdio mode** `file_path` is unrestricted unless this is set. In **HTTP mode** `file_path` reads from the server's filesystem, so it is **disabled unless this variable is set** — HTTP clients should upload via the `content` (Base64) parameter instead. |
91
+ | `MANTIS_UPLOAD_DIR` | – | – | Restrict `upload_file`'s `file_path` to files within this directory (path traversal via `../` and symbolic links that lead outside are blocked; keep the directory unwritable for less trusted users, as a link swapped in between check and read is not caught). In **stdio mode** `file_path` is unrestricted unless this is set — **setting it is recommended in stdio mode as well** (see [Security considerations](#security-considerations)); starting with 2.0.0, `file_path` will be disabled in stdio mode too unless it is set. In **HTTP mode** `file_path` reads from the server's filesystem, so it is **disabled unless this variable is set** — HTTP clients should upload via the `content` (Base64) parameter instead. |
86
92
  | `MANTIS_TOOLSETS` | – | – (all) | Comma-separated list of tool groups to offer: `issues`, `notes`, `links`, `files`, `projects`, `versions`, `meta`, `search` (case-insensitive). Unset or empty means all groups. The `search` group additionally requires `MANTIS_SEARCH_ENABLED=true`; without the group the search module is not started at all. An unknown group name stops the server at startup with a message listing the valid names. See [Limit the offered tools](#limit-the-offered-tools) and the [cookbook](docs/cookbook.md#offer-only-some-tool-groups). |
87
- | `MANTIS_DISABLE_DESTRUCTIVE` | – | `false` | Set to `true` to hide every destructive tool — `delete_issue`, `delete_note`, `delete_version`, `remove_relationship`, `remove_monitor` — regardless of the active groups. Hidden tools are not listed and return "Tool … not found" when called. |
93
+ | `MANTIS_DISABLE_DESTRUCTIVE` | – | `false` | Set to `true` to hide every destructive tool — `delete_issue`, `delete_note`, `delete_version`, `remove_relationship`, `remove_monitor` — regardless of the active groups. Hidden tools are not listed and return "Tool … not found" when called. `update_issue` then also rejects `remove_monitors`. |
88
94
 
89
95
  ### Limit the offered tools
90
96
 
@@ -321,10 +327,18 @@ MCP_HTTP_TOKEN=secret MANTIS_BASE_URL=... MANTIS_API_KEY=... \
321
327
  # TRANSPORT=http PORT=3456 MCP_HTTP_HOST=0.0.0.0 node dist/index.js
322
328
  ```
323
329
 
324
- Every `/mcp` request must send `Authorization: Bearer <token>`. Over HTTP, `upload_file`'s `file_path` is disabled unless `MANTIS_UPLOAD_DIR` is set — use the `content` (Base64) parameter instead.
330
+ Every `/mcp` request must send `Authorization: Bearer <token>`. Over HTTP, `upload_file`'s `file_path` is disabled unless `MANTIS_UPLOAD_DIR` is set — use the `content` (Base64) parameter instead. A request body may be at most 64 MB by default (`MCP_HTTP_MAX_BODY_SIZE`) — larger ones get `413 Payload Too Large` — so `content` fits files up to about 48 MB.
325
331
 
326
332
  Health check: `GET http://localhost:3456/health` (always public, no token required)
327
333
 
334
+ ## Security considerations
335
+
336
+ Issue descriptions, notes and attachments are written by whoever can report or comment in your MantisBT — and the model reads them. Treat them as **untrusted input**: a prompt injection hidden in an issue can try to make the model call write tools such as `create_issue`, `update_issue` or `upload_file`.
337
+
338
+ - **Set `MANTIS_UPLOAD_DIR` — in stdio mode as well.** Without it, `upload_file` reads any local file the server process can read via `file_path`. An injected instruction could get the model to attach, for example, a private SSH key to an issue, where everyone who can read the issue sees it. With `MANTIS_UPLOAD_DIR`, `file_path` is limited to that directory. Over HTTP, `file_path` is already disabled without it; starting with 2.0.0 the same applies to stdio.
339
+ - **Offer only the tools you need.** `MANTIS_TOOLSETS` limits the offered groups — leave out `files` if you never upload, or use only `issues,meta` for read-mostly triage. `MANTIS_DISABLE_DESTRUCTIVE=true` hides every delete and remove tool. Both are especially useful where people outside your team can write issues. See [Limit the offered tools](#limit-the-offered-tools).
340
+ - **Let your client confirm write calls.** Every tool carries MCP annotations (`readOnlyHint`, `destructiveHint`), so clients that support them can ask before a tool changes data.
341
+
328
342
  ## Documentation
329
343
 
330
344
  - [**Cookbook**](docs/cookbook.md) — tool-oriented recipes with copy-paste-ready parameter examples for all registered tools
@@ -341,7 +355,7 @@ npm test # Run tests (vitest)
341
355
  npm run test:watch # Run tests in watch mode
342
356
  npm run test:coverage # Coverage report
343
357
  npm run benchmark # Token benchmark against a live MantisBT (see Token usage above)
344
- npm run benchmark:search # Search recall and exact-first order on an existing index (MANTIS_SEARCH_DIR), vectors vs. hybrid
358
+ npm run benchmark:search # Search recall and exact-first order on an existing index (found like the server does: MANTIS_SEARCH_DIR, MANTIS_BASE_URL, MANTIS_API_KEY), vectors vs. hybrid
345
359
  ```
346
360
 
347
361
  `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`.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.18.1",
3
- "commit": "53e8d2f",
4
- "built_at": "2026-11-10T20:19:47.616Z"
2
+ "version": "1.18.3",
3
+ "commit": "759969d",
4
+ "built_at": "2026-11-12T11:13:36.433Z"
5
5
  }
package/dist/config.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { readFile } from 'node:fs/promises';
2
3
  import { homedir } from 'node:os';
3
4
  import { join, dirname } from 'node:path';
@@ -41,9 +42,25 @@ async function readDotEnvLocal() {
41
42
  // Non-credential config (safe to read at startup without credentials)
42
43
  // ---------------------------------------------------------------------------
43
44
  export const DEFAULT_SEARCH_MODEL = 'Xenova/paraphrase-multilingual-MiniLM-L12-v2';
45
+ /**
46
+ * Subdirectory of the cache and search directories for one MantisBT instance
47
+ * and account (#8625), so configurations sharing a directory never read each
48
+ * other's data. The host keeps it recognisable; the hash covers URL and API
49
+ * key, never the key itself. Without credentials: an empty folder of its own.
50
+ */
51
+ export function instanceDirName(baseUrl, apiKey) {
52
+ if (!baseUrl || !apiKey)
53
+ return 'unconfigured';
54
+ const url = normalizeBaseUrl(baseUrl);
55
+ const hash = createHash('sha256').update(`${url}\n${apiKey}`).digest('hex').slice(0, 16);
56
+ // The regex also drops the [ ] : of IPv6 hosts, not allowed in Windows paths.
57
+ const host = URL.canParse(url) ? new URL(url).hostname.replace(/[^a-z0-9.-]/gi, '') : '';
58
+ return host ? `${host}-${hash}` : hash;
59
+ }
44
60
  function readNonCredentialConfig() {
45
61
  const defaultCacheDir = join(homedir(), '.cache', 'mantisbt-mcp');
46
- const cacheDir = process.env.MANTIS_CACHE_DIR ?? defaultCacheDir;
62
+ const instance = instanceDirName(process.env.MANTIS_BASE_URL, process.env.MANTIS_API_KEY);
63
+ const cacheDir = join(process.env.MANTIS_CACHE_DIR ?? defaultCacheDir, instance);
47
64
  const cacheTtl = process.env.MANTIS_CACHE_TTL
48
65
  ? parseInt(process.env.MANTIS_CACHE_TTL, 10)
49
66
  : 3600;
@@ -53,7 +70,9 @@ function readNonCredentialConfig() {
53
70
  process.stderr.write(`[mantisbt-config] Unknown MANTIS_SEARCH_BACKEND="${searchBackendRaw}", falling back to "vectra"\n`);
54
71
  }
55
72
  const searchBackend = searchBackendRaw === 'sqlite-vec' ? 'sqlite-vec' : 'vectra';
56
- const searchDir = process.env.MANTIS_SEARCH_DIR ?? join(cacheDir, 'search');
73
+ const searchDir = process.env.MANTIS_SEARCH_DIR
74
+ ? join(process.env.MANTIS_SEARCH_DIR, instance)
75
+ : join(cacheDir, 'search');
57
76
  const searchModelName = process.env.MANTIS_SEARCH_MODEL ?? DEFAULT_SEARCH_MODEL;
58
77
  const searchNumThreads = Math.max(1, parseInt(process.env.MANTIS_SEARCH_THREADS ?? '', 10) || 1);
59
78
  return {
@@ -63,6 +82,7 @@ function readNonCredentialConfig() {
63
82
  httpHost: process.env.MCP_HTTP_HOST ?? '127.0.0.1',
64
83
  httpPort: parseInt(process.env.PORT ?? '3000', 10),
65
84
  httpToken: process.env.MCP_HTTP_TOKEN,
85
+ httpMaxBodySize: process.env.MCP_HTTP_MAX_BODY_SIZE,
66
86
  search: {
67
87
  enabled: searchEnabled,
68
88
  backend: searchBackend,
@@ -99,7 +119,8 @@ function resolveUseIndexPhp(rawBaseUrl) {
99
119
  * Returns all non-credential config values. Never throws, even when
100
120
  * MANTIS_BASE_URL / MANTIS_API_KEY are absent. Use this at server startup
101
121
  * so the MCP transport can connect and respond to tools/list without
102
- * requiring credentials to be configured.
122
+ * requiring credentials to be configured. The credentials, when present,
123
+ * still pick the instance subdirectory of the cache and search paths.
103
124
  */
104
125
  export async function getStartupConfig() {
105
126
  await loadDotEnvLocal();
@@ -0,0 +1,63 @@
1
+ // Default upper bound for a POST /mcp body (#8624). Generous because
2
+ // upload_file sends files as Base64 in `content`, a third larger than the
3
+ // file itself.
4
+ export const MAX_REQUEST_BODY_BYTES = 64 * 1024 * 1024;
5
+ /**
6
+ * MCP_HTTP_MAX_BODY_SIZE in bytes (#8631); the default when unset. Anything
7
+ * but a positive whole number throws — silently falling back to the default
8
+ * would hide the typo until an upload fails. Called in HTTP mode only, so a
9
+ * stray value never stops the stdio server.
10
+ */
11
+ export function parseMaxBodySize(raw) {
12
+ const value = raw?.trim();
13
+ if (!value)
14
+ return MAX_REQUEST_BODY_BYTES;
15
+ const bytes = Number(value);
16
+ if (!Number.isSafeInteger(bytes) || bytes <= 0) {
17
+ throw new Error(`MCP_HTTP_MAX_BODY_SIZE must be a positive whole number of bytes, got "${raw}".`);
18
+ }
19
+ return bytes;
20
+ }
21
+ /**
22
+ * Reads the request body, at most `limit` bytes. A larger body — announced
23
+ * by Content-Length or found while reading — gets 413 and the connection is
24
+ * closed. Resolves undefined then, or when the client aborts; the caller must
25
+ * not respond in either case.
26
+ *
27
+ * The SDK transport has its own limit (maxRequestBodySize), but it reads the
28
+ * body inside handleRequest — in runHttp that runs in the serialized request
29
+ * queue, where one slow upload would stall every other request. Reading here,
30
+ * before the queue, avoids that; the 413 matches the SDK's JSON-RPC error.
31
+ */
32
+ export function readBody(req, res, limit) {
33
+ return new Promise(resolve => {
34
+ const tooLarge = () => {
35
+ res.writeHead(413, { 'Content-Type': 'application/json', Connection: 'close' });
36
+ res.end(JSON.stringify({
37
+ jsonrpc: '2.0',
38
+ error: { code: -32000, message: `Request body exceeds ${limit} bytes` },
39
+ id: null,
40
+ }));
41
+ resolve(undefined);
42
+ };
43
+ if (Number(req.headers['content-length']) > limit) {
44
+ tooLarge();
45
+ return;
46
+ }
47
+ let chunks = [];
48
+ let size = 0;
49
+ req.on('data', (chunk) => {
50
+ if (!chunks)
51
+ return; // over the limit: drop the rest until the socket closes
52
+ size += chunk.length;
53
+ if (size <= limit) {
54
+ chunks.push(chunk);
55
+ return;
56
+ }
57
+ chunks = undefined;
58
+ tooLarge();
59
+ });
60
+ req.on('end', () => resolve(chunks && Buffer.concat(chunks)));
61
+ req.on('error', () => resolve(undefined));
62
+ });
63
+ }
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { createServer } from 'node:http';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { dirname } from 'node:path';
8
8
  import { loadBuildInfo } from './build-info.js';
9
+ import { parseMaxBodySize, readBody } from './http-body.js';
9
10
  import { getConfig, getStartupConfig, assertHttpAuthConfigured } from './config.js';
10
11
  import { MantisClient } from './client.js';
11
12
  import { VersionHintService, setGlobalVersionHint } from './version-hint.js';
@@ -38,6 +39,9 @@ async function createMcpServer(transport) {
38
39
  };
39
40
  }, (response) => versionHint.onSuccessfulResponse(response));
40
41
  const cache = new MetadataCache(startupConfig.cacheDir, startupConfig.cacheTtl);
42
+ console.error(`[mantisbt-config] cache directory: ${startupConfig.cacheDir}`);
43
+ if (startupConfig.search.enabled)
44
+ console.error(`[mantisbt-config] search index: ${startupConfig.search.dir}`);
41
45
  const server = new McpServer({
42
46
  name: 'mantisbt-mcp-server',
43
47
  version,
@@ -58,6 +62,7 @@ async function createMcpServer(transport) {
58
62
  build,
59
63
  uploadDir: startupConfig.uploadDir,
60
64
  transport,
65
+ disableDestructive: startupConfig.disableDestructive,
61
66
  searchIndexStatus,
62
67
  });
63
68
  registerPrompts(server);
@@ -80,6 +85,7 @@ async function runStdio() {
80
85
  async function runHttp() {
81
86
  const startupConfig = await getStartupConfig();
82
87
  assertHttpAuthConfigured(startupConfig.httpToken);
88
+ const maxBodySize = parseMaxBodySize(startupConfig.httpMaxBodySize);
83
89
  const server = await createMcpServer('http');
84
90
  const port = startupConfig.httpPort;
85
91
  // Serialize stateless requests: each request waits for the previous transport
@@ -97,34 +103,42 @@ async function runHttp() {
97
103
  res.end(JSON.stringify({ error: 'Unauthorized' }));
98
104
  return;
99
105
  }
100
- const chunks = [];
101
- req.on('data', (chunk) => chunks.push(chunk));
102
- req.on('end', () => {
103
- const prev = requestQueue;
104
- let releaseLock;
105
- requestQueue = new Promise(resolve => { releaseLock = resolve; });
106
- void prev.then(async () => {
107
- try {
108
- const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
109
- const transport = new StreamableHTTPServerTransport({
110
- sessionIdGenerator: undefined,
111
- enableJsonResponse: true,
112
- });
113
- await server.connect(transport);
114
- await transport.handleRequest(req, res, body);
115
- }
116
- catch (err) {
117
- console.error('[HTTP handler error]', err instanceof Error ? err.stack : err);
118
- if (!res.headersSent) {
119
- res.writeHead(400, { 'Content-Type': 'application/json' });
120
- res.end(JSON.stringify({ error: 'Bad Request' }));
121
- }
122
- }
123
- finally {
124
- await server.close();
125
- releaseLock();
106
+ const raw = await readBody(req, res, maxBodySize);
107
+ if (raw === undefined)
108
+ return; // aborted, or 413 already sent
109
+ // Parsed before queueing, so the raw bytes are not held while waiting.
110
+ let body;
111
+ try {
112
+ body = JSON.parse(raw.toString('utf8'));
113
+ }
114
+ catch {
115
+ res.writeHead(400, { 'Content-Type': 'application/json' });
116
+ res.end(JSON.stringify({ error: 'Bad Request' }));
117
+ return;
118
+ }
119
+ const prev = requestQueue;
120
+ let releaseLock;
121
+ requestQueue = new Promise(resolve => { releaseLock = resolve; });
122
+ void prev.then(async () => {
123
+ try {
124
+ const transport = new StreamableHTTPServerTransport({
125
+ sessionIdGenerator: undefined,
126
+ enableJsonResponse: true,
127
+ });
128
+ await server.connect(transport);
129
+ await transport.handleRequest(req, res, body);
130
+ }
131
+ catch (err) {
132
+ console.error('[HTTP handler error]', err instanceof Error ? err.stack : err);
133
+ if (!res.headersSent) {
134
+ res.writeHead(400, { 'Content-Type': 'application/json' });
135
+ res.end(JSON.stringify({ error: 'Bad Request' }));
126
136
  }
127
- });
137
+ }
138
+ finally {
139
+ await server.close();
140
+ releaseLock();
141
+ }
128
142
  });
129
143
  }
130
144
  else if (req.method === 'GET' && req.url === '/health') {
@@ -21,6 +21,12 @@ export function tokenize(text) {
21
21
  // common tokens such as UTF-8 from taking over a search. The rare identifiers
22
22
  // of the search benchmark occur in at most 8 issues, well below the cap.
23
23
  const MAX_EXACT_MATCHES = 20;
24
+ // The query is caller input of any length, and each identifier costs a text
25
+ // check per candidate issue (#8623). These caps bound that work per query.
26
+ // Measured on 7,800 issues: real queries name at most 22 identifiers and
27
+ // check at most 2,700 texts, so neither cap changes a real search.
28
+ export const MAX_QUERY_IDENTIFIERS = 32;
29
+ export const MAX_CHECKED_TEXTS = 10_000;
24
30
  // Words, or words joined by - . : / (CVE-2026-1234, 7.0.0-1014-azure).
25
31
  const COMPOUND = /[\p{L}\p{N}_]+(?:[-.:/][\p{L}\p{N}_]+)*/gu;
26
32
  function looksLikeIdentifier(word) {
@@ -132,24 +138,35 @@ export class LexicalIndex {
132
138
  * Per document id, how many identifiers of the query it contains as a whole
133
139
  * (#8545); documents without one are absent. Candidates come from the
134
140
  * postings of the identifier's rarest word, the text check decides.
141
+ *
142
+ * The MAX_QUERY_IDENTIFIERS cheapest identifiers, until MAX_CHECKED_TEXTS
143
+ * is spent (#8623). An identifier with more candidates than the budget left
144
+ * could count for no issue anyway — it either exceeds MAX_EXACT_MATCHES or
145
+ * runs out of budget — and the ones after it have at least as many.
135
146
  */
136
147
  exactMatches(query) {
137
148
  const counts = new Map();
138
149
  const frequency = (term) => this.offsets[term + 1] - this.offsets[term];
139
- for (const identifier of identifiers(query)) {
150
+ const lookups = identifiers(query).flatMap(identifier => {
140
151
  const terms = tokenize(identifier).map(word => this.termIds.get(word));
141
152
  if (terms.length === 0 || terms.some(term => term === undefined))
142
- continue;
153
+ return [];
143
154
  const rarest = terms.reduce((a, b) => (frequency(a) <= frequency(b) ? a : b));
155
+ return [{ identifier, start: this.offsets[rarest], end: this.offsets[rarest + 1] }];
156
+ }).sort((a, b) => (a.end - a.start) - (b.end - b.start)).slice(0, MAX_QUERY_IDENTIFIERS);
157
+ let budget = MAX_CHECKED_TEXTS;
158
+ for (const { identifier, start, end } of lookups) {
159
+ if (end - start > budget)
160
+ break;
144
161
  const pattern = wholeIdentifier(identifier);
145
162
  const matches = [];
146
- for (let at = this.offsets[rarest]; at < this.offsets[rarest + 1]; at++) {
163
+ let at = start;
164
+ for (; at < end && matches.length <= MAX_EXACT_MATCHES; at++) {
147
165
  const doc = this.documents[this.docs[at]];
148
166
  if (pattern.test(doc.summary) || pattern.test(doc.description ?? ''))
149
167
  matches.push(doc.id);
150
- if (matches.length > MAX_EXACT_MATCHES)
151
- break;
152
168
  }
169
+ budget -= at - start;
153
170
  if (matches.length > MAX_EXACT_MATCHES)
154
171
  continue;
155
172
  for (const id of matches)
@@ -126,9 +126,14 @@ export class VectraStore {
126
126
  await this.ensureLoaded();
127
127
  return this.items.get(id) ?? null;
128
128
  }
129
- async delete(id) {
129
+ async ids() {
130
130
  await this.ensureLoaded();
131
- this.items.delete(id);
131
+ return [...this.items.keys()];
132
+ }
133
+ async delete(...ids) {
134
+ await this.ensureLoaded();
135
+ for (const id of ids)
136
+ this.items.delete(id);
132
137
  this.lexical = null;
133
138
  await this.persist();
134
139
  }
@@ -5,6 +5,12 @@ import { acquireSyncLock } from './sync-lock.js';
5
5
  const PAGE_SIZE = 50;
6
6
  const EMBED_BATCH_SIZE = 10;
7
7
  const CHECKPOINT_INTERVAL = 100; // flush to disk every N indexed issues
8
+ // Listing of the issue IDs the account can see (#8626), with select=id. It
9
+ // ends at an empty page, not at a short one: a server that caps page_size
10
+ // would otherwise end it after the first page and empty the index. Past
11
+ // MAX_ID_PAGES it counts as incomplete, and nothing is removed.
12
+ const ID_PAGE_SIZE = 1000;
13
+ const MAX_ID_PAGES = 200;
8
14
  export class SyncLockedError extends Error {
9
15
  constructor() {
10
16
  super('A search index sync is already running — try again once it has finished');
@@ -98,8 +104,22 @@ export class SearchSyncService {
98
104
  indexedSinceCheckpoint = 0;
99
105
  }
100
106
  }
107
+ // Issues deleted in MantisBT, or no longer visible to this account, never
108
+ // show up as changed (#8626). A sync over all projects therefore drops
109
+ // every indexed issue the account cannot see any more. A project-scoped
110
+ // sync sees only part of the issues, and a cleared index holds no stale
111
+ // ones — both skip the listing.
112
+ let removed = 0;
113
+ if (scope === undefined && !full && !outdated) {
114
+ const visible = await this.fetchVisibleIds();
115
+ const stale = visible ? (await this.store.ids()).filter(id => !visible.has(id)) : [];
116
+ if (stale.length > 0) {
117
+ await this.store.delete(...stale); // also writes the items added above
118
+ removed = stale.length;
119
+ }
120
+ }
101
121
  // Final flush for any remaining items not yet written by a checkpoint.
102
- if (indexedSinceCheckpoint > 0) {
122
+ if (indexedSinceCheckpoint > 0 && removed === 0) {
103
123
  await this.store.flush();
104
124
  }
105
125
  // The watermark is the newest updated_at the server reported, not the local
@@ -118,7 +138,20 @@ export class SearchSyncService {
118
138
  const storeCount = await this.store.count();
119
139
  const total = totalFromApi ?? (lastSyncedAt === null ? allIssues.length : storeCount);
120
140
  await this.store.setLastKnownTotal(total);
121
- return { indexed, skipped, total };
141
+ return { indexed, skipped, removed, total };
142
+ }
143
+ /** IDs of all issues the account can see; null if the listing did not end. */
144
+ async fetchVisibleIds() {
145
+ const ids = new Set();
146
+ for (let page = 1; page <= MAX_ID_PAGES; page++) {
147
+ const response = await this.client.get('issues', { page_size: ID_PAGE_SIZE, page, select: 'id' });
148
+ const issues = response.issues ?? [];
149
+ if (issues.length === 0)
150
+ return ids;
151
+ for (const { id } of issues)
152
+ ids.add(id);
153
+ }
154
+ return null;
122
155
  }
123
156
  async fetchAllIssues(updatedAfter, projectId) {
124
157
  const changedIssues = [];
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { coerceBool } from '../coerce.js';
3
- import { buildIssueViewUrl } from '../client.js';
3
+ import { buildIssueViewUrl, MantisApiError } from '../client.js';
4
4
  import { SearchSyncService } from './sync.js';
5
5
  import { getVersionHint } from '../version-hint.js';
6
6
  import { firstIssue } from '../issue-envelope.js';
@@ -39,6 +39,8 @@ export async function getSearchIndexStatus(store) {
39
39
  const summary = total !== null ? `${indexed}/${total} (${percent} %)` : `${indexed}/? (total unknown)`;
40
40
  return { summary, indexed, total, percent, last_synced_at: lastSyncedAt };
41
41
  }
42
+ /** API answers that mean the issue is not (or no longer) visible to this account. */
43
+ const HIDDEN_ISSUE_STATUSES = new Set([401, 403, 404]);
42
44
  export function registerSearchTools(server, client, store, embedder, lockDir) {
43
45
  // ---------------------------------------------------------------------------
44
46
  // search_issues
@@ -63,7 +65,8 @@ export function registerSearchTools(server, client, store, embedder, lockDir) {
63
65
  .describe('Number of results to return (default: 10, max: 50)'),
64
66
  select: z.string().optional().describe('Comma-separated list of fields to include for each result (e.g. "id,summary,status,handler,priority"). ' +
65
67
  'When provided, each matching issue is fetched from MantisBT and enriched with the requested fields. ' +
66
- 'The relevance score is always included. Without this parameter only id and score are returned.'),
68
+ 'The relevance score is always included. Issues that MantisBT reports as inaccessible or deleted are left out. ' +
69
+ 'Without this parameter only id and score are returned.'),
67
70
  highlight: z
68
71
  .preprocess(coerceBool, z.boolean().default(false))
69
72
  .describe('If true, adds a "highlights" field per result with query terms bolded (**term**) ' +
@@ -144,7 +147,12 @@ export function registerSearchTools(server, client, store, embedder, lockDir) {
144
147
  }
145
148
  return projected;
146
149
  }
147
- catch {
150
+ catch (error) {
151
+ // Denied or deleted: MantisBT has the final say, so indexed text
152
+ // must not leak through the fallback below (#8617).
153
+ if (error instanceof MantisApiError && HIDDEN_ISSUE_STATUSES.has(error.statusCode)) {
154
+ return null;
155
+ }
148
156
  const result = { id, score, view_url: buildIssueViewUrl(baseUrl, id) };
149
157
  if (terms.length > 0) {
150
158
  const item = await store.getItem(id);
@@ -219,13 +227,13 @@ export function registerSearchTools(server, client, store, embedder, lockDir) {
219
227
  try {
220
228
  const startMs = Date.now();
221
229
  const syncService = new SearchSyncService(client, store, embedder, lockDir);
222
- const { indexed, skipped, total } = await syncService.sync(project_id, { full });
230
+ const { indexed, skipped, removed, total } = await syncService.sync(project_id, { full });
223
231
  const duration_ms = Date.now() - startMs;
224
232
  return {
225
233
  content: [
226
234
  {
227
235
  type: 'text',
228
- text: JSON.stringify({ indexed, skipped, total, duration_ms }, null, 2),
236
+ text: JSON.stringify({ indexed, skipped, removed, total, duration_ms }, null, 2),
229
237
  },
230
238
  ],
231
239
  };
@@ -1,4 +1,4 @@
1
- import { readFile } from 'node:fs/promises';
1
+ import { readFile, realpath } from 'node:fs/promises';
2
2
  import { basename, resolve, sep } from 'node:path';
3
3
  import { z } from 'zod';
4
4
  import { enrichIssue } from '../issue-enrich.js';
@@ -12,13 +12,13 @@ function errorText(msg) {
12
12
  return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
13
13
  }
14
14
  export function registerFileTools(server, client, uploadDir, transport = 'stdio') {
15
- const normalizedUploadDir = uploadDir ? resolve(uploadDir) + sep : undefined;
15
+ const uploadRoot = uploadDir ? resolve(uploadDir) : undefined;
16
16
  // file_path reads from the server's own filesystem. Over stdio the caller is
17
17
  // the trusted local agent (same machine, same user), so any path is fine. Over
18
18
  // HTTP a remote client neither controls nor should reach server-local paths —
19
19
  // it must send Base64 via `content`. Only allow file_path in HTTP mode when an
20
20
  // explicit MANTIS_UPLOAD_DIR sandboxes it.
21
- const filePathAllowed = transport === 'stdio' || normalizedUploadDir !== undefined;
21
+ const filePathAllowed = transport === 'stdio' || uploadRoot !== undefined;
22
22
  // ---------------------------------------------------------------------------
23
23
  // list_issue_files
24
24
  // ---------------------------------------------------------------------------
@@ -93,13 +93,19 @@ Use this tool to attach files such as logs, screenshots, or patches to an existi
93
93
  if (!filePathAllowed) {
94
94
  return { content: [{ type: 'text', text: errorText('file_path is disabled over the HTTP transport. Send the file via the "content" parameter (Base64), or set MANTIS_UPLOAD_DIR to permit server-local paths within that directory.') }], isError: true };
95
95
  }
96
- if (normalizedUploadDir) {
97
- const normalizedPath = resolve(file_path);
98
- if (!normalizedPath.startsWith(normalizedUploadDir)) {
99
- return { content: [{ type: 'text', text: errorText('file_path is not allowed — access restricted to the designated upload directory') }], isError: true };
100
- }
96
+ let readPath = file_path;
97
+ if (uploadRoot) {
98
+ const denied = { content: [{ type: 'text', text: errorText('file_path is not allowed — access restricted to the designated upload directory') }], isError: true };
99
+ if (!resolve(file_path).startsWith(uploadRoot + sep))
100
+ return denied;
101
+ // The textual check alone is not enough: a link inside the directory
102
+ // must not lead the read outside, so compare the real targets (#8618).
103
+ const [realTarget, realRoot] = await Promise.all([realpath(file_path), realpath(uploadRoot)]);
104
+ if (!realTarget.startsWith(realRoot + sep))
105
+ return denied;
106
+ readPath = realTarget;
101
107
  }
102
- const fileBuffer = await readFile(file_path);
108
+ const fileBuffer = await readFile(readPath);
103
109
  base64Content = fileBuffer.toString('base64');
104
110
  fileName = filename ?? basename(file_path);
105
111
  }
@@ -18,7 +18,7 @@ import { registerVersionTools } from './version.js';
18
18
  */
19
19
  export function registerAllTools(server, deps) {
20
20
  const { client, cache } = deps;
21
- registerIssueTools(server, client, cache);
21
+ registerIssueTools(server, client, cache, { disableDestructive: deps.disableDestructive });
22
22
  registerNoteTools(server, client);
23
23
  registerFileTools(server, client, deps.uploadDir, deps.transport);
24
24
  registerRelationshipTools(server, client);
@@ -139,7 +139,7 @@ async function resolveEnum(group, value, client) {
139
139
  const valid = Object.values(MANTIS_CANONICAL_ENUM_NAMES[group]).join(', ');
140
140
  return `Invalid ${group} "${value}". Valid canonical names: ${valid}. Call get_issue_schema to see localized labels.`;
141
141
  }
142
- export function registerIssueTools(server, client, cache) {
142
+ export function registerIssueTools(server, client, cache, { disableDestructive = false } = {}) {
143
143
  /**
144
144
  * Pages through GET /issues and hands every issue matching the client-side
145
145
  * filter to onMatch (return false to stop). Ends on a short page, or after
@@ -542,7 +542,7 @@ Steps run in order: fields, monitors, note. Once one step succeeded, a later fai
542
542
  inputSchema: z.object({
543
543
  id: z.coerce.number().int().positive().describe('Numeric issue ID to update'),
544
544
  add_monitors: z.array(z.string().min(1)).min(1).optional().describe('MantisBT login names to add as monitors (watchers) of the issue'),
545
- remove_monitors: z.array(z.string().min(1)).min(1).optional().describe('MantisBT login names to remove from the monitors of the issue; names that are not monitoring are ignored'),
545
+ remove_monitors: z.array(z.string().min(1)).min(1).optional().describe('MantisBT login names to remove from the monitors of the issue; names that are not monitoring are ignored. Rejected when the server runs with MANTIS_DISABLE_DESTRUCTIVE=true'),
546
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.'),
547
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.'),
548
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.'),
@@ -591,6 +591,13 @@ Steps run in order: fields, monitors, note. Once one step succeeded, a later fai
591
591
  isError: true,
592
592
  };
593
593
  }
594
+ // remove_monitor is hidden then; the same removal must not run here (#8621).
595
+ if (disableDestructive && remove_monitors !== undefined) {
596
+ return {
597
+ content: [{ type: 'text', text: errorText('remove_monitors is disabled — this server runs with MANTIS_DISABLE_DESTRUCTIVE=true.') }],
598
+ isError: true,
599
+ };
600
+ }
594
601
  const removing = new Set(remove_monitors?.map(u => u.toLowerCase()));
595
602
  const conflicting = add_monitors?.filter(u => removing.has(u.toLowerCase())) ?? [];
596
603
  if (conflicting.length > 0) {
@@ -35,10 +35,17 @@ const noteEdit = z.object({
35
35
  }).strict().refine(e => e.text !== undefined || e.view_state !== undefined, { message: "A note edit needs 'text' or 'view_state'" });
36
36
  /** Schema of update_issue fields.notes. */
37
37
  export const noteEditsSchema = z.array(noteEdit).min(1).superRefine((edits, ctx) => {
38
- const ids = edits.map(e => e.id);
39
- const twice = ids.filter((noteId, i) => ids.indexOf(noteId) !== i);
40
- if (twice.length > 0)
41
- ctx.addIssue({ code: 'custom', message: `Note ${twice.join(', ')} listed more than once` });
38
+ // Linear on purpose: this runs before any access check (#8622).
39
+ const seen = new Set();
40
+ const twice = new Set();
41
+ for (const { id } of edits) {
42
+ if (seen.has(id))
43
+ twice.add(id);
44
+ else
45
+ seen.add(id);
46
+ }
47
+ if (twice.size > 0)
48
+ ctx.addIssue({ code: 'custom', message: `Note ${[...twice].join(', ')} listed more than once` });
42
49
  });
43
50
  /**
44
51
  * Turns note edits into the PATCH payload. MantisBT has no REST route for
@@ -120,7 +120,7 @@ Endet `MANTIS_BASE_URL` auf `/api/rest/index.php`, wird das index.php-Routing au
120
120
 
121
121
  ### Nur bestimmte Tool-Gruppen anbieten
122
122
 
123
- Standardmäßig sind alle 41 Tools aktiv (Suche deaktiviert). Mit `MANTIS_TOOLSETS` lassen sich nur die benötigten Gruppen laden – das hält den Kontext des Modells kürzer. Mit `MANTIS_DISABLE_DESTRUCTIVE=true` werden alle destruktiven Tools ausgeblendet, unabhängig von den aktiven Gruppen.
123
+ Standardmäßig sind alle 41 Tools aktiv (Suche deaktiviert). Mit `MANTIS_TOOLSETS` lassen sich nur die benötigten Gruppen laden – das hält den Kontext des Modells kürzer. Mit `MANTIS_DISABLE_DESTRUCTIVE=true` werden alle destruktiven Tools ausgeblendet, unabhängig von den aktiven Gruppen; `update_issue` lehnt dann auch `remove_monitors` ab.
124
124
 
125
125
  Gültige Gruppennamen: `issues`, `notes`, `links`, `files`, `projects`, `versions`, `meta`, `search`. Ein unbekannter Name stoppt den Server beim Start mit einer Meldung, die die gültigen Namen auflistet. Veraltete Tools gehören immer zur gleichen Gruppe wie ihr Ersatz. Die vollständige Zuordnung von Gruppe zu Tools findet sich unter [Angebotene Tools einschränken im README](../README.de.md#angebotene-tools-einschränken).
126
126
 
@@ -1140,13 +1140,13 @@ Hängt eine Datei aus dem lokalen Dateisystem an ein Issue an.
1140
1140
 
1141
1141
  > **Hinweis:** Falls die MantisBT-Instanz keine Datei-Metadaten zurückgibt, lautet die Response `{ "success": true }`.
1142
1142
 
1143
- > **Hinweis:** Wenn `MANTIS_UPLOAD_DIR` gesetzt ist, muss `file_path` auf eine Datei innerhalb dieses Verzeichnisses zeigen. Pfade außerhalb des Verzeichnisses oder Path-Traversal-Versuche (`../`) werden mit einem Fehler abgelehnt.
1143
+ > **Hinweis:** Wenn `MANTIS_UPLOAD_DIR` gesetzt ist, muss `file_path` auf eine Datei innerhalb dieses Verzeichnisses zeigen. Pfade außerhalb des Verzeichnisses, Path-Traversal-Versuche (`../`) und symbolische Links, die nach außen führen, werden mit einem Fehler abgelehnt. Das Setzen wird auch im stdio-Modus empfohlen, damit eine Prompt Injection im Tickettext das Modell nicht dazu bringen kann, beliebige lokale Dateien hochzuladen; ab 2.0.0 setzt `file_path` die Variable voraus. Siehe [Sicherheitshinweise](../README.de.md#sicherheitshinweise).
1144
1144
 
1145
1145
  ---
1146
1146
 
1147
1147
  ### Dateiinhalt hochladen (Base64)
1148
1148
 
1149
- Hängt eine Datei an, indem ihr base64-kodierter Inhalt direkt übergeben wird. Nur als **Fallback** verwenden, wenn die Datei nicht über einen Pfad im Server-Dateisystem erreichbar ist (z.B. im Speicher erzeugte Daten oder Dateien, die nur auf der Client-Seite vorliegen). `file_path` bevorzugen, wenn die Datei auf der Festplatte existiert – der Server liest und kodiert sie automatisch.
1149
+ Hängt eine Datei an, indem ihr base64-kodierter Inhalt direkt übergeben wird. Nur als **Fallback** verwenden, wenn die Datei nicht über einen Pfad im Server-Dateisystem erreichbar ist (z.B. im Speicher erzeugte Daten oder Dateien, die nur auf der Client-Seite vorliegen). `file_path` bevorzugen, wenn die Datei auf der Festplatte existiert – der Server liest und kodiert sie automatisch. Über den HTTP-Transport darf eine Anfrage standardmäßig höchstens 64 MB groß sein, das reicht für Dateien bis etwa 48 MB; `MCP_HTTP_MAX_BODY_SIZE` ändert die Grenze.
1150
1150
 
1151
1151
  **Tool:** `upload_file`
1152
1152
 
@@ -1646,6 +1646,7 @@ Der Server baut den Index außerdem von selbst neu auf, wenn er mit einem andere
1646
1646
  {
1647
1647
  "indexed": 312,
1648
1648
  "skipped": 0,
1649
+ "removed": 0,
1649
1650
  "total": 312,
1650
1651
  "duration_ms": 48203
1651
1652
  }
@@ -1657,7 +1658,7 @@ Der Server baut den Index außerdem von selbst neu auf, wenn er mit einem andere
1657
1658
 
1658
1659
  Aktualisiert den Index mit Issues, die seit der letzten Synchronisation hinzugekommen oder geändert wurden. Schneller als ein vollständiger Neuaufbau: Nur die seitdem geänderten Issues werden abgerufen und neu eingebettet.
1659
1660
 
1660
- Es synchronisiert immer nur ein Serverprozess den gemeinsamen Index. Läuft bereits eine Synchronisation in einem anderen Prozess (etwa einer anderen Client-Sitzung), gibt das Tool einen Fehler zurück – nach deren Ende einfach erneut aufrufen. Eine auf `project_id` beschränkte Synchronisation verschiebt `last_synced_at` nicht, damit Änderungen in anderen Projekten beim nächsten Sync trotzdem erfasst werden. Wurde der Index mit einem anderen Embedding-Modell oder einer anderen Version erstellt, baut der Sync ihn für alle Projekte neu auf, auch mit `project_id`.
1661
+ Es synchronisiert immer nur ein Serverprozess den gemeinsamen Index. Läuft bereits eine Synchronisation in einem anderen Prozess (etwa einer anderen Client-Sitzung), gibt das Tool einen Fehler zurück – nach deren Ende einfach erneut aufrufen. Eine auf `project_id` beschränkte Synchronisation entfernt nichts und verschiebt `last_synced_at` nicht, damit Änderungen in anderen Projekten beim nächsten Sync trotzdem erfasst werden. Eine Synchronisation über alle Projekte entfernt außerdem Issues, die in MantisBT gelöscht wurden oder die das Konto nicht mehr sehen darf (`removed`); dafür listet sie die sichtbaren Issue-IDs auf, rund 1 s für 7.800 Issues. Wurde der Index mit einem anderen Embedding-Modell oder einer anderen Version erstellt, baut der Sync ihn für alle Projekte neu auf, auch mit `project_id`.
1661
1662
 
1662
1663
  **Tool:** `rebuild_search_index`
1663
1664
 
@@ -1679,6 +1680,7 @@ Es synchronisiert immer nur ein Serverprozess den gemeinsamen Index. Läuft bere
1679
1680
  {
1680
1681
  "indexed": 14,
1681
1682
  "skipped": 0,
1683
+ "removed": 0,
1682
1684
  "total": 312,
1683
1685
  "duration_ms": 2104
1684
1686
  }
package/docs/cookbook.md CHANGED
@@ -120,7 +120,7 @@ When `MANTIS_BASE_URL` ends with `/api/rest/index.php`, index.php routing is ena
120
120
 
121
121
  ### Offer only some tool groups
122
122
 
123
- By default all 41 tools are active (search off). Restrict them with `MANTIS_TOOLSETS` to keep the model's context shorter and load only what is needed for the task. Use `MANTIS_DISABLE_DESTRUCTIVE=true` to hide every destructive tool regardless of the active groups.
123
+ By default all 41 tools are active (search off). Restrict them with `MANTIS_TOOLSETS` to keep the model's context shorter and load only what is needed for the task. Use `MANTIS_DISABLE_DESTRUCTIVE=true` to hide every destructive tool regardless of the active groups; `update_issue` then also rejects `remove_monitors`.
124
124
 
125
125
  Valid group names: `issues`, `notes`, `links`, `files`, `projects`, `versions`, `meta`, `search`. An unknown name stops the server at startup with a message listing the valid ones. Deprecated tools always belong to the same group as their replacement. For the complete group-to-tool mapping see [Limit the offered tools in the README](../README.md#limit-the-offered-tools).
126
126
 
@@ -1140,13 +1140,13 @@ Attaches a file from the local filesystem to an issue.
1140
1140
 
1141
1141
  > **Note:** If the MantisBT instance does not return file metadata, the response is `{ "success": true }`.
1142
1142
 
1143
- > **Note:** If `MANTIS_UPLOAD_DIR` is set, `file_path` must point to a file inside that directory. Paths outside the directory or path traversal attempts (`../`) are rejected with an error.
1143
+ > **Note:** If `MANTIS_UPLOAD_DIR` is set, `file_path` must point to a file inside that directory. Paths outside the directory, path traversal attempts (`../`) and symbolic links that lead outside are rejected with an error. Setting it is recommended in stdio mode as well, so that a prompt injection in issue text cannot make the model upload arbitrary local files; starting with 2.0.0, `file_path` requires it. See [Security considerations](../README.md#security-considerations).
1144
1144
 
1145
1145
  ---
1146
1146
 
1147
1147
  ### Upload file content (base64)
1148
1148
 
1149
- Attaches a file by passing its base64-encoded content directly. Use this **only** as a fallback when the file is not accessible via a path on the server filesystem (e.g. in-memory data or files generated on the client side). Prefer `file_path` whenever the file exists on disk — the server reads and encodes it automatically.
1149
+ Attaches a file by passing its base64-encoded content directly. Use this **only** as a fallback when the file is not accessible via a path on the server filesystem (e.g. in-memory data or files generated on the client side). Prefer `file_path` whenever the file exists on disk — the server reads and encodes it automatically. Over the HTTP transport a request may be at most 64 MB by default, which fits files up to about 48 MB; `MCP_HTTP_MAX_BODY_SIZE` changes the limit.
1150
1150
 
1151
1151
  **Tool:** `upload_file`
1152
1152
 
@@ -1646,6 +1646,7 @@ The server also rebuilds the index by itself when it was made with another embed
1646
1646
  {
1647
1647
  "indexed": 312,
1648
1648
  "skipped": 0,
1649
+ "removed": 0,
1649
1650
  "total": 312,
1650
1651
  "duration_ms": 48203
1651
1652
  }
@@ -1657,7 +1658,7 @@ The server also rebuilds the index by itself when it was made with another embed
1657
1658
 
1658
1659
  Updates the index with issues added or changed since the last sync. Faster than a full rebuild: only issues changed since then are fetched and re-embedded.
1659
1660
 
1660
- Only one server process syncs the shared index at a time. If another process (for example another client session) is already syncing, the tool returns an error ("A search index sync is already running") — try again once that sync has finished. A sync limited to `project_id` does not advance `last_synced_at`, so changes in other projects are still picked up by the next sync. If the index was made with another embedding model or version, the sync rebuilds it for all projects, even with `project_id`.
1661
+ Only one server process syncs the shared index at a time. If another process (for example another client session) is already syncing, the tool returns an error ("A search index sync is already running") — try again once that sync has finished. A sync limited to `project_id` removes nothing and does not advance `last_synced_at`, so changes in other projects are still picked up by the next sync. A sync over all projects also removes issues that were deleted in MantisBT or that the account can no longer see (`removed`); it lists the visible issue IDs for that, about 1 s for 7,800 issues. If the index was made with another embedding model or version, the sync rebuilds it for all projects, even with `project_id`.
1661
1662
 
1662
1663
  **Tool:** `rebuild_search_index`
1663
1664
 
@@ -1679,6 +1680,7 @@ Only one server process syncs the shared index at a time. If another process (fo
1679
1680
  {
1680
1681
  "indexed": 14,
1681
1682
  "skipped": 0,
1683
+ "removed": 0,
1682
1684
  "total": 312,
1683
1685
  "duration_ms": 2104
1684
1686
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dpesch/mantisbt-mcp-server",
3
- "version": "1.18.1",
3
+ "version": "1.18.3",
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",
@@ -53,12 +53,17 @@
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/node": "^22.20.4",
56
- "@vitest/coverage-v8": "^5.0.2",
56
+ "@vitest/coverage-v8": "^5.1.0",
57
57
  "tsx": "^4.23.15",
58
58
  "typescript": "^5.9.3",
59
- "vitest": "^5.0.2"
59
+ "vitest": "^5.1.0"
60
60
  },
61
61
  "engines": {
62
62
  "node": ">=22"
63
+ },
64
+ "allowScripts": {
65
+ "esbuild@0.28.2": true,
66
+ "onnxruntime-node@1.30.0": true,
67
+ "protobufjs@7.6.6": true
63
68
  }
64
69
  }
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.18.1",
6
+ "version": "1.18.3",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "identifier": "@dpesch/mantisbt-mcp-server",
11
- "version": "1.18.1",
11
+ "version": "1.18.3",
12
12
  "runtimeHint": "npx",
13
13
  "transport": {
14
14
  "type": "stdio"
@@ -39,7 +39,7 @@
39
39
  },
40
40
  {
41
41
  "name": "MANTIS_CACHE_DIR",
42
- "description": "Directory for the metadata cache. Defaults to ~/.cache/mantisbt-mcp",
42
+ "description": "Directory for the metadata cache. Defaults to ~/.cache/mantisbt-mcp; each base URL and API key gets its own subdirectory",
43
43
  "isRequired": false,
44
44
  "isSecret": false,
45
45
  "format": "filepath"
@@ -79,11 +79,19 @@
79
79
  },
80
80
  {
81
81
  "name": "MCP_HTTP_TOKEN",
82
- "description": "Optional Bearer token to protect the /mcp endpoint when TRANSPORT=http",
82
+ "description": "Bearer token protecting the /mcp endpoint. Required when TRANSPORT=http; the server refuses to start without it",
83
83
  "isRequired": false,
84
84
  "isSecret": true,
85
85
  "format": "string"
86
86
  },
87
+ {
88
+ "name": "MCP_HTTP_MAX_BODY_SIZE",
89
+ "description": "Largest accepted /mcp request body in bytes when TRANSPORT=http. Default: 67108864 (64 MB)",
90
+ "isRequired": false,
91
+ "isSecret": false,
92
+ "format": "number",
93
+ "default": "67108864"
94
+ },
87
95
  {
88
96
  "name": "MANTIS_SEARCH_ENABLED",
89
97
  "description": "Enable offline semantic search (requires ~80 MB model download on first use)",
@@ -103,7 +111,7 @@
103
111
  },
104
112
  {
105
113
  "name": "MANTIS_SEARCH_DIR",
106
- "description": "Directory for the semantic search index. Defaults to ~/.cache/mantisbt-mcp/search",
114
+ "description": "Directory for the semantic search index. Defaults to the search subdirectory of the instance cache directory; when set, each base URL and API key gets its own subdirectory",
107
115
  "isRequired": false,
108
116
  "isSecret": false,
109
117
  "format": "filepath"