@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 ADDED
@@ -0,0 +1,3 @@
1
+ MANTIS_BASE_URL=https://mantis.dev.11com7.de
2
+ MANTIS_API_KEY=3vhKDwxVavmCzRUR7Cp0Lk7F8E4dha6n
3
+ MANTIS_SEARCH_ENABLED=true
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);
@@ -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) {
@@ -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.
@@ -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(results, null, 2) }],
78
+ content: [{ type: 'text', text: JSON.stringify(enriched, null, 2) }],
54
79
  };
55
80
  }
56
81
  catch (error) {
@@ -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
  }
@@ -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 (handler_id)
162
- body.handler = { id: handler_id };
163
- const result = await client.post('issues', body);
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(result.issue ?? result, null, 2) }],
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 type IDs:
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)
22
- - ${RELATIONSHIP_TYPES.CHILD_OF} = child_of — this issue blocks target (target can't proceed until this is done)
23
- - ${RELATIONSHIP_TYPES.HAS_DUPLICATE} = has_duplicate — this issue has target as a 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
- Important: The API only accepts numeric type IDs, not string names.`,
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(`Relationship type ID: 0=duplicate_of, 1=related_to, 2=parent_of (depends on), 3=child_of (blocks), 4=has_duplicate`),
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: 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dpesch/mantisbt-mcp-server",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "MCP server for MantisBT REST API – read and manage bug tracker issues",
5
5
  "author": "Dominik Pesch",
6
6
  "license": "MIT",
@@ -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
+ }