@dpesch/mantisbt-mcp-server 1.18.1 → 1.18.2
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 +19 -0
- package/README.de.md +7 -6
- package/README.md +7 -6
- package/dist/build-info.json +3 -3
- package/dist/config.js +24 -3
- package/dist/http-body.js +63 -0
- package/dist/index.js +41 -27
- package/dist/search/lexical.js +22 -5
- package/dist/search/store.js +7 -2
- package/dist/search/sync.js +35 -2
- package/dist/search/tools.js +13 -5
- package/dist/tools/files.js +15 -9
- package/dist/tools/index.js +1 -1
- package/dist/tools/issues.js +9 -2
- package/dist/tools/notes.js +11 -4
- package/docs/cookbook.de.md +6 -4
- package/docs/cookbook.md +6 -4
- package/package.json +6 -1
- package/server.json +13 -5
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,25 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## [1.18.2] – 2026-11-11
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `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.
|
|
19
|
+
|
|
20
|
+
### Security
|
|
21
|
+
|
|
22
|
+
- `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.
|
|
23
|
+
- `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.
|
|
24
|
+
- `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.
|
|
25
|
+
- `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.
|
|
26
|
+
- `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.
|
|
27
|
+
- 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.
|
|
28
|
+
- 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.
|
|
29
|
+
- 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.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
14
33
|
## [1.18.1] – 2026-11-10
|
|
15
34
|
|
|
16
35
|
### Fixed
|
package/README.de.md
CHANGED
|
@@ -71,20 +71,21 @@ npm run build
|
|
|
71
71
|
| `MANTIS_BASE_URL` | ✅ | – | Basis-URL der MantisBT-Installation. Beide Formate werden akzeptiert: `https://deine-mantis-instanz.example.com` und `https://deine-mantis-instanz.example.com/api/rest` – das `/api/rest`-Suffix wird automatisch normalisiert. |
|
|
72
72
|
| `MANTIS_API_KEY` | ✅ | – | API-Token für die Authentifizierung |
|
|
73
73
|
| `MANTIS_USE_INDEX_PHP` | – | auto | Auf `true` setzen, wenn URL-Rewriting nicht verfügbar ist – REST-Anfragen verwenden dann `/api/rest/index.php/` statt `/api/rest/`. Wird automatisch erkannt, wenn `MANTIS_BASE_URL` auf `/api/rest/index.php` endet; ein explizit gesetzter Wert hat immer Vorrang. Siehe [Cookbook](docs/cookbook.de.md#installation-ohne-url-rewriting-anbinden). |
|
|
74
|
-
| `MANTIS_CACHE_DIR` | – | `~/.cache/mantisbt-mcp` |
|
|
74
|
+
| `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
75
|
| `MANTIS_CACHE_TTL` | – | `3600` | Cache-Lebensdauer in Sekunden |
|
|
76
76
|
| `TRANSPORT` | – | `stdio` | Transport-Modus: `stdio` oder `http` |
|
|
77
77
|
| `PORT` | – | `3000` | Port für HTTP-Modus |
|
|
78
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
79
|
| `MCP_HTTP_TOKEN` | ✅ (HTTP-Modus) | – | Bearer-Token für den `/mcp`-Endpunkt (`Authorization: Bearer <token>`). **Pflicht bei `TRANSPORT=http`** – der Server startet im HTTP-Modus ohne diesen Wert nicht, damit Tools nie unauthentifiziert erreichbar sind. Im stdio-Modus ohne Bedeutung. `/health` ist immer öffentlich. |
|
|
80
|
+
| `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
81
|
| `MANTIS_SEARCH_ENABLED` | – | `false` | Auf `true` setzen, um die semantische Suche zu aktivieren |
|
|
81
82
|
| `MANTIS_SEARCH_BACKEND` | – | `vectra` | Vektorspeicher: `vectra` (reines JS) oder `sqlite-vec` (manuelle Installation erforderlich) |
|
|
82
|
-
| `MANTIS_SEARCH_DIR` | – | `{MANTIS_CACHE_DIR}
|
|
83
|
+
| `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
84
|
| `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
85
|
| `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
|
|
86
|
+
| `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. 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
87
|
| `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. |
|
|
88
|
+
| `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
89
|
|
|
89
90
|
### Angebotene Tools einschränken
|
|
90
91
|
|
|
@@ -321,7 +322,7 @@ MCP_HTTP_TOKEN=secret MANTIS_BASE_URL=... MANTIS_API_KEY=... \
|
|
|
321
322
|
# TRANSPORT=http PORT=3456 MCP_HTTP_HOST=0.0.0.0 node dist/index.js
|
|
322
323
|
```
|
|
323
324
|
|
|
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.
|
|
325
|
+
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
326
|
|
|
326
327
|
Healthcheck: `GET http://localhost:3456/health` (immer öffentlich, kein Token erforderlich)
|
|
327
328
|
|
|
@@ -341,7 +342,7 @@ npm test # Tests ausführen (vitest)
|
|
|
341
342
|
npm run test:watch # Tests im Watch-Modus
|
|
342
343
|
npm run test:coverage # Coverage-Report
|
|
343
344
|
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
|
|
345
|
+
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
346
|
```
|
|
346
347
|
|
|
347
348
|
`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
|
@@ -71,20 +71,21 @@ npm run build
|
|
|
71
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. |
|
|
72
72
|
| `MANTIS_API_KEY` | ✅ | – | API token for authentication |
|
|
73
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). |
|
|
74
|
-
| `MANTIS_CACHE_DIR` | – | `~/.cache/mantisbt-mcp` |
|
|
74
|
+
| `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
75
|
| `MANTIS_CACHE_TTL` | – | `3600` | Cache lifetime in seconds |
|
|
76
76
|
| `TRANSPORT` | – | `stdio` | Transport mode: `stdio` or `http` |
|
|
77
77
|
| `PORT` | – | `3000` | Port for HTTP mode |
|
|
78
78
|
| `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
79
|
| `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. |
|
|
80
|
+
| `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
81
|
| `MANTIS_SEARCH_ENABLED` | – | `false` | Set to `true` to enable semantic search |
|
|
81
82
|
| `MANTIS_SEARCH_BACKEND` | – | `vectra` | Vector store backend: `vectra` (pure JS) or `sqlite-vec` (requires manual install) |
|
|
82
|
-
| `MANTIS_SEARCH_DIR` | – | `{MANTIS_CACHE_DIR}
|
|
83
|
+
| `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
84
|
| `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
85
|
| `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
|
|
86
|
+
| `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. 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
87
|
| `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. |
|
|
88
|
+
| `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
89
|
|
|
89
90
|
### Limit the offered tools
|
|
90
91
|
|
|
@@ -321,7 +322,7 @@ MCP_HTTP_TOKEN=secret MANTIS_BASE_URL=... MANTIS_API_KEY=... \
|
|
|
321
322
|
# TRANSPORT=http PORT=3456 MCP_HTTP_HOST=0.0.0.0 node dist/index.js
|
|
322
323
|
```
|
|
323
324
|
|
|
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.
|
|
325
|
+
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
326
|
|
|
326
327
|
Health check: `GET http://localhost:3456/health` (always public, no token required)
|
|
327
328
|
|
|
@@ -341,7 +342,7 @@ npm test # Run tests (vitest)
|
|
|
341
342
|
npm run test:watch # Run tests in watch mode
|
|
342
343
|
npm run test:coverage # Coverage report
|
|
343
344
|
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
|
|
345
|
+
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
346
|
```
|
|
346
347
|
|
|
347
348
|
`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`.
|
package/dist/build-info.json
CHANGED
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
|
|
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
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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') {
|
package/dist/search/lexical.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
package/dist/search/store.js
CHANGED
|
@@ -126,9 +126,14 @@ export class VectraStore {
|
|
|
126
126
|
await this.ensureLoaded();
|
|
127
127
|
return this.items.get(id) ?? null;
|
|
128
128
|
}
|
|
129
|
-
async
|
|
129
|
+
async ids() {
|
|
130
130
|
await this.ensureLoaded();
|
|
131
|
-
this.items.
|
|
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
|
}
|
package/dist/search/sync.js
CHANGED
|
@@ -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 = [];
|
package/dist/search/tools.js
CHANGED
|
@@ -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.
|
|
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
|
};
|
package/dist/tools/files.js
CHANGED
|
@@ -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
|
|
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' ||
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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(
|
|
108
|
+
const fileBuffer = await readFile(readPath);
|
|
103
109
|
base64Content = fileBuffer.toString('base64');
|
|
104
110
|
fileName = filename ?? basename(file_path);
|
|
105
111
|
}
|
package/dist/tools/index.js
CHANGED
|
@@ -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);
|
package/dist/tools/issues.js
CHANGED
|
@@ -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) {
|
package/dist/tools/notes.js
CHANGED
|
@@ -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
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
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
|
package/docs/cookbook.de.md
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
3
|
+
"version": "1.18.2",
|
|
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",
|
|
@@ -60,5 +60,10 @@
|
|
|
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.
|
|
6
|
+
"version": "1.18.2",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"registryType": "npm",
|
|
10
10
|
"identifier": "@dpesch/mantisbt-mcp-server",
|
|
11
|
-
"version": "1.18.
|
|
11
|
+
"version": "1.18.2",
|
|
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": "
|
|
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
|
|
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"
|