@dpesch/mantisbt-mcp-server 1.4.0 → 1.5.1
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/.env.local +3 -0
- package/CHANGELOG.md +18 -0
- package/README.de.md +5 -4
- package/README.md +5 -4
- package/dist/config.js +2 -0
- package/dist/constants.js +77 -0
- package/dist/index.js +1 -1
- package/dist/search/embedder.js +10 -3
- package/dist/search/index.js +1 -1
- package/dist/search/tools.js +27 -2
- package/dist/tools/config.js +16 -2
- package/dist/tools/issues.js +46 -6
- package/dist/tools/relationships.js +27 -11
- package/package.json +1 -1
- package/tests/fixtures/recorded/get_current_user.json +108 -0
- package/tests/fixtures/recorded/get_issue.json +138 -0
- package/tests/fixtures/recorded/get_issue_enums.json +67 -0
- package/tests/fixtures/recorded/get_issue_fields_sample.json +138 -0
- package/tests/fixtures/recorded/get_project_categories.json +241 -0
- package/tests/fixtures/recorded/get_project_versions.json +28 -0
- package/tests/fixtures/recorded/list_issues.json +463 -0
- package/tests/fixtures/recorded/list_projects.json +10641 -0
- package/tests/search/embedder.test.ts +81 -0
- package/tests/search/tools.test.ts +117 -0
- package/tests/tools/config.test.ts +71 -2
- package/tests/tools/issues.test.ts +156 -1
- package/tests/tools/relationships.test.ts +75 -0
- package/tests/tools/string-coercion.test.ts +3 -1
package/.env.local
ADDED
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.5.1] – 2026-03-17
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Semantic search: ONNX thread pool now defaults to 1 thread (`intra_op_num_threads=1`) instead of auto-detecting all available CPU cores. On WSL and multi-core machines the unrestricted default caused CPU saturation (700%+ CPU, 12 GB VM) during the initial index build. The number of threads is configurable via the new `MANTIS_SEARCH_THREADS` environment variable (default: `1`). `inter_op_num_threads` is always kept at 1 because Transformer model graphs are sequential and inter-op parallelism provides no benefit.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## [1.5.0] – 2026-03-17
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- `create_issue`: always returns the complete issue object. Older MantisBT versions returned only `{ id: N }` on `POST /issues`; the tool now detects this and performs an automatic `GET /issues/{id}` to retrieve the full issue. If that fetch fails the minimal object is returned instead (the issue was already created).
|
|
21
|
+
- `get_issue_enums`: each entry now includes an optional `canonical_name` field containing the standard English API name (e.g. `"minor"`, `"block"`) when the returned `name` differs from it. This occurs on localized MantisBT installations where enum values have been customized at the database level (e.g. `name: "kleinerer Fehler"` instead of `"minor"`). The field is omitted when `name` already matches the canonical value and for custom entries without a known canonical name. Available for all five enum groups (severity, status, priority, resolution, reproducibility).
|
|
22
|
+
- `create_issue`: new optional `handler` parameter accepts a username string as alternative to `handler_id`. The server resolves the name against the project members list (from the metadata cache when available, otherwise via a direct API call). If the username is not found, an error is returned with a list of available users.
|
|
23
|
+
- `add_relationship`: new optional `type_name` parameter accepts string names as alternative to the numeric `type_id` (e.g. `"related_to"`, `"duplicate_of"`, `"depends_on"`, `"blocks"`). Dash variants (`"related-to"`) are also accepted. `type_id` becomes optional — at least one of the two must be provided; `type_id` takes precedence when both are given.
|
|
24
|
+
- `search_issues`: new optional `select` parameter. When provided, each matching issue is fetched from MantisBT and the response is enriched with the requested fields (comma-separated, e.g. `"id,summary,status,handler,priority"`). Without `select` the behaviour is unchanged — only `id` and `score` are returned. `id` and `score` are always included regardless of the `select` value. If an individual issue fetch fails, that result falls back silently to `{id, score}`.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
10
28
|
## [1.4.0] – 2026-03-17
|
|
11
29
|
|
|
12
30
|
### Added
|
package/README.de.md
CHANGED
|
@@ -73,6 +73,7 @@ npm run build
|
|
|
73
73
|
| `MANTIS_SEARCH_BACKEND` | – | `vectra` | Vektorspeicher: `vectra` (reines JS) oder `sqlite-vec` (manuelle Installation erforderlich) |
|
|
74
74
|
| `MANTIS_SEARCH_DIR` | – | `{MANTIS_CACHE_DIR}/search` | Verzeichnis für den Suchindex |
|
|
75
75
|
| `MANTIS_SEARCH_MODEL` | – | `Xenova/paraphrase-multilingual-MiniLM-L12-v2` | Embedding-Modell (wird beim ersten Start einmalig heruntergeladen, ~80 MB) |
|
|
76
|
+
| `MANTIS_SEARCH_THREADS` | – | `1` | Anzahl der ONNX-Intra-Op-Threads für das Embedding-Modell. Standard ist 1, um CPU-Sättigung auf Mehrkernsystemen und in WSL zu verhindern. Nur erhöhen, wenn die Indexierungsgeschwindigkeit kritisch ist und der Host ausschließlich für diese Last vorgesehen ist. |
|
|
76
77
|
|
|
77
78
|
### Config-Datei (Fallback)
|
|
78
79
|
|
|
@@ -93,7 +94,7 @@ Falls keine Umgebungsvariablen gesetzt sind, wird `~/.claude/mantis.json` ausgel
|
|
|
93
94
|
|---|---|
|
|
94
95
|
| `get_issue` | Ein Issue anhand seiner ID abrufen |
|
|
95
96
|
| `list_issues` | Issues nach Projekt, Status, Autor u.v.m. filtern; optionales `select` für Feldprojektion und `status` für clientseitige Statusfilterung |
|
|
96
|
-
| `create_issue` | Neues Issue anlegen |
|
|
97
|
+
| `create_issue` | Neues Issue anlegen; optionaler `handler`-Parameter akzeptiert einen Benutzernamen als Alternative zu `handler_id` (wird gegen die Projektmitglieder aufgelöst) |
|
|
97
98
|
| `update_issue` | Bestehendes Issue bearbeiten |
|
|
98
99
|
| `delete_issue` | Issue löschen |
|
|
99
100
|
|
|
@@ -116,7 +117,7 @@ Falls keine Umgebungsvariablen gesetzt sind, wird `~/.claude/mantis.json` ausgel
|
|
|
116
117
|
|
|
117
118
|
| Tool | Beschreibung |
|
|
118
119
|
|---|---|
|
|
119
|
-
| `add_relationship` | Beziehung zwischen zwei Issues erstellen |
|
|
120
|
+
| `add_relationship` | Beziehung zwischen zwei Issues erstellen; optionaler `type_name`-Parameter akzeptiert einen String-Namen (z.B. `"related_to"`, `"duplicate_of"`) als Alternative zur numerischen `type_id` |
|
|
120
121
|
| `remove_relationship` | Beziehung von einem Issue entfernen (die `id` aus dem Beziehungsobjekt verwenden, nicht die type-ID) |
|
|
121
122
|
|
|
122
123
|
### Beobachter
|
|
@@ -157,7 +158,7 @@ Aktivierung mit `MANTIS_SEARCH_ENABLED=true`.
|
|
|
157
158
|
|
|
158
159
|
| Tool | Beschreibung |
|
|
159
160
|
|---|---|
|
|
160
|
-
| `search_issues` | Natürlichsprachige Suche über alle indizierten Issues – liefert Top-N-Ergebnisse mit Cosine-Similarity-Score |
|
|
161
|
+
| `search_issues` | Natürlichsprachige Suche über alle indizierten Issues – liefert Top-N-Ergebnisse mit Cosine-Similarity-Score; optionales `select` (kommagetrennte Feldnamen) reichert jedes Ergebnis mit den angeforderten Issue-Feldern an |
|
|
161
162
|
| `rebuild_search_index` | Suchindex aufbauen oder aktualisieren; `full: true` löscht und baut ihn vollständig neu |
|
|
162
163
|
| `get_search_index_status` | Aktuellen Füllstand des Suchindex zurückgeben: wie viele Issues bereits indiziert sind im Verhältnis zur Gesamtanzahl, plus Zeitstempel der letzten Synchronisation |
|
|
163
164
|
|
|
@@ -188,7 +189,7 @@ npm install sqlite-vec better-sqlite3
|
|
|
188
189
|
| `get_current_user` | Eigenes Benutzerprofil abrufen |
|
|
189
190
|
| `list_languages` | Verfügbare Sprachen auflisten |
|
|
190
191
|
| `get_config` | Server-Konfiguration (Basis-URL, Cache-TTL) anzeigen |
|
|
191
|
-
| `get_issue_enums` | Gültige ID/Name-Paare für alle Enum-Felder zurückgeben (Severity, Status, Priority, Resolution, Reproducibility) — vor `create_issue` / `update_issue` verwenden, um korrekte Werte nachzuschlagen |
|
|
192
|
+
| `get_issue_enums` | Gültige ID/Name-Paare für alle Enum-Felder zurückgeben (Severity, Status, Priority, Resolution, Reproducibility) — vor `create_issue` / `update_issue` verwenden, um korrekte Werte nachzuschlagen; auf lokalisierten Installationen kann jeder Eintrag ein `canonical_name`-Feld mit dem englischen Standard-API-Namen enthalten |
|
|
192
193
|
| `get_mantis_version` | MantisBT-Version abrufen und auf Updates prüfen |
|
|
193
194
|
| `get_mcp_version` | Version dieser mantisbt-mcp-server-Instanz zurückgeben |
|
|
194
195
|
|
package/README.md
CHANGED
|
@@ -73,6 +73,7 @@ npm run build
|
|
|
73
73
|
| `MANTIS_SEARCH_BACKEND` | – | `vectra` | Vector store backend: `vectra` (pure JS) or `sqlite-vec` (requires manual install) |
|
|
74
74
|
| `MANTIS_SEARCH_DIR` | – | `{MANTIS_CACHE_DIR}/search` | Directory for the search index |
|
|
75
75
|
| `MANTIS_SEARCH_MODEL` | – | `Xenova/paraphrase-multilingual-MiniLM-L12-v2` | Embedding model name (downloaded once on first use, ~80 MB) |
|
|
76
|
+
| `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. Increase only if index rebuild speed matters and the host is dedicated to this workload. |
|
|
76
77
|
|
|
77
78
|
### Config file (fallback)
|
|
78
79
|
|
|
@@ -93,7 +94,7 @@ If no environment variables are set, `~/.claude/mantis.json` is read:
|
|
|
93
94
|
|---|---|
|
|
94
95
|
| `get_issue` | Retrieve an issue by its numeric ID |
|
|
95
96
|
| `list_issues` | Filter issues by project, status, author, and more; optional `select` for field projection and `status` for client-side status filtering |
|
|
96
|
-
| `create_issue` | Create a new issue |
|
|
97
|
+
| `create_issue` | Create a new issue; optional `handler` parameter accepts a username as alternative to `handler_id` (resolved against project members) |
|
|
97
98
|
| `update_issue` | Update an existing issue |
|
|
98
99
|
| `delete_issue` | Delete an issue |
|
|
99
100
|
|
|
@@ -116,7 +117,7 @@ If no environment variables are set, `~/.claude/mantis.json` is read:
|
|
|
116
117
|
|
|
117
118
|
| Tool | Description |
|
|
118
119
|
|---|---|
|
|
119
|
-
| `add_relationship` | Create a relationship between two issues |
|
|
120
|
+
| `add_relationship` | Create a relationship between two issues; optional `type_name` parameter accepts a string name (e.g. `"related_to"`, `"duplicate_of"`) as alternative to numeric `type_id` |
|
|
120
121
|
| `remove_relationship` | Remove a relationship from an issue (use the `id` from the relationship object, not the type) |
|
|
121
122
|
|
|
122
123
|
### Monitors
|
|
@@ -157,7 +158,7 @@ Activate with `MANTIS_SEARCH_ENABLED=true`.
|
|
|
157
158
|
|
|
158
159
|
| Tool | Description |
|
|
159
160
|
|---|---|
|
|
160
|
-
| `search_issues` | Natural language search over all indexed issues — returns top-N results with cosine similarity score |
|
|
161
|
+
| `search_issues` | Natural language search over all indexed issues — returns top-N results with cosine similarity score; optional `select` (comma-separated field names) enriches each result with the requested issue fields |
|
|
161
162
|
| `rebuild_search_index` | Build or update the search index; `full: true` clears and rebuilds from scratch |
|
|
162
163
|
| `get_search_index_status` | Return the current fill level of the search index: how many issues are indexed vs. total, and the timestamp of the last sync |
|
|
163
164
|
|
|
@@ -188,7 +189,7 @@ npm install sqlite-vec better-sqlite3
|
|
|
188
189
|
| `get_current_user` | Retrieve your own user profile |
|
|
189
190
|
| `list_languages` | List available languages |
|
|
190
191
|
| `get_config` | Show server configuration (base URL, cache TTL) |
|
|
191
|
-
| `get_issue_enums` | Return valid ID/name pairs for all issue enum fields (severity, status, priority, resolution, reproducibility) — use before `create_issue` / `update_issue` to look up correct values |
|
|
192
|
+
| `get_issue_enums` | Return valid ID/name pairs for all issue enum fields (severity, status, priority, resolution, reproducibility) — use before `create_issue` / `update_issue` to look up correct values; on localized installations each entry may include a `canonical_name` with the standard English API name |
|
|
192
193
|
| `get_mantis_version` | Get MantisBT version and check for updates |
|
|
193
194
|
| `get_mcp_version` | Return the version of this mantisbt-mcp-server instance |
|
|
194
195
|
|
package/dist/config.js
CHANGED
|
@@ -76,6 +76,7 @@ export async function getConfig() {
|
|
|
76
76
|
const searchDir = process.env.MANTIS_SEARCH_DIR ?? join(cacheDir, 'search');
|
|
77
77
|
const searchModelName = process.env.MANTIS_SEARCH_MODEL ??
|
|
78
78
|
'Xenova/paraphrase-multilingual-MiniLM-L12-v2';
|
|
79
|
+
const searchNumThreads = Math.max(1, parseInt(process.env.MANTIS_SEARCH_THREADS ?? '', 10) || 1);
|
|
79
80
|
cachedConfig = {
|
|
80
81
|
baseUrl: baseUrl.replace(/\/$/, ''), // strip trailing slash
|
|
81
82
|
apiKey,
|
|
@@ -86,6 +87,7 @@ export async function getConfig() {
|
|
|
86
87
|
backend: searchBackend,
|
|
87
88
|
dir: searchDir,
|
|
88
89
|
modelName: searchModelName,
|
|
90
|
+
numThreads: searchNumThreads,
|
|
89
91
|
},
|
|
90
92
|
};
|
|
91
93
|
return cachedConfig;
|
package/dist/constants.js
CHANGED
|
@@ -10,12 +10,89 @@ export const RELATIONSHIP_TYPES = {
|
|
|
10
10
|
HAS_DUPLICATE: 4,
|
|
11
11
|
};
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
|
+
// Relationship type name → ID mapping (string aliases accepted by add_relationship)
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
export const RELATIONSHIP_NAME_TO_ID = {
|
|
16
|
+
'duplicate_of': RELATIONSHIP_TYPES.DUPLICATE_OF,
|
|
17
|
+
'duplicate-of': RELATIONSHIP_TYPES.DUPLICATE_OF,
|
|
18
|
+
'duplicateof': RELATIONSHIP_TYPES.DUPLICATE_OF,
|
|
19
|
+
'related_to': RELATIONSHIP_TYPES.RELATED_TO,
|
|
20
|
+
'related-to': RELATIONSHIP_TYPES.RELATED_TO,
|
|
21
|
+
'relatedto': RELATIONSHIP_TYPES.RELATED_TO,
|
|
22
|
+
'parent_of': RELATIONSHIP_TYPES.PARENT_OF,
|
|
23
|
+
'parent-of': RELATIONSHIP_TYPES.PARENT_OF,
|
|
24
|
+
'parentof': RELATIONSHIP_TYPES.PARENT_OF,
|
|
25
|
+
'depends_on': RELATIONSHIP_TYPES.PARENT_OF,
|
|
26
|
+
'depends-on': RELATIONSHIP_TYPES.PARENT_OF,
|
|
27
|
+
'dependson': RELATIONSHIP_TYPES.PARENT_OF,
|
|
28
|
+
'child_of': RELATIONSHIP_TYPES.CHILD_OF,
|
|
29
|
+
'child-of': RELATIONSHIP_TYPES.CHILD_OF,
|
|
30
|
+
'childof': RELATIONSHIP_TYPES.CHILD_OF,
|
|
31
|
+
'blocks': RELATIONSHIP_TYPES.CHILD_OF,
|
|
32
|
+
'has_duplicate': RELATIONSHIP_TYPES.HAS_DUPLICATE,
|
|
33
|
+
'has-duplicate': RELATIONSHIP_TYPES.HAS_DUPLICATE,
|
|
34
|
+
'hasduplicate': RELATIONSHIP_TYPES.HAS_DUPLICATE,
|
|
35
|
+
};
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
13
37
|
// Status names (internal English names used in API calls)
|
|
14
38
|
// ---------------------------------------------------------------------------
|
|
15
39
|
// MantisBT default status ID for "resolved". Issues with status.id strictly
|
|
16
40
|
// below this value are considered open (new/feedback/acknowledged/confirmed/assigned).
|
|
17
41
|
export const MANTIS_RESOLVED_STATUS_ID = 80;
|
|
18
42
|
// ---------------------------------------------------------------------------
|
|
43
|
+
// Canonical English enum names for standard MantisBT installations.
|
|
44
|
+
// Keyed by the enum group name (without _enum_string suffix).
|
|
45
|
+
// Used by get_issue_enums to add a canonical_name field on localized installs.
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
export const MANTIS_CANONICAL_ENUM_NAMES = {
|
|
48
|
+
severity: {
|
|
49
|
+
10: 'feature',
|
|
50
|
+
20: 'trivial',
|
|
51
|
+
30: 'text',
|
|
52
|
+
40: 'tweak',
|
|
53
|
+
50: 'minor',
|
|
54
|
+
60: 'major',
|
|
55
|
+
70: 'crash',
|
|
56
|
+
80: 'block',
|
|
57
|
+
},
|
|
58
|
+
status: {
|
|
59
|
+
10: 'new',
|
|
60
|
+
20: 'feedback',
|
|
61
|
+
30: 'acknowledged',
|
|
62
|
+
40: 'confirmed',
|
|
63
|
+
50: 'assigned',
|
|
64
|
+
80: 'resolved',
|
|
65
|
+
90: 'closed',
|
|
66
|
+
},
|
|
67
|
+
priority: {
|
|
68
|
+
10: 'none',
|
|
69
|
+
20: 'low',
|
|
70
|
+
30: 'normal',
|
|
71
|
+
40: 'high',
|
|
72
|
+
50: 'urgent',
|
|
73
|
+
60: 'immediate',
|
|
74
|
+
},
|
|
75
|
+
resolution: {
|
|
76
|
+
10: 'open',
|
|
77
|
+
20: 'fixed',
|
|
78
|
+
30: 'reopened',
|
|
79
|
+
40: 'unable to duplicate',
|
|
80
|
+
50: 'not fixable',
|
|
81
|
+
60: 'duplicate',
|
|
82
|
+
70: 'no change required',
|
|
83
|
+
80: 'suspended',
|
|
84
|
+
90: 'wont fix',
|
|
85
|
+
},
|
|
86
|
+
reproducibility: {
|
|
87
|
+
10: 'always',
|
|
88
|
+
30: 'sometimes',
|
|
89
|
+
50: 'random',
|
|
90
|
+
70: 'have not tried',
|
|
91
|
+
90: 'unable to reproduce',
|
|
92
|
+
100: 'N/A',
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
19
96
|
// Issue enum config option names
|
|
20
97
|
// ---------------------------------------------------------------------------
|
|
21
98
|
export const ISSUE_ENUM_OPTIONS = [
|
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ async function createMcpServer() {
|
|
|
42
42
|
name: 'mantisbt-mcp-server',
|
|
43
43
|
version,
|
|
44
44
|
});
|
|
45
|
-
registerIssueTools(server, client);
|
|
45
|
+
registerIssueTools(server, client, cache);
|
|
46
46
|
registerNoteTools(server, client);
|
|
47
47
|
registerFileTools(server, client);
|
|
48
48
|
registerRelationshipTools(server, client);
|
package/dist/search/embedder.js
CHANGED
|
@@ -8,14 +8,16 @@
|
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
export class Embedder {
|
|
10
10
|
modelName;
|
|
11
|
+
numThreads;
|
|
11
12
|
pipe = null;
|
|
12
|
-
constructor(modelName) {
|
|
13
|
+
constructor(modelName, numThreads = 1) {
|
|
13
14
|
this.modelName = modelName;
|
|
15
|
+
this.numThreads = numThreads;
|
|
14
16
|
}
|
|
15
17
|
async load() {
|
|
16
18
|
if (this.pipe)
|
|
17
19
|
return this.pipe;
|
|
18
|
-
process.stderr.write(`[mantisbt-search] Loading embedding model ${this.modelName}...\n`);
|
|
20
|
+
process.stderr.write(`[mantisbt-search] Loading embedding model ${this.modelName} (threads: ${this.numThreads})...\n`);
|
|
19
21
|
let transformers;
|
|
20
22
|
try {
|
|
21
23
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
@@ -25,7 +27,12 @@ export class Embedder {
|
|
|
25
27
|
const msg = err instanceof Error ? err.message : String(err);
|
|
26
28
|
throw new Error(`Failed to load @huggingface/transformers: ${msg}`);
|
|
27
29
|
}
|
|
28
|
-
this.pipe = await transformers.pipeline('feature-extraction', this.modelName
|
|
30
|
+
this.pipe = await transformers.pipeline('feature-extraction', this.modelName, {
|
|
31
|
+
session_options: {
|
|
32
|
+
intra_op_num_threads: this.numThreads,
|
|
33
|
+
inter_op_num_threads: 1, // Transformer graphs are sequential — no benefit from inter-op parallelism
|
|
34
|
+
},
|
|
35
|
+
});
|
|
29
36
|
return this.pipe;
|
|
30
37
|
}
|
|
31
38
|
async embed(text) {
|
package/dist/search/index.js
CHANGED
|
@@ -9,7 +9,7 @@ export async function initializeSearchModule(server, client, config) {
|
|
|
9
9
|
if (!config.enabled)
|
|
10
10
|
return;
|
|
11
11
|
const store = createVectorStore(config.backend, config.dir);
|
|
12
|
-
const embedder = new Embedder(config.modelName);
|
|
12
|
+
const embedder = new Embedder(config.modelName, config.numThreads);
|
|
13
13
|
registerSearchTools(server, client, store, embedder);
|
|
14
14
|
// Pre-initialize lastKnownTotal so get_search_index_status shows a value
|
|
15
15
|
// immediately on startup, even while the background sync is still running.
|
package/dist/search/tools.js
CHANGED
|
@@ -27,13 +27,16 @@ export function registerSearchTools(server, client, store, embedder) {
|
|
|
27
27
|
.max(50)
|
|
28
28
|
.default(10)
|
|
29
29
|
.describe('Number of results to return (default: 10, max: 50)'),
|
|
30
|
+
select: z.string().optional().describe('Comma-separated list of fields to include for each result (e.g. "id,summary,status,handler,priority"). ' +
|
|
31
|
+
'When provided, each matching issue is fetched from MantisBT and enriched with the requested fields. ' +
|
|
32
|
+
'The relevance score is always included. Without this parameter only id and score are returned.'),
|
|
30
33
|
}),
|
|
31
34
|
annotations: {
|
|
32
35
|
readOnlyHint: true,
|
|
33
36
|
destructiveHint: false,
|
|
34
37
|
idempotentHint: true,
|
|
35
38
|
},
|
|
36
|
-
}, async ({ query, top_n }) => {
|
|
39
|
+
}, async ({ query, top_n, select }) => {
|
|
37
40
|
try {
|
|
38
41
|
const count = await store.count();
|
|
39
42
|
if (count === 0) {
|
|
@@ -49,8 +52,30 @@ export function registerSearchTools(server, client, store, embedder) {
|
|
|
49
52
|
}
|
|
50
53
|
const queryVector = await embedder.embed(query);
|
|
51
54
|
const results = await store.search(queryVector, top_n);
|
|
55
|
+
if (!select) {
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const fields = select.split(',').map(f => f.trim()).filter(Boolean);
|
|
61
|
+
const enriched = await Promise.all(results.map(async ({ id, score }) => {
|
|
62
|
+
try {
|
|
63
|
+
const issueResult = await client.get(`issues/${id}`);
|
|
64
|
+
const issue = issueResult.issues?.[0] ?? {};
|
|
65
|
+
const projected = { id, score };
|
|
66
|
+
for (const field of fields) {
|
|
67
|
+
if (field !== 'id' && field in issue) {
|
|
68
|
+
projected[field] = issue[field];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return projected;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return { id, score };
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
52
77
|
return {
|
|
53
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
78
|
+
content: [{ type: 'text', text: JSON.stringify(enriched, null, 2) }],
|
|
54
79
|
};
|
|
55
80
|
}
|
|
56
81
|
catch (error) {
|
package/dist/tools/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { ISSUE_ENUM_OPTIONS } from '../constants.js';
|
|
2
|
+
import { ISSUE_ENUM_OPTIONS, MANTIS_CANONICAL_ENUM_NAMES } from '../constants.js';
|
|
3
3
|
import { getVersionHint } from '../version-hint.js';
|
|
4
4
|
function errorText(msg) {
|
|
5
5
|
const vh = getVersionHint();
|
|
@@ -21,6 +21,10 @@ function parseEnumString(raw) {
|
|
|
21
21
|
})
|
|
22
22
|
.filter((e) => e !== null);
|
|
23
23
|
}
|
|
24
|
+
function resolveCanonicalName(id, name, canonicalMap) {
|
|
25
|
+
const canonical = canonicalMap[id];
|
|
26
|
+
return canonical !== undefined && canonical !== name ? canonical : undefined;
|
|
27
|
+
}
|
|
24
28
|
export function registerConfigTools(server, client, cache) {
|
|
25
29
|
// ---------------------------------------------------------------------------
|
|
26
30
|
// get_config
|
|
@@ -130,14 +134,24 @@ to use for API calls — regardless of language.`,
|
|
|
130
134
|
const key = keyMap[option];
|
|
131
135
|
if (!key)
|
|
132
136
|
continue;
|
|
137
|
+
const canonicalMap = MANTIS_CANONICAL_ENUM_NAMES[key] ?? {};
|
|
133
138
|
if (typeof value === 'string') {
|
|
134
|
-
enums[key] = parseEnumString(value)
|
|
139
|
+
enums[key] = parseEnumString(value).map(({ id, name }) => {
|
|
140
|
+
const entry = { id, name };
|
|
141
|
+
const canonical_name = resolveCanonicalName(id, name, canonicalMap);
|
|
142
|
+
if (canonical_name !== undefined)
|
|
143
|
+
entry.canonical_name = canonical_name;
|
|
144
|
+
return entry;
|
|
145
|
+
});
|
|
135
146
|
}
|
|
136
147
|
else if (Array.isArray(value)) {
|
|
137
148
|
enums[key] = value.map(({ id, name, label }) => {
|
|
138
149
|
const entry = { id, name };
|
|
139
150
|
if (label && label !== name)
|
|
140
151
|
entry.label = label;
|
|
152
|
+
const canonical_name = resolveCanonicalName(id, name, canonicalMap);
|
|
153
|
+
if (canonical_name !== undefined)
|
|
154
|
+
entry.canonical_name = canonical_name;
|
|
141
155
|
return entry;
|
|
142
156
|
});
|
|
143
157
|
}
|
package/dist/tools/issues.js
CHANGED
|
@@ -7,7 +7,7 @@ function errorText(msg) {
|
|
|
7
7
|
const hint = vh?.getUpdateHint();
|
|
8
8
|
return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
|
|
9
9
|
}
|
|
10
|
-
export function registerIssueTools(server, client) {
|
|
10
|
+
export function registerIssueTools(server, client, cache) {
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
12
|
// get_issue
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
@@ -141,13 +141,38 @@ export function registerIssueTools(server, client) {
|
|
|
141
141
|
priority: z.string().optional().describe('Priority name (e.g. "normal", "high", "urgent", "immediate", "low", "none")'),
|
|
142
142
|
severity: z.string().default('minor').describe('Severity name (e.g. "minor", "major", "crash", "block", "feature", "trivial", "text") — default: "minor"'),
|
|
143
143
|
handler_id: z.coerce.number().int().positive().optional().describe('User ID of the person to assign the issue to'),
|
|
144
|
+
handler: z.string().optional().describe('Username (login name) of the person to assign the issue to. Alternative to handler_id — the server resolves the name to a user ID from the project members. Use get_project_users to see available users.'),
|
|
144
145
|
}),
|
|
145
146
|
annotations: {
|
|
146
147
|
readOnlyHint: false,
|
|
147
148
|
destructiveHint: false,
|
|
148
149
|
idempotentHint: false,
|
|
149
150
|
},
|
|
150
|
-
}, async ({ summary, description, project_id, category, priority, severity, handler_id }) => {
|
|
151
|
+
}, async ({ summary, description, project_id, category, priority, severity, handler_id, handler }) => {
|
|
152
|
+
// Resolve handler username to handler_id when only a name is given
|
|
153
|
+
let resolvedHandlerId = handler_id;
|
|
154
|
+
if (resolvedHandlerId === undefined && handler !== undefined) {
|
|
155
|
+
const metadata = await cache.loadIfValid();
|
|
156
|
+
let users = metadata?.byProject[project_id]?.users ?? [];
|
|
157
|
+
if (users.length === 0) {
|
|
158
|
+
try {
|
|
159
|
+
const usersResult = await client.get(`projects/${project_id}/users`);
|
|
160
|
+
users = usersResult.users ?? [];
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
users = [];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const user = users.find(u => u.name === handler || u.real_name === handler);
|
|
167
|
+
if (!user) {
|
|
168
|
+
const names = users.map(u => u.name).join(', ');
|
|
169
|
+
return {
|
|
170
|
+
content: [{ type: 'text', text: errorText(`User "${handler}" not found in project ${project_id}. Available users: ${names || 'none (run sync_metadata or check project_id)'}`) }],
|
|
171
|
+
isError: true,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
resolvedHandlerId = user.id;
|
|
175
|
+
}
|
|
151
176
|
try {
|
|
152
177
|
const body = {
|
|
153
178
|
summary,
|
|
@@ -158,11 +183,26 @@ export function registerIssueTools(server, client) {
|
|
|
158
183
|
if (priority)
|
|
159
184
|
body.priority = { name: priority };
|
|
160
185
|
body.severity = { name: severity };
|
|
161
|
-
if (
|
|
162
|
-
body.handler = { id:
|
|
163
|
-
const
|
|
186
|
+
if (resolvedHandlerId)
|
|
187
|
+
body.handler = { id: resolvedHandlerId };
|
|
188
|
+
const raw = await client.post('issues', body);
|
|
189
|
+
const partial = ('issue' in raw && typeof raw['issue'] === 'object' && raw['issue'] !== null)
|
|
190
|
+
? raw['issue']
|
|
191
|
+
: raw;
|
|
192
|
+
let issue = partial;
|
|
193
|
+
if (!('summary' in partial)) {
|
|
194
|
+
// Older MantisBT returned only { id: N } — fetch the full issue.
|
|
195
|
+
// Suppress GET errors: the issue was already created.
|
|
196
|
+
try {
|
|
197
|
+
const fetched = await client.get(`issues/${partial.id}`);
|
|
198
|
+
issue = fetched.issues?.[0] ?? partial;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// unable to fetch details — return minimal object
|
|
202
|
+
}
|
|
203
|
+
}
|
|
164
204
|
return {
|
|
165
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
205
|
+
content: [{ type: 'text', text: JSON.stringify(issue, null, 2) }],
|
|
166
206
|
};
|
|
167
207
|
}
|
|
168
208
|
catch (error) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { RELATIONSHIP_TYPES } from '../constants.js';
|
|
2
|
+
import { RELATIONSHIP_TYPES, RELATIONSHIP_NAME_TO_ID } from '../constants.js';
|
|
3
3
|
import { getVersionHint } from '../version-hint.js';
|
|
4
4
|
function errorText(msg) {
|
|
5
5
|
const vh = getVersionHint();
|
|
@@ -15,31 +15,47 @@ export function registerRelationshipTools(server, client) {
|
|
|
15
15
|
title: 'Add Issue Relationship',
|
|
16
16
|
description: `Add a relationship between two MantisBT issues.
|
|
17
17
|
|
|
18
|
-
Relationship
|
|
19
|
-
- ${RELATIONSHIP_TYPES.DUPLICATE_OF}
|
|
20
|
-
- ${RELATIONSHIP_TYPES.RELATED_TO}
|
|
21
|
-
- ${RELATIONSHIP_TYPES.PARENT_OF}
|
|
22
|
-
- ${RELATIONSHIP_TYPES.CHILD_OF}
|
|
23
|
-
- ${RELATIONSHIP_TYPES.HAS_DUPLICATE}
|
|
18
|
+
Relationship types — use either type_id (numeric) or type_name (string):
|
|
19
|
+
- ${RELATIONSHIP_TYPES.DUPLICATE_OF} / "duplicate_of" — this issue is a duplicate of target
|
|
20
|
+
- ${RELATIONSHIP_TYPES.RELATED_TO} / "related_to" — this issue is related to target
|
|
21
|
+
- ${RELATIONSHIP_TYPES.PARENT_OF} / "parent_of" — this issue depends on target (target must be done first); alias: "depends_on"
|
|
22
|
+
- ${RELATIONSHIP_TYPES.CHILD_OF} / "child_of" — this issue blocks target (target can't proceed until this is done); alias: "blocks"
|
|
23
|
+
- ${RELATIONSHIP_TYPES.HAS_DUPLICATE} / "has_duplicate" — this issue has target as a duplicate
|
|
24
24
|
|
|
25
25
|
Directionality note: "A child_of B" means A blocks B. "A parent_of B" means A depends on B.
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Dash variants (e.g. "related-to") are also accepted for type_name.`,
|
|
28
28
|
inputSchema: z.object({
|
|
29
29
|
issue_id: z.coerce.number().int().positive().describe('The source issue ID (the one the relationship is added to)'),
|
|
30
30
|
target_id: z.coerce.number().int().positive().describe('The target issue ID'),
|
|
31
|
-
type_id: z.coerce.number().int().min(0).max(4).describe(
|
|
31
|
+
type_id: z.coerce.number().int().min(0).max(4).optional().describe('Relationship type ID: 0=duplicate_of, 1=related_to, 2=parent_of (depends on), 3=child_of (blocks), 4=has_duplicate. Use either type_id or type_name.'),
|
|
32
|
+
type_name: z.string().optional().describe('Relationship type name as alternative to type_id. Accepted: "duplicate_of", "related_to", "parent_of" (or "depends_on"), "child_of" (or "blocks"), "has_duplicate". Dash variants (e.g. "related-to") also work.'),
|
|
32
33
|
}),
|
|
33
34
|
annotations: {
|
|
34
35
|
readOnlyHint: false,
|
|
35
36
|
destructiveHint: false,
|
|
36
37
|
idempotentHint: false,
|
|
37
38
|
},
|
|
38
|
-
}, async ({ issue_id, target_id, type_id }) => {
|
|
39
|
+
}, async ({ issue_id, target_id, type_id, type_name }) => {
|
|
40
|
+
// Resolve type_id from type_name when type_id is not provided
|
|
41
|
+
let resolvedTypeId = type_id;
|
|
42
|
+
if (resolvedTypeId === undefined) {
|
|
43
|
+
if (type_name === undefined) {
|
|
44
|
+
return { content: [{ type: 'text', text: errorText('Either type_id or type_name must be provided') }], isError: true };
|
|
45
|
+
}
|
|
46
|
+
const normalized = type_name.toLowerCase().replace(/-/g, '_');
|
|
47
|
+
resolvedTypeId = RELATIONSHIP_NAME_TO_ID[normalized];
|
|
48
|
+
if (resolvedTypeId === undefined) {
|
|
49
|
+
return {
|
|
50
|
+
content: [{ type: 'text', text: errorText(`Unknown relationship type name: "${type_name}". Valid values: duplicate_of, related_to, parent_of, child_of, has_duplicate`) }],
|
|
51
|
+
isError: true,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
39
55
|
try {
|
|
40
56
|
const body = {
|
|
41
57
|
issue: { id: target_id },
|
|
42
|
-
type: { id:
|
|
58
|
+
type: { id: resolvedTypeId },
|
|
43
59
|
};
|
|
44
60
|
const result = await client.post(`issues/${issue_id}/relationships`, body);
|
|
45
61
|
return {
|
package/package.json
CHANGED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": 51,
|
|
3
|
+
"name": "domAgent",
|
|
4
|
+
"real_name": "Dominik Pesch (via Agent)",
|
|
5
|
+
"email": "d.pesch+agent@11com7.de",
|
|
6
|
+
"language": "german",
|
|
7
|
+
"timezone": "Europe/Berlin",
|
|
8
|
+
"access_level": {
|
|
9
|
+
"id": 70,
|
|
10
|
+
"name": "manager",
|
|
11
|
+
"label": "Manager"
|
|
12
|
+
},
|
|
13
|
+
"created_at": "2026-02-28T09:08:30+01:00",
|
|
14
|
+
"projects": [
|
|
15
|
+
{
|
|
16
|
+
"id": 30,
|
|
17
|
+
"name": "((Haus)) (Bolliggasse 1a)"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": 54,
|
|
21
|
+
"name": "11com7 Claude MetaRepo"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": 2,
|
|
25
|
+
"name": "11com7 ContentGo-Lib"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": 48,
|
|
29
|
+
"name": "11com7 DevServer"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": 25,
|
|
33
|
+
"name": "11com7 GmbH"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": 39,
|
|
37
|
+
"name": "11com7 Hosting (Keyweb, Ansible)"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": 44,
|
|
41
|
+
"name": "11com7-Ausbildung-FIAE"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": 52,
|
|
45
|
+
"name": "11com7-DVMS (Datenschutzverletzungsmelder)"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": 5,
|
|
49
|
+
"name": "11com7-Homepage"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": 27,
|
|
53
|
+
"name": "b11com7"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": 3,
|
|
57
|
+
"name": "BIBB NAA309"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": 11,
|
|
61
|
+
"name": "BIBB wbmonitor"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": 38,
|
|
65
|
+
"name": "DSGV Budgetanalyse 3"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": 28,
|
|
69
|
+
"name": "DSGV Finanzchecker (App)"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": 45,
|
|
73
|
+
"name": "DSGV Finanzchecker (Landingpage)"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"id": 40,
|
|
77
|
+
"name": "DSGV Login"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": 26,
|
|
81
|
+
"name": "DSGV Referenzbudgets"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": 8,
|
|
85
|
+
"name": "DSGV Vortragsservice"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": 14,
|
|
89
|
+
"name": "DSGV Web-Budgetplaner"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"id": 42,
|
|
93
|
+
"name": "Lingua-World"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": 49,
|
|
97
|
+
"name": "SIK M&E-App"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"id": 21,
|
|
101
|
+
"name": "Uni Bonn (Hausprint V2)"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"id": 53,
|
|
105
|
+
"name": "ZEEEM GSM-Landingpage"
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
}
|