@dpesch/mantisbt-mcp-server 1.3.1 → 1.5.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/.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,27 @@ This project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.5.0] – 2026-03-17
11
+
12
+ ### Added
13
+ - `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).
14
+ - `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).
15
+ - `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.
16
+ - `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.
17
+ - `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}`.
18
+
19
+ ---
20
+
21
+ ## [1.4.0] – 2026-03-17
22
+
23
+ ### Added
24
+ - `get_issue_enums` now includes a `label` field in each enum entry when it differs from `name`. On localized MantisBT installations (e.g. German UI) this provides a translation table from the UI language back to the API name/id: `{"id": 10, "name": "new", "label": "Neu"}`. When `label` and `name` are identical the field is omitted to keep the output compact. The tool description also clarifies that `name` may itself be localized on installations where enum values have been customized at the database level.
25
+
26
+ ### Fixed
27
+ - `list_issues`: `assigned_to`, `reporter_id`, and `status` filters now reliably return matching issues regardless of the requested `page_size`. Previously, the tool fetched exactly `page_size` items from the API before filtering — so a small `page_size` combined with an active filter returned zero results if the matching issues were not at the top of the unfiltered list. The tool now internally fetches batches of 50 (API maximum) and scans up to 500 issues until enough matching results are found. All filter parameters are still forwarded to the API as a hint for installations that support server-side filtering.
28
+
29
+ ---
30
+
10
31
  ## [1.3.1] – 2026-03-16
11
32
 
12
33
  ### Fixed
package/README.de.md CHANGED
@@ -93,7 +93,7 @@ Falls keine Umgebungsvariablen gesetzt sind, wird `~/.claude/mantis.json` ausgel
93
93
  |---|---|
94
94
  | `get_issue` | Ein Issue anhand seiner ID abrufen |
95
95
  | `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 |
96
+ | `create_issue` | Neues Issue anlegen; optionaler `handler`-Parameter akzeptiert einen Benutzernamen als Alternative zu `handler_id` (wird gegen die Projektmitglieder aufgelöst) |
97
97
  | `update_issue` | Bestehendes Issue bearbeiten |
98
98
  | `delete_issue` | Issue löschen |
99
99
 
@@ -116,7 +116,7 @@ Falls keine Umgebungsvariablen gesetzt sind, wird `~/.claude/mantis.json` ausgel
116
116
 
117
117
  | Tool | Beschreibung |
118
118
  |---|---|
119
- | `add_relationship` | Beziehung zwischen zwei Issues erstellen |
119
+ | `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
120
  | `remove_relationship` | Beziehung von einem Issue entfernen (die `id` aus dem Beziehungsobjekt verwenden, nicht die type-ID) |
121
121
 
122
122
  ### Beobachter
@@ -157,7 +157,7 @@ Aktivierung mit `MANTIS_SEARCH_ENABLED=true`.
157
157
 
158
158
  | Tool | Beschreibung |
159
159
  |---|---|
160
- | `search_issues` | Natürlichsprachige Suche über alle indizierten Issues – liefert Top-N-Ergebnisse mit Cosine-Similarity-Score |
160
+ | `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
161
  | `rebuild_search_index` | Suchindex aufbauen oder aktualisieren; `full: true` löscht und baut ihn vollständig neu |
162
162
  | `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
163
 
@@ -188,7 +188,7 @@ npm install sqlite-vec better-sqlite3
188
188
  | `get_current_user` | Eigenes Benutzerprofil abrufen |
189
189
  | `list_languages` | Verfügbare Sprachen auflisten |
190
190
  | `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 |
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; auf lokalisierten Installationen kann jeder Eintrag ein `canonical_name`-Feld mit dem englischen Standard-API-Namen enthalten |
192
192
  | `get_mantis_version` | MantisBT-Version abrufen und auf Updates prüfen |
193
193
  | `get_mcp_version` | Version dieser mantisbt-mcp-server-Instanz zurückgeben |
194
194
 
package/README.md CHANGED
@@ -93,7 +93,7 @@ If no environment variables are set, `~/.claude/mantis.json` is read:
93
93
  |---|---|
94
94
  | `get_issue` | Retrieve an issue by its numeric ID |
95
95
  | `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 |
96
+ | `create_issue` | Create a new issue; optional `handler` parameter accepts a username as alternative to `handler_id` (resolved against project members) |
97
97
  | `update_issue` | Update an existing issue |
98
98
  | `delete_issue` | Delete an issue |
99
99
 
@@ -116,7 +116,7 @@ If no environment variables are set, `~/.claude/mantis.json` is read:
116
116
 
117
117
  | Tool | Description |
118
118
  |---|---|
119
- | `add_relationship` | Create a relationship between two issues |
119
+ | `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
120
  | `remove_relationship` | Remove a relationship from an issue (use the `id` from the relationship object, not the type) |
121
121
 
122
122
  ### Monitors
@@ -157,7 +157,7 @@ Activate with `MANTIS_SEARCH_ENABLED=true`.
157
157
 
158
158
  | Tool | Description |
159
159
  |---|---|
160
- | `search_issues` | Natural language search over all indexed issues — returns top-N results with cosine similarity score |
160
+ | `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
161
  | `rebuild_search_index` | Build or update the search index; `full: true` clears and rebuilds from scratch |
162
162
  | `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
163
 
@@ -188,7 +188,7 @@ npm install sqlite-vec better-sqlite3
188
188
  | `get_current_user` | Retrieve your own user profile |
189
189
  | `list_languages` | List available languages |
190
190
  | `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 |
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; on localized installations each entry may include a `canonical_name` with the standard English API name |
192
192
  | `get_mantis_version` | Get MantisBT version and check for updates |
193
193
  | `get_mcp_version` | Return the version of this mantisbt-mcp-server instance |
194
194
 
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);
@@ -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
@@ -66,12 +70,12 @@ Common option names:
66
70
  // ---------------------------------------------------------------------------
67
71
  server.registerTool('get_issue_enums', {
68
72
  title: 'Get Issue Enum Values',
69
- description: `Return valid ID and name pairs for all issue enum fields.
73
+ description: `Return valid ID, name, and (if available) localized label for all issue enum fields.
70
74
 
71
75
  Use this tool before creating or updating issues to look up the correct value
72
76
  for severity, status, priority, resolution, or reproducibility.
73
77
 
74
- Example response:
78
+ Example response (English installation):
75
79
  {
76
80
  "severity": [{"id": 10, "name": "feature"}, {"id": 50, "name": "minor"}, ...],
77
81
  "status": [{"id": 10, "name": "new"}, {"id": 20, "name": "feedback"}, ...],
@@ -80,7 +84,30 @@ Example response:
80
84
  "reproducibility": [{"id": 10, "name": "always"}, {"id": 70, "name": "have not tried"}, ...]
81
85
  }
82
86
 
83
- The "name" field is the value to pass to create_issue or update_issue.`,
87
+ Example response (localized installation, e.g. German):
88
+ {
89
+ "status": [
90
+ {"id": 10, "name": "new", "label": "Neu"},
91
+ {"id": 20, "name": "feedback", "label": "Feedback"},
92
+ {"id": 30, "name": "acknowledged", "label": "Bestätigt"},
93
+ ...
94
+ ],
95
+ ...
96
+ }
97
+
98
+ Fields:
99
+ - "id" — numeric ID accepted by the API
100
+ - "name" — the API value to pass to create_issue or update_issue; normally English, but may be
101
+ localized if the installation has customized enum values in the database
102
+ - "label" — localized display label shown in the UI (only present when it differs from "name")
103
+
104
+ Always pass either the "id" or the "name" value to create_issue or update_issue — never the "label".
105
+ Use the "label" to map user input in the UI language back to the correct "name"/"id" for the API.
106
+
107
+ Note: on some installations enum values are customized at the database level. In that case "name"
108
+ itself may be localized (e.g. "kleinerer Fehler" instead of "minor") and no "label" will be present
109
+ because there is no separate English original. The "name" value returned is always the correct one
110
+ to use for API calls — regardless of language.`,
84
111
  inputSchema: z.object({}),
85
112
  annotations: {
86
113
  readOnlyHint: true,
@@ -107,11 +134,26 @@ The "name" field is the value to pass to create_issue or update_issue.`,
107
134
  const key = keyMap[option];
108
135
  if (!key)
109
136
  continue;
137
+ const canonicalMap = MANTIS_CANONICAL_ENUM_NAMES[key] ?? {};
110
138
  if (typeof value === 'string') {
111
- 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
+ });
112
146
  }
113
147
  else if (Array.isArray(value)) {
114
- enums[key] = value.map(({ id, name }) => ({ id, name }));
148
+ enums[key] = value.map(({ id, name, label }) => {
149
+ const entry = { id, name };
150
+ if (label && label !== name)
151
+ entry.label = label;
152
+ const canonical_name = resolveCanonicalName(id, name, canonicalMap);
153
+ if (canonical_name !== undefined)
154
+ entry.canonical_name = canonical_name;
155
+ return entry;
156
+ });
115
157
  }
116
158
  }
117
159
  return {
@@ -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
  // ---------------------------------------------------------------------------
@@ -40,7 +40,7 @@ export function registerIssueTools(server, client) {
40
40
  // ---------------------------------------------------------------------------
41
41
  server.registerTool('list_issues', {
42
42
  title: 'List Issues',
43
- description: 'List MantisBT issues with optional filtering. Returns a paginated list of issues. Use the "select" parameter to limit returned fields and reduce response size significantly.',
43
+ description: 'List MantisBT issues with optional filtering. Returns a paginated list of issues. Use the "select" parameter to limit returned fields and reduce response size significantly.\n\nNote: "assigned_to", "reporter_id", and "status" filters are applied client-side (the MantisBT REST API does not reliably support these as server-side filters). When any of these filters are active the tool automatically fetches multiple pages internally until enough matching results are found (up to 500 issues scanned). The "page" and "page_size" parameters refer to the resulting filtered list.',
44
44
  inputSchema: z.object({
45
45
  project_id: z.coerce.number().int().positive().optional().describe('Filter by project ID'),
46
46
  page: z.coerce.number().int().positive().default(1).describe('Page number (default: 1)'),
@@ -60,9 +60,7 @@ export function registerIssueTools(server, client) {
60
60
  },
61
61
  }, async ({ project_id, page, page_size, assigned_to, reporter_id, filter_id, sort, direction, select, status }) => {
62
62
  try {
63
- const params = {
64
- page,
65
- page_size,
63
+ const baseParams = {
66
64
  project_id,
67
65
  assigned_to,
68
66
  reporter_id,
@@ -71,19 +69,57 @@ export function registerIssueTools(server, client) {
71
69
  direction,
72
70
  select,
73
71
  };
74
- const result = await client.get('issues', params);
75
- if (status && result.issues) {
76
- const statusLower = status.toLowerCase();
77
- result.issues = result.issues.filter(issue => {
78
- if (!issue.status)
79
- return false;
80
- if (statusLower === 'open')
81
- return (issue.status.id ?? 0) < MANTIS_RESOLVED_STATUS_ID;
82
- return issue.status.name?.toLowerCase() === statusLower;
72
+ const needsClientFilter = status !== undefined || assigned_to !== undefined || reporter_id !== undefined;
73
+ if (!needsClientFilter) {
74
+ // No client-side filtering — single API call, pass pagination as-is
75
+ const result = await client.get('issues', { ...baseParams, page, page_size });
76
+ return {
77
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
78
+ };
79
+ }
80
+ // Client-side filtering active: scan multiple API pages until we have
81
+ // enough matching results for the requested logical page.
82
+ const API_PAGE_SIZE = 50; // always fetch max to minimise round-trips
83
+ const MAX_API_PAGES = 10; // hard cap: scan at most 500 issues
84
+ const neededTotal = page * page_size; // need this many matches to serve page N
85
+ const matching = [];
86
+ let serverPage = 1;
87
+ let hasMore = true;
88
+ const statusLower = status?.toLowerCase();
89
+ while (matching.length < neededTotal && serverPage <= MAX_API_PAGES && hasMore) {
90
+ const batch = await client.get('issues', {
91
+ ...baseParams,
92
+ page: serverPage,
93
+ page_size: API_PAGE_SIZE,
83
94
  });
95
+ const issues = batch.issues ?? [];
96
+ hasMore = issues.length === API_PAGE_SIZE;
97
+ for (const issue of issues) {
98
+ if (statusLower) {
99
+ if (!issue.status)
100
+ continue;
101
+ if (statusLower === 'open') {
102
+ if ((issue.status.id ?? 0) >= MANTIS_RESOLVED_STATUS_ID)
103
+ continue;
104
+ }
105
+ else if (issue.status.name?.toLowerCase() !== statusLower) {
106
+ continue;
107
+ }
108
+ }
109
+ if (assigned_to !== undefined && issue.handler?.id !== assigned_to)
110
+ continue;
111
+ if (reporter_id !== undefined && issue.reporter?.id !== reporter_id)
112
+ continue;
113
+ matching.push(issue);
114
+ }
115
+ serverPage++;
84
116
  }
117
+ const start = (page - 1) * page_size;
85
118
  return {
86
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
119
+ content: [{
120
+ type: 'text',
121
+ text: JSON.stringify({ issues: matching.slice(start, start + page_size) }, null, 2),
122
+ }],
87
123
  };
88
124
  }
89
125
  catch (error) {
@@ -105,13 +141,38 @@ export function registerIssueTools(server, client) {
105
141
  priority: z.string().optional().describe('Priority name (e.g. "normal", "high", "urgent", "immediate", "low", "none")'),
106
142
  severity: z.string().default('minor').describe('Severity name (e.g. "minor", "major", "crash", "block", "feature", "trivial", "text") — default: "minor"'),
107
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.'),
108
145
  }),
109
146
  annotations: {
110
147
  readOnlyHint: false,
111
148
  destructiveHint: false,
112
149
  idempotentHint: false,
113
150
  },
114
- }, 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
+ }
115
176
  try {
116
177
  const body = {
117
178
  summary,
@@ -122,11 +183,26 @@ export function registerIssueTools(server, client) {
122
183
  if (priority)
123
184
  body.priority = { name: priority };
124
185
  body.severity = { name: severity };
125
- if (handler_id)
126
- body.handler = { id: handler_id };
127
- 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
+ }
128
204
  return {
129
- content: [{ type: 'text', text: JSON.stringify(result.issue ?? result, null, 2) }],
205
+ content: [{ type: 'text', text: JSON.stringify(issue, null, 2) }],
130
206
  };
131
207
  }
132
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.3.1",
3
+ "version": "1.5.0",
4
4
  "description": "MCP server for MantisBT REST API – read and manage bug tracker issues",
5
5
  "author": "Dominik Pesch",
6
6
  "license": "MIT",