@dpesch/mantisbt-mcp-server 1.8.3 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.de.md +3 -3
- package/README.md +3 -3
- package/dist/date-filter.js +55 -0
- package/dist/search/highlight.js +63 -0
- package/dist/search/store.js +4 -0
- package/dist/search/tools.js +65 -4
- package/dist/tools/config.js +23 -8
- package/dist/tools/issues.js +123 -18
- package/dist/tools/notes.js +1 -1
- package/docs/cookbook.de.md +64 -7
- package/docs/cookbook.md +64 -7
- package/docs/examples.de.md +12 -0
- package/docs/examples.md +12 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/tests/fixtures/get_issue.json +22 -0
- package/tests/helpers/search-mocks.ts +29 -6
- package/tests/search/highlight.test.ts +129 -0
- package/tests/search/tools.test.ts +258 -0
- package/tests/tools/issues.test.ts +446 -4
- package/tests/utils/date-filter.test.ts +169 -0
package/dist/tools/notes.js
CHANGED
|
@@ -12,7 +12,7 @@ export function registerNoteTools(server, client) {
|
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
server.registerTool('list_notes', {
|
|
14
14
|
title: 'List Issue Notes',
|
|
15
|
-
description: 'List all notes (comments) attached to a MantisBT issue.',
|
|
15
|
+
description: 'List all notes (comments) attached to a MantisBT issue. Note: get_issue already includes notes in its response — use list_notes only when you need notes without fetching the full issue.',
|
|
16
16
|
inputSchema: z.object({
|
|
17
17
|
issue_id: z.coerce.number().int().positive().describe('Numeric issue ID'),
|
|
18
18
|
}),
|
package/docs/cookbook.de.md
CHANGED
|
@@ -145,7 +145,7 @@ Gibt die auf der eigenen MantisBT-Instanz konfigurierten Enum-Werte zurück. Vor
|
|
|
145
145
|
}
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
> **Hinweis:**
|
|
148
|
+
> **Hinweis:** Auf lokalisierten Instanzen liefert `get_issue_enums()` im Feld `name` den lokalisierten Begriff und optional im Feld `canonical_name` den englischen Originalnamen. `create_issue` akzeptiert **beides** — sowohl den kanonischen englischen Namen (z.B. `minor`) als auch den lokalisierten Namen (z.B. `Unschönheit`). Der Server löst den Wert automatisch auf.
|
|
149
149
|
|
|
150
150
|
---
|
|
151
151
|
|
|
@@ -347,6 +347,8 @@ Gibt nur Issues mit einem bestimmten Status zurück. Der Filter wird clientseiti
|
|
|
347
347
|
}
|
|
348
348
|
```
|
|
349
349
|
|
|
350
|
+
> **Hinweis:** Kanonische Statusnamen (z.B. `"new"`, `"resolved"`) werden zur numerischen ID aufgelöst und per `issue.status.id` gefiltert — funktioniert auch auf lokalisierten Installationen, bei denen die API übersetzte Statusnamen zurückgibt. Direkt übergebene lokalisierte Namen (z.B. `"Neu"`) werden als Fallback über den Namen abgeglichen. Das Kürzel `"open"` (alle Status mit id < 80) steht unabhängig von der Installationssprache immer zur Verfügung.
|
|
351
|
+
|
|
350
352
|
> **Hinweis:** Bei großen Projekten mit vielen Issues stattdessen einen vorgespeicherten MantisBT-Filter über `filter_id` verwenden — die clientseitige Filterung durchsucht nur die ersten 500 Issues (10 Seiten × 50).
|
|
351
353
|
|
|
352
354
|
---
|
|
@@ -456,8 +458,8 @@ Legt ein neues Issue in MantisBT an.
|
|
|
456
458
|
- `project_id` — numerische Projekt-ID
|
|
457
459
|
- `category` — Kategoriename als Zeichenkette
|
|
458
460
|
- `description` — _(optional)_ ausführliche Beschreibung
|
|
459
|
-
- `priority` — _(optional)_ kanonischer englischer
|
|
460
|
-
- `severity` — _(optional)_ kanonischer englischer
|
|
461
|
+
- `priority` — _(optional)_ Priorität: kanonischer englischer Name (`none`, `low`, `normal`, `high`, `urgent`, `immediate`) oder lokalisierter Begriff. Standard: `"normal"`. Alle verfügbaren Werte über `get_issue_enums()` ermitteln.
|
|
462
|
+
- `severity` — _(optional)_ Schweregrad: kanonischer englischer Name (`feature`, `trivial`, `text`, `tweak`, `minor`, `major`, `crash`, `block`) oder lokalisierter Begriff. Standard: `"minor"`. Alle verfügbaren Werte über `get_issue_enums()` ermitteln.
|
|
461
463
|
- `handler` — _(optional)_ Benutzername des Bearbeiters (wird automatisch in eine ID aufgelöst)
|
|
462
464
|
- `handler_id` — _(optional)_ numerische Benutzer-ID des Bearbeiters (Alternative zu `handler`)
|
|
463
465
|
|
|
@@ -500,11 +502,11 @@ Legt ein neues Issue in MantisBT an.
|
|
|
500
502
|
|
|
501
503
|
**Fehler: unbekannter Schweregrad oder unbekannte Priorität**
|
|
502
504
|
|
|
503
|
-
|
|
505
|
+
Der Server prüft zuerst kanonische englische Namen und fällt dann auf einen Live-`get_issue_enums`-Lookup zurück. Ein Fehler wird nur zurückgegeben, wenn der Wert weder kanonisch noch lokalisiert erkannt wird:
|
|
504
506
|
|
|
505
|
-
> Error: Invalid severity "
|
|
507
|
+
> Error: Invalid severity "xyz". Valid canonical names: feature, trivial, text, tweak, minor, major, crash, block. Call get_issue_enums to see localized labels.
|
|
506
508
|
|
|
507
|
-
Mit `get_issue_enums` lassen sich
|
|
509
|
+
Mit `get_issue_enums` lassen sich alle akzeptierten Werte ermitteln — sowohl kanonische als auch lokalisierte Namen funktionieren.
|
|
508
510
|
|
|
509
511
|
---
|
|
510
512
|
|
|
@@ -519,6 +521,8 @@ Löst ein Issue auf und schließt es. **Immer beide Felder** `status` und `resol
|
|
|
519
521
|
- `fields.status` — Status-Objekt mit Name
|
|
520
522
|
- `fields.resolution` — Auflösungs-Objekt mit ID
|
|
521
523
|
|
|
524
|
+
> **Hinweis:** Alle Enum-Felder (`status`, `priority`, `severity`, `resolution`, `reproducibility`) akzeptieren kanonische englische Namen, lokalisierte Namen oder numerische IDs. Der Server löst Namen automatisch zu IDs auf — dadurch ist die Übergabe sprachunabhängig.
|
|
525
|
+
|
|
522
526
|
**Request:**
|
|
523
527
|
|
|
524
528
|
```json
|
|
@@ -1246,6 +1250,7 @@ Findet Issues, die einem natürlichsprachigen Suchbegriff semantisch ähnlich si
|
|
|
1246
1250
|
**Parameter:**
|
|
1247
1251
|
- `query` — Suchanfrage in natürlicher Sprache
|
|
1248
1252
|
- `top_n` — _(optional)_ Anzahl zurückzugebender Ergebnisse; Standard 10
|
|
1253
|
+
- `highlight` — _(optional)_ bei `true` werden keyword-basierte Ausschnitte je Ergebnis ergänzt; Standard `false`
|
|
1249
1254
|
|
|
1250
1255
|
**Request:**
|
|
1251
1256
|
|
|
@@ -1280,6 +1285,7 @@ Reichert Suchergebnisse mit bestimmten Feldern aus MantisBT an. Ohne `select` we
|
|
|
1280
1285
|
- `query` — Suchanfrage in natürlicher Sprache
|
|
1281
1286
|
- `top_n` — _(optional)_ Anzahl der Ergebnisse
|
|
1282
1287
|
- `select` — kommagetrennte Feldnamen, die für jedes Ergebnis abgerufen werden
|
|
1288
|
+
- `highlight` — _(optional)_ bei `true` werden keyword-basierte Ausschnitte je Ergebnis ergänzt; Standard `false`
|
|
1283
1289
|
|
|
1284
1290
|
**Request:**
|
|
1285
1291
|
|
|
@@ -1310,6 +1316,57 @@ Reichert Suchergebnisse mit bestimmten Feldern aus MantisBT an. Ohne `select` we
|
|
|
1310
1316
|
|
|
1311
1317
|
---
|
|
1312
1318
|
|
|
1319
|
+
### Suche mit Keyword-Highlights
|
|
1320
|
+
|
|
1321
|
+
Zeigt, welcher Teil eines Issues mit der Suchanfrage übereinstimmt. Jedes Ergebnis, das lexikalisch mit der Anfrage übereinstimmt, erhält ein `highlights`-Feld mit fett hervorgehobenen Ausschnitten. Highlights sind keyword-basiert (lexikalisch), nicht semantisch — Ergebnisse ohne lexikalische Übereinstimmung haben kein `highlights`-Feld.
|
|
1322
|
+
|
|
1323
|
+
**Tool:** `search_issues`
|
|
1324
|
+
|
|
1325
|
+
**Parameter:**
|
|
1326
|
+
- `query` — Suchanfrage in natürlicher Sprache
|
|
1327
|
+
- `top_n` — _(optional)_ Anzahl der Ergebnisse; Standard 10
|
|
1328
|
+
- `highlight` — auf `true` setzen, um Highlights zu aktivieren
|
|
1329
|
+
|
|
1330
|
+
**Request:**
|
|
1331
|
+
|
|
1332
|
+
```json
|
|
1333
|
+
{
|
|
1334
|
+
"query": "Login-Fehler nach Passwort-Reset",
|
|
1335
|
+
"top_n": 5,
|
|
1336
|
+
"highlight": true
|
|
1337
|
+
}
|
|
1338
|
+
```
|
|
1339
|
+
|
|
1340
|
+
**Response:**
|
|
1341
|
+
|
|
1342
|
+
```json
|
|
1343
|
+
[
|
|
1344
|
+
{
|
|
1345
|
+
"id": 1042,
|
|
1346
|
+
"score": 0.91,
|
|
1347
|
+
"highlights": {
|
|
1348
|
+
"summary": "**Login**-Button reagiert nach **Passwort**-**Reset** auf Mobile Safari nicht",
|
|
1349
|
+
"description": "…Benutzer tippt auf **Login** und es passiert nichts. Reproduzierbar nach einem **Passwort**-**Reset**-Vorgang…"
|
|
1350
|
+
}
|
|
1351
|
+
},
|
|
1352
|
+
{
|
|
1353
|
+
"id": 987,
|
|
1354
|
+
"score": 0.84,
|
|
1355
|
+
"highlights": {
|
|
1356
|
+
"summary": "**Login** schlägt mit 401 fehl — Token ungültig"
|
|
1357
|
+
}
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
"id": 1015,
|
|
1361
|
+
"score": 0.79
|
|
1362
|
+
}
|
|
1363
|
+
]
|
|
1364
|
+
```
|
|
1365
|
+
|
|
1366
|
+
> **Hinweis:** Nur Ergebnisse mit mindestens einer Keyword-Übereinstimmung in `summary` oder `description` erhalten ein `highlights`-Feld. Der `description`-Ausschnitt umfasst ca. 300 Zeichen, zentriert um den ersten Treffer. Wenn zusätzlich `select` gesetzt ist und `summary` oder `description` enthält, werden die Highlights aus den abgerufenen Feldern generiert; andernfalls stammen sie aus den indizierten Metadaten.
|
|
1367
|
+
|
|
1368
|
+
---
|
|
1369
|
+
|
|
1313
1370
|
## Projekte & Kategorien
|
|
1314
1371
|
|
|
1315
1372
|
### Projektkategorien auflisten
|
|
@@ -1635,7 +1692,7 @@ Gibt eine kombinierte Ansicht eines einzelnen Projekts zurück: Projektfelder so
|
|
|
1635
1692
|
|
|
1636
1693
|
### Issue-Enum-Werte lesen
|
|
1637
1694
|
|
|
1638
|
-
Gibt gültige ID/Name-Paare für alle Issue-Enum-Felder zurück (Severity, Priority, Status, Resolution, Reproducibility).
|
|
1695
|
+
Gibt gültige ID/Name-Paare für alle Issue-Enum-Felder zurück (Severity, Priority, Status, Resolution, Reproducibility). Bei `create_issue` werden sowohl kanonische englische Namen als auch lokalisierte `name`/`label`-Werte akzeptiert — diese Ressource hilft, alle verfügbaren Werte zu ermitteln.
|
|
1639
1696
|
|
|
1640
1697
|
**Ressource-URI:** `mantis://enums`
|
|
1641
1698
|
|
package/docs/cookbook.md
CHANGED
|
@@ -145,7 +145,7 @@ Returns the enum values configured on your specific MantisBT instance. Use this
|
|
|
145
145
|
}
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
> **Note:**
|
|
148
|
+
> **Note:** On localized installations, `get_issue_enums()` returns a `name` in the local language and an optional `canonical_name` with the English original. `create_issue` accepts **both** — pass either the canonical English name (e.g. `minor`) or the localized name (e.g. `Unschönheit`). The server resolves the value automatically.
|
|
149
149
|
|
|
150
150
|
---
|
|
151
151
|
|
|
@@ -347,6 +347,8 @@ Returns only issues with a specific status. The filter is applied client-side
|
|
|
347
347
|
}
|
|
348
348
|
```
|
|
349
349
|
|
|
350
|
+
> **Note:** Canonical status names (e.g. `"new"`, `"resolved"`) are resolved to their numeric ID and matched by `issue.status.id` — this works correctly even on localized installations where the API returns translated status names. Localized names passed directly (e.g. `"Neu"`) are matched by name as a fallback. The `"open"` shorthand (all statuses with id < 80) is always available regardless of installation language.
|
|
351
|
+
|
|
350
352
|
> **Note:** For large projects with many issues, use a pre-saved MantisBT filter via `filter_id` instead — client-side filtering only scans the first 500 issues (10 pages × 50).
|
|
351
353
|
|
|
352
354
|
---
|
|
@@ -456,8 +458,8 @@ Creates a new issue in MantisBT.
|
|
|
456
458
|
- `project_id` — numeric project ID
|
|
457
459
|
- `category` — category name string
|
|
458
460
|
- `description` — _(optional)_ detailed description
|
|
459
|
-
- `priority` — _(optional)_ canonical English
|
|
460
|
-
- `severity` — _(optional)_ canonical English
|
|
461
|
+
- `priority` — _(optional)_ priority: canonical English name (`none`, `low`, `normal`, `high`, `urgent`, `immediate`) or localized label. Default: `"normal"`. Use `get_issue_enums()` to see all available values.
|
|
462
|
+
- `severity` — _(optional)_ severity: canonical English name (`feature`, `trivial`, `text`, `tweak`, `minor`, `major`, `crash`, `block`) or localized label. Default: `"minor"`. Use `get_issue_enums()` to see all available values.
|
|
461
463
|
- `handler` — _(optional)_ assignee username (resolved to ID automatically)
|
|
462
464
|
- `handler_id` — _(optional)_ assignee numeric user ID (alternative to `handler`)
|
|
463
465
|
|
|
@@ -500,11 +502,11 @@ Creates a new issue in MantisBT.
|
|
|
500
502
|
|
|
501
503
|
**Error: unknown severity or priority**
|
|
502
504
|
|
|
503
|
-
|
|
505
|
+
The server first checks canonical English names, then falls back to a live `get_issue_enums` lookup. An error is only returned if the value matches neither:
|
|
504
506
|
|
|
505
|
-
> Error: Invalid severity "
|
|
507
|
+
> Error: Invalid severity "xyz". Valid canonical names: feature, trivial, text, tweak, minor, major, crash, block. Call get_issue_enums to see localized labels.
|
|
506
508
|
|
|
507
|
-
Use `get_issue_enums` to discover
|
|
509
|
+
Use `get_issue_enums` to discover all accepted values — both canonical and localized names work.
|
|
508
510
|
|
|
509
511
|
---
|
|
510
512
|
|
|
@@ -519,6 +521,8 @@ Resolves and closes an issue. Always set **both** `status` and `resolution` —
|
|
|
519
521
|
- `fields.status` — status object with name
|
|
520
522
|
- `fields.resolution` — resolution object with id
|
|
521
523
|
|
|
524
|
+
> **Note:** All enum fields (`status`, `priority`, `severity`, `resolution`, `reproducibility`) accept the canonical English name, a localized name, or a numeric `id`. The server resolves names to IDs automatically — IDs are always sent to the API, ensuring language-independence.
|
|
525
|
+
|
|
522
526
|
**Request:**
|
|
523
527
|
|
|
524
528
|
```json
|
|
@@ -1246,6 +1250,7 @@ Finds issues semantically similar to a natural language query. Returns issue IDs
|
|
|
1246
1250
|
**Parameters:**
|
|
1247
1251
|
- `query` — natural language search query
|
|
1248
1252
|
- `top_n` — _(optional)_ number of results to return; default 10
|
|
1253
|
+
- `highlight` — _(optional)_ when `true`, adds keyword-matched excerpts to each result; default `false`
|
|
1249
1254
|
|
|
1250
1255
|
**Request:**
|
|
1251
1256
|
|
|
@@ -1280,6 +1285,7 @@ Enriches search results with specific fields fetched from MantisBT. Without `sel
|
|
|
1280
1285
|
- `query` — natural language search query
|
|
1281
1286
|
- `top_n` — _(optional)_ number of results
|
|
1282
1287
|
- `select` — comma-separated field names to fetch for each result
|
|
1288
|
+
- `highlight` — _(optional)_ when `true`, adds keyword-matched excerpts to each result; default `false`
|
|
1283
1289
|
|
|
1284
1290
|
**Request:**
|
|
1285
1291
|
|
|
@@ -1310,6 +1316,57 @@ Enriches search results with specific fields fetched from MantisBT. Without `sel
|
|
|
1310
1316
|
|
|
1311
1317
|
---
|
|
1312
1318
|
|
|
1319
|
+
### Search with keyword highlights
|
|
1320
|
+
|
|
1321
|
+
Shows which part of an issue matched the search query. Each result that has keyword overlap with the query receives a `highlights` field with bold-marked excerpts. Highlights are keyword-based (lexical), not semantic — results with no lexical overlap will not have a `highlights` field.
|
|
1322
|
+
|
|
1323
|
+
**Tool:** `search_issues`
|
|
1324
|
+
|
|
1325
|
+
**Parameters:**
|
|
1326
|
+
- `query` — natural language search query
|
|
1327
|
+
- `top_n` — _(optional)_ number of results; default 10
|
|
1328
|
+
- `highlight` — set to `true` to enable highlights
|
|
1329
|
+
|
|
1330
|
+
**Request:**
|
|
1331
|
+
|
|
1332
|
+
```json
|
|
1333
|
+
{
|
|
1334
|
+
"query": "login error after password reset",
|
|
1335
|
+
"top_n": 5,
|
|
1336
|
+
"highlight": true
|
|
1337
|
+
}
|
|
1338
|
+
```
|
|
1339
|
+
|
|
1340
|
+
**Response:**
|
|
1341
|
+
|
|
1342
|
+
```json
|
|
1343
|
+
[
|
|
1344
|
+
{
|
|
1345
|
+
"id": 1042,
|
|
1346
|
+
"score": 0.91,
|
|
1347
|
+
"highlights": {
|
|
1348
|
+
"summary": "**Login** button unresponsive after **password** **reset** on mobile Safari",
|
|
1349
|
+
"description": "…user taps **login** and nothing happens. Reproducible after a **password** **reset** flow…"
|
|
1350
|
+
}
|
|
1351
|
+
},
|
|
1352
|
+
{
|
|
1353
|
+
"id": 987,
|
|
1354
|
+
"score": 0.84,
|
|
1355
|
+
"highlights": {
|
|
1356
|
+
"summary": "**Login** fails with 401 — token invalidated"
|
|
1357
|
+
}
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
"id": 1015,
|
|
1361
|
+
"score": 0.79
|
|
1362
|
+
}
|
|
1363
|
+
]
|
|
1364
|
+
```
|
|
1365
|
+
|
|
1366
|
+
> **Note:** Only results with at least one keyword match in `summary` or `description` receive a `highlights` field. The `description` excerpt is approximately 300 characters, centred around the first match. When `select` is also set and includes `summary` or `description`, highlights are generated from the fetched fields; otherwise they come from the indexed metadata.
|
|
1367
|
+
|
|
1368
|
+
---
|
|
1369
|
+
|
|
1313
1370
|
## Projects & Categories
|
|
1314
1371
|
|
|
1315
1372
|
### List project categories
|
|
@@ -1635,7 +1692,7 @@ Returns a combined view of a single project: its fields plus all members, versio
|
|
|
1635
1692
|
|
|
1636
1693
|
### Read issue enum values
|
|
1637
1694
|
|
|
1638
|
-
Returns valid ID/name pairs for all issue enum fields (severity, priority, status, resolution, reproducibility).
|
|
1695
|
+
Returns valid ID/name pairs for all issue enum fields (severity, priority, status, resolution, reproducibility). For `create_issue`, both the canonical English name and the localized `name`/`label` are accepted — this resource helps discover all available values.
|
|
1639
1696
|
|
|
1640
1697
|
**Resource URI:** `mantis://enums`
|
|
1641
1698
|
|
package/docs/examples.de.md
CHANGED
|
@@ -155,6 +155,18 @@ Die semantische Suche versteht die *Bedeutung* deiner Frage — nicht nur einzel
|
|
|
155
155
|
|
|
156
156
|
---
|
|
157
157
|
|
|
158
|
+
### Keyword-Highlights
|
|
159
|
+
|
|
160
|
+
> »Suche nach Login-Fehlern und zeige mir, welche Stelle im Ticket relevant ist.«
|
|
161
|
+
|
|
162
|
+
> »Finde Issues zum Passwort-Reset-Ablauf und hebe die passenden Stellen hervor, damit ich die Ergebnisse schnell überfliegen kann.«
|
|
163
|
+
|
|
164
|
+
> »Suche nach 'Rechnungsexport' und markiere die relevanten Ausschnitte in Titel und Beschreibung.«
|
|
165
|
+
|
|
166
|
+
Exakte Parameter und Response-Shape: [Cookbook — Suche mit Keyword-Highlights](cookbook.de.md#suche-mit-keyword-highlights).
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
158
170
|
### Unscharfe / terminologieunabhängige Suche
|
|
159
171
|
|
|
160
172
|
> »Finde Issues zu 'doppelten Einträgen' — sie könnten auch als 'zweimal angezeigt', 'doppelte Datensätze' oder 'Phantom-Zeilen' beschrieben sein.«
|
package/docs/examples.md
CHANGED
|
@@ -155,6 +155,18 @@ Semantic search understands the *meaning* of your question — not just keywords
|
|
|
155
155
|
|
|
156
156
|
---
|
|
157
157
|
|
|
158
|
+
### Keyword highlights
|
|
159
|
+
|
|
160
|
+
> "Search for login errors and show me which part of each ticket is relevant."
|
|
161
|
+
|
|
162
|
+
> "Find issues about the password reset flow and highlight the matching passages so I can quickly scan the results."
|
|
163
|
+
|
|
164
|
+
> "Search for 'invoice export' and highlight the relevant excerpts in summary and description."
|
|
165
|
+
|
|
166
|
+
For exact parameters and response shape, see the [Cookbook — Search with keyword highlights](cookbook.md#search-with-keyword-highlights).
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
158
170
|
### Fuzzy / terminology-independent search
|
|
159
171
|
|
|
160
172
|
> "Find issues about 'duplicate entries' — they might also be described as 'shown twice', 'double records', or 'phantom rows'."
|
package/package.json
CHANGED
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.
|
|
6
|
+
"version": "1.9.0",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"registryType": "npm",
|
|
10
10
|
"identifier": "@dpesch/mantisbt-mcp-server",
|
|
11
|
-
"version": "1.
|
|
11
|
+
"version": "1.9.0",
|
|
12
12
|
"runtimeHint": "npx",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|
|
@@ -58,6 +58,28 @@
|
|
|
58
58
|
"sticky": false,
|
|
59
59
|
"created_at": "2026-03-16T15:40:38+01:00",
|
|
60
60
|
"updated_at": "2026-03-16T15:40:38+01:00",
|
|
61
|
+
"notes": [
|
|
62
|
+
{
|
|
63
|
+
"id": 1001,
|
|
64
|
+
"reporter": { "id": 51, "name": "user_1" },
|
|
65
|
+
"text": "First note on this issue.",
|
|
66
|
+
"view_state": { "id": 10, "name": "public", "label": "öffentlich" },
|
|
67
|
+
"attachments": [],
|
|
68
|
+
"type": "note",
|
|
69
|
+
"created_at": "2026-03-16T16:00:00+01:00",
|
|
70
|
+
"updated_at": "2026-03-16T16:00:00+01:00"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": 1002,
|
|
74
|
+
"reporter": { "id": 51, "name": "user_1" },
|
|
75
|
+
"text": "Second note on this issue.",
|
|
76
|
+
"view_state": { "id": 10, "name": "public", "label": "öffentlich" },
|
|
77
|
+
"attachments": [],
|
|
78
|
+
"type": "note",
|
|
79
|
+
"created_at": "2026-03-16T17:00:00+01:00",
|
|
80
|
+
"updated_at": "2026-03-16T17:00:00+01:00"
|
|
81
|
+
}
|
|
82
|
+
],
|
|
61
83
|
"history": [
|
|
62
84
|
{
|
|
63
85
|
"created_at": "2026-03-16T15:40:38+01:00",
|
|
@@ -12,10 +12,29 @@ export const MOCK_VECTOR = Array(384).fill(0.1) as number[];
|
|
|
12
12
|
// makeMockStore
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
|
|
15
|
-
export
|
|
15
|
+
export interface MockStoreItem {
|
|
16
|
+
id: number;
|
|
17
|
+
score?: number;
|
|
18
|
+
updated_at?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function makeMockStore(options?: {
|
|
22
|
+
lastSyncedAt?: string | null;
|
|
23
|
+
itemCount?: number;
|
|
24
|
+
lastKnownTotal?: number | null;
|
|
25
|
+
items?: MockStoreItem[];
|
|
26
|
+
}): VectorStore {
|
|
16
27
|
const lastSyncedAt = options?.lastSyncedAt ?? null;
|
|
28
|
+
const seedItems = options?.items ?? null;
|
|
17
29
|
const addedItems: VectorStoreItem[] = [];
|
|
18
|
-
|
|
30
|
+
const itemMap = new Map<number, VectorStoreItem>(
|
|
31
|
+
(seedItems ?? []).map(i => [i.id, {
|
|
32
|
+
id: i.id,
|
|
33
|
+
vector: MOCK_VECTOR,
|
|
34
|
+
metadata: { summary: `Issue ${i.id}`, updated_at: i.updated_at },
|
|
35
|
+
}])
|
|
36
|
+
);
|
|
37
|
+
let count = options?.itemCount ?? seedItems?.length ?? 0;
|
|
19
38
|
|
|
20
39
|
return {
|
|
21
40
|
add: vi.fn(async (item: VectorStoreItem) => {
|
|
@@ -28,12 +47,16 @@ export function makeMockStore(options?: { lastSyncedAt?: string | null; itemCoun
|
|
|
28
47
|
}
|
|
29
48
|
count += items.length;
|
|
30
49
|
}),
|
|
31
|
-
search: vi.fn(async (_vec: number[], topN: number) =>
|
|
32
|
-
|
|
50
|
+
search: vi.fn(async (_vec: number[], topN: number) => {
|
|
51
|
+
if (seedItems) {
|
|
52
|
+
return seedItems.slice(0, topN).map(i => ({ id: i.id, score: i.score ?? 0.9 }));
|
|
53
|
+
}
|
|
54
|
+
return Array.from({ length: Math.min(topN, count) }, (_, i) => ({
|
|
33
55
|
id: i + 1,
|
|
34
56
|
score: 1 - i * 0.1,
|
|
35
|
-
}))
|
|
36
|
-
),
|
|
57
|
+
}));
|
|
58
|
+
}),
|
|
59
|
+
getItem: vi.fn(async (id: number) => itemMap.get(id) ?? null),
|
|
37
60
|
delete: vi.fn(async () => {}),
|
|
38
61
|
count: vi.fn(async () => count),
|
|
39
62
|
clear: vi.fn(async () => {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { extractTerms, highlightText, extractSnippet } from '../../src/search/highlight.js';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// extractTerms
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
describe('extractTerms', () => {
|
|
9
|
+
it('splits by whitespace and returns terms of length >= 3', () => {
|
|
10
|
+
expect(extractTerms('login error')).toEqual(['login', 'error']);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('filters out terms shorter than 3 characters', () => {
|
|
14
|
+
expect(extractTerms('a db login')).toEqual(['login']);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('deduplicates terms case-insensitively', () => {
|
|
18
|
+
const terms = extractTerms('Login login LOGIN');
|
|
19
|
+
expect(terms).toHaveLength(1);
|
|
20
|
+
expect(terms[0]!.toLowerCase()).toBe('login');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('sorts longest terms first to prevent partial-match overlap', () => {
|
|
24
|
+
const terms = extractTerms('err error errors');
|
|
25
|
+
expect(terms[0]).toBe('errors');
|
|
26
|
+
expect(terms[1]).toBe('error');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('returns empty array for empty query', () => {
|
|
30
|
+
expect(extractTerms('')).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns empty array when all terms are too short', () => {
|
|
34
|
+
expect(extractTerms('a ab')).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('trims whitespace', () => {
|
|
38
|
+
expect(extractTerms(' login error ')).toEqual(['login', 'error']);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// highlightText
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
describe('highlightText', () => {
|
|
47
|
+
it('wraps a matching term in **bold**', () => {
|
|
48
|
+
expect(highlightText('Login failed', ['login'])).toBe('**Login** failed');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('is case-insensitive', () => {
|
|
52
|
+
expect(highlightText('CRASH on startup', ['crash'])).toBe('**CRASH** on startup');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('highlights multiple terms', () => {
|
|
56
|
+
const result = highlightText('Login error occurred', ['login', 'error']);
|
|
57
|
+
expect(result).toBe('**Login** **error** occurred');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns original text when no terms match', () => {
|
|
61
|
+
expect(highlightText('Something unrelated', ['crash'])).toBe('Something unrelated');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns original text when terms array is empty', () => {
|
|
65
|
+
expect(highlightText('Login failed', [])).toBe('Login failed');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('does not match term as substring within a word (word-boundary-aware)', () => {
|
|
69
|
+
// "or" should NOT match inside "error"
|
|
70
|
+
expect(highlightText('error occurred', ['or'])).toBe('error occurred');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('escapes special regex characters in terms', () => {
|
|
74
|
+
// Term with special regex chars should not throw
|
|
75
|
+
expect(() => highlightText('test (foo)', ['(foo)'])).not.toThrow();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('highlights all occurrences of a term', () => {
|
|
79
|
+
const result = highlightText('login and login again', ['login']);
|
|
80
|
+
expect(result).toBe('**login** and **login** again');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// extractSnippet
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
describe('extractSnippet', () => {
|
|
89
|
+
it('returns full text highlighted when text is short', () => {
|
|
90
|
+
const text = 'Login error on startup';
|
|
91
|
+
const result = extractSnippet(text, ['login']);
|
|
92
|
+
expect(result).toBe('**Login** error on startup');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('returns first ~300 chars around the first match for long text', () => {
|
|
96
|
+
const prefix = 'x'.repeat(200);
|
|
97
|
+
const text = `${prefix} login error ${' unrelated text'.repeat(30)}`;
|
|
98
|
+
const result = extractSnippet(text, ['login']);
|
|
99
|
+
expect(result).toContain('**login**');
|
|
100
|
+
expect(result.length).toBeLessThanOrEqual(350); // snippet + some overhead
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('centers snippet around first match', () => {
|
|
104
|
+
const padding = 'word '.repeat(60); // ~300 chars before the match
|
|
105
|
+
const text = `${padding}crash happens here ${'and more text '.repeat(30)}`;
|
|
106
|
+
const result = extractSnippet(text, ['crash']);
|
|
107
|
+
expect(result).toContain('**crash**');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('returns first 300 chars (no highlight) when no term matches', () => {
|
|
111
|
+
const text = 'a'.repeat(600);
|
|
112
|
+
const result = extractSnippet(text, ['nomatch']);
|
|
113
|
+
expect(result).toBe('a'.repeat(300) + '…');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('returns full text (no truncation) when text is shorter than 300 chars and no match', () => {
|
|
117
|
+
const text = 'Short text with no match';
|
|
118
|
+
const result = extractSnippet(text, ['nomatch']);
|
|
119
|
+
expect(result).toBe(text);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('respects custom contextChars parameter', () => {
|
|
123
|
+
const padding = 'x'.repeat(100);
|
|
124
|
+
const text = `${padding} crash ${padding}`;
|
|
125
|
+
const result = extractSnippet(text, ['crash'], 50);
|
|
126
|
+
expect(result).toContain('**crash**');
|
|
127
|
+
expect(result.length).toBeLessThanOrEqual(150);
|
|
128
|
+
});
|
|
129
|
+
});
|