@dpesch/mantisbt-mcp-server 1.8.3 → 1.9.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.
@@ -1,20 +1,48 @@
1
1
  import { z } from 'zod';
2
2
  import { getVersionHint } from '../version-hint.js';
3
3
  import { MANTIS_CANONICAL_ENUM_NAMES, MANTIS_RESOLVED_STATUS_ID, resolveEnumId } from '../constants.js';
4
+ import { dateFilterSchema, matchesDateFilter, hasDateFilter } from '../date-filter.js';
5
+ import { fetchIssueEnumsWithCache } from './config.js';
4
6
  function errorText(msg) {
5
7
  const vh = getVersionHint();
6
8
  vh?.triggerLatestVersionFetch();
7
9
  const hint = vh?.getUpdateHint();
8
10
  return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
9
11
  }
10
- // Resolves a canonical enum name to { id } or returns an error string.
11
- function resolveEnum(group, value) {
12
+ const GET_ISSUES_CONCURRENCY = 5;
13
+ // Worker-pool: runs `fn` over all `items` with at most `concurrency` in-flight at once.
14
+ // nextIndex is only incremented inside microtasks, so the ++ is safe without a lock.
15
+ async function runWithConcurrency(items, concurrency, fn) {
16
+ const results = new Array(items.length);
17
+ let nextIndex = 0;
18
+ async function worker() {
19
+ while (nextIndex < items.length) {
20
+ const i = nextIndex++;
21
+ results[i] = await fn(items[i]);
22
+ }
23
+ }
24
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
25
+ return results;
26
+ }
27
+ // Resolves an enum name (canonical or localized) to { id } or returns an error string.
28
+ async function resolveEnum(group, value, client) {
12
29
  const id = resolveEnumId(group, value);
13
- if (id === undefined) {
14
- const valid = Object.values(MANTIS_CANONICAL_ENUM_NAMES[group]).join(', ');
15
- return `Invalid ${group} "${value}". Valid canonical names: ${valid}. Call get_issue_enums to see localized labels.`;
30
+ if (id !== undefined)
31
+ return { id };
32
+ try {
33
+ const enums = await fetchIssueEnumsWithCache(client);
34
+ const entries = enums[group] ?? [];
35
+ const lower = value.toLowerCase();
36
+ const entry = entries.find(e => e.name.toLowerCase() === lower ||
37
+ (e.label !== undefined && e.label.toLowerCase() === lower));
38
+ if (entry !== undefined)
39
+ return { id: entry.id };
40
+ }
41
+ catch {
42
+ // localized lookup unavailable — fall through to static error
16
43
  }
17
- return { id };
44
+ const valid = Object.values(MANTIS_CANONICAL_ENUM_NAMES[group]).join(', ');
45
+ return `Invalid ${group} "${value}". Valid canonical names: ${valid}. Call get_issue_enums to see localized labels.`;
18
46
  }
19
47
  export function registerIssueTools(server, client, cache) {
20
48
  // ---------------------------------------------------------------------------
@@ -22,7 +50,7 @@ export function registerIssueTools(server, client, cache) {
22
50
  // ---------------------------------------------------------------------------
23
51
  server.registerTool('get_issue', {
24
52
  title: 'Get Issue',
25
- description: 'Retrieve a single MantisBT issue by its numeric ID. Returns all issue fields including notes, attachments, and relationships.',
53
+ description: 'Retrieve a single MantisBT issue by its numeric ID. Returns all issue fields including notes, attachments, and relationships. Notes are always included — no separate list_notes call needed.',
26
54
  inputSchema: z.object({
27
55
  id: z.coerce.number().int().positive().describe('Numeric issue ID'),
28
56
  }),
@@ -45,11 +73,51 @@ export function registerIssueTools(server, client, cache) {
45
73
  }
46
74
  });
47
75
  // ---------------------------------------------------------------------------
76
+ // get_issues
77
+ // ---------------------------------------------------------------------------
78
+ server.registerTool('get_issues', {
79
+ title: 'Get Multiple Issues',
80
+ description: 'Retrieve multiple MantisBT issues by their numeric IDs in a single MCP call. ' +
81
+ 'Requests run in parallel (max 5 concurrent). ' +
82
+ 'Missing or inaccessible IDs return null at their array position — ' +
83
+ 'the call never fails due to individual missing IDs. ' +
84
+ 'Response includes "requested", "found", and "failed" counters for quick validation.',
85
+ inputSchema: z.object({
86
+ ids: z
87
+ .array(z.coerce.number().int().positive())
88
+ .min(1)
89
+ .max(50)
90
+ .describe('Array of numeric issue IDs to fetch (1–50). null is returned per ID on 404/403/error instead of failing the whole call.'),
91
+ }),
92
+ annotations: {
93
+ readOnlyHint: true,
94
+ destructiveHint: false,
95
+ idempotentHint: true,
96
+ },
97
+ }, async ({ ids }) => {
98
+ const results = await runWithConcurrency(ids, GET_ISSUES_CONCURRENCY, async (id) => {
99
+ try {
100
+ const result = await client.get(`issues/${id}`);
101
+ return result.issues?.[0] ?? result;
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ });
107
+ const found = results.filter((r) => r !== null).length;
108
+ return {
109
+ content: [{
110
+ type: 'text',
111
+ text: JSON.stringify({ issues: results, requested: ids.length, found, failed: ids.length - found }, null, 2),
112
+ }],
113
+ };
114
+ });
115
+ // ---------------------------------------------------------------------------
48
116
  // list_issues
49
117
  // ---------------------------------------------------------------------------
50
118
  server.registerTool('list_issues', {
51
119
  title: 'List Issues',
52
- 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.',
120
+ 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", "status", and date filters are applied client-side (the MantisBT REST API does not 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.\n\nTip for date queries: fetching with select="id,updated_at,created_at" plus a date filter is very compact and efficient.',
53
121
  inputSchema: z.object({
54
122
  project_id: z.coerce.number().int().positive().optional().describe('Filter by project ID'),
55
123
  page: z.coerce.number().int().positive().default(1).describe('Page number (default: 1)'),
@@ -61,13 +129,14 @@ export function registerIssueTools(server, client, cache) {
61
129
  direction: z.enum(['ASC', 'DESC']).optional().describe('Sort direction'),
62
130
  select: z.string().optional().describe('Comma-separated list of fields to include in the response (server-side projection). Significantly reduces response size. Example: "id,summary,status,priority,handler,updated_at"'),
63
131
  status: z.string().optional().describe('Filter issues by status name (e.g. "new", "feedback", "acknowledged", "confirmed", "assigned", "resolved", "closed") or use "open" as shorthand for all statuses with id < 80 (i.e. not yet resolved or closed). Applied client-side after fetching — when combined with pagination, a page may contain fewer results than page_size.'),
132
+ ...dateFilterSchema,
64
133
  }),
65
134
  annotations: {
66
135
  readOnlyHint: true,
67
136
  destructiveHint: false,
68
137
  idempotentHint: true,
69
138
  },
70
- }, async ({ project_id, page, page_size, assigned_to, reporter_id, filter_id, sort, direction, select, status }) => {
139
+ }, async ({ project_id, page, page_size, assigned_to, reporter_id, filter_id, sort, direction, select, status, updated_after, updated_before, created_after, created_before }) => {
71
140
  try {
72
141
  const baseParams = {
73
142
  project_id,
@@ -78,7 +147,8 @@ export function registerIssueTools(server, client, cache) {
78
147
  direction,
79
148
  select,
80
149
  };
81
- const needsClientFilter = status !== undefined || assigned_to !== undefined || reporter_id !== undefined;
150
+ const dateFilter = { updated_after, updated_before, created_after, created_before };
151
+ const needsClientFilter = status !== undefined || assigned_to !== undefined || reporter_id !== undefined || hasDateFilter(dateFilter);
82
152
  if (!needsClientFilter) {
83
153
  // No client-side filtering — single API call, pass pagination as-is
84
154
  const result = await client.get('issues', { ...baseParams, page, page_size });
@@ -95,6 +165,9 @@ export function registerIssueTools(server, client, cache) {
95
165
  let serverPage = 1;
96
166
  let hasMore = true;
97
167
  const statusLower = status?.toLowerCase();
168
+ const statusId = status ? resolveEnumId('status', status) : undefined;
169
+ // Pre-parse date thresholds once — avoids repeated new Date() inside the scan loop
170
+ const updatedAfterMs = updated_after ? new Date(updated_after).getTime() : undefined;
98
171
  while (matching.length < neededTotal && serverPage <= MAX_API_PAGES && hasMore) {
99
172
  const batch = await client.get('issues', {
100
173
  ...baseParams,
@@ -103,6 +176,7 @@ export function registerIssueTools(server, client, cache) {
103
176
  });
104
177
  const issues = batch.issues ?? [];
105
178
  hasMore = issues.length === API_PAGE_SIZE;
179
+ let stopAfterBatch = false;
106
180
  for (const issue of issues) {
107
181
  if (statusLower) {
108
182
  if (!issue.status)
@@ -111,6 +185,10 @@ export function registerIssueTools(server, client, cache) {
111
185
  if ((issue.status.id ?? 0) >= MANTIS_RESOLVED_STATUS_ID)
112
186
  continue;
113
187
  }
188
+ else if (statusId !== undefined) {
189
+ if (issue.status.id !== statusId)
190
+ continue;
191
+ }
114
192
  else if (issue.status.name?.toLowerCase() !== statusLower) {
115
193
  continue;
116
194
  }
@@ -119,8 +197,20 @@ export function registerIssueTools(server, client, cache) {
119
197
  continue;
120
198
  if (reporter_id !== undefined && issue.reporter?.id !== reporter_id)
121
199
  continue;
200
+ if (!matchesDateFilter(issue, dateFilter)) {
201
+ // MantisBT returns results newest-first. Once updated_at drops below
202
+ // updated_after, all subsequent pages are guaranteed to be older too.
203
+ // Finish the current batch first (items within it may still be newer),
204
+ // then stop fetching further pages.
205
+ if (updatedAfterMs && issue.updated_at && new Date(issue.updated_at).getTime() <= updatedAfterMs) {
206
+ stopAfterBatch = true;
207
+ }
208
+ continue;
209
+ }
122
210
  matching.push(issue);
123
211
  }
212
+ if (stopAfterBatch)
213
+ break;
124
214
  serverPage++;
125
215
  }
126
216
  const start = (page - 1) * page_size;
@@ -147,8 +237,8 @@ export function registerIssueTools(server, client, cache) {
147
237
  description: z.string().min(1).describe('Detailed issue description. Required — do not create issues without a description. Plain text or Markdown.'),
148
238
  project_id: z.coerce.number().int().positive().describe('Project ID the issue belongs to'),
149
239
  category: z.string().min(1).describe('Category name (use get_project_categories to list available categories)'),
150
- priority: z.string().default('normal').describe('Priority name — must be a canonical English name: none, low, normal, high, urgent, immediate. Default: "normal". Call get_issue_enums to see localized labels.'),
151
- severity: z.string().default('minor').describe('Severity name — must be a canonical English name: feature, trivial, text, tweak, minor, major, crash, block. Default: "minor". Call get_issue_enums to see localized labels.'),
240
+ priority: z.string().default('normal').describe('Priority: canonical English name (none, low, normal, high, urgent, immediate) or localized label. Default: "normal". Use get_issue_enums to see all available values.'),
241
+ severity: z.string().default('minor').describe('Severity: canonical English name (feature, trivial, text, tweak, minor, major, crash, block) or localized label. Default: "minor". Use get_issue_enums to see all available values.'),
152
242
  handler_id: z.coerce.number().int().positive().optional().describe('User ID of the person to assign the issue to'),
153
243
  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.'),
154
244
  version: z.string().optional().describe('Affected product version name (use get_project_versions to list available versions)'),
@@ -156,7 +246,7 @@ export function registerIssueTools(server, client, cache) {
156
246
  fixed_in_version: z.string().optional().describe('Version name in which the issue was fixed (use get_project_versions to list available versions)'),
157
247
  steps_to_reproduce: z.string().optional().describe('Steps to reproduce the issue. Plain text or Markdown.'),
158
248
  additional_information: z.string().optional().describe('Additional information about the issue. Plain text or Markdown.'),
159
- reproducibility: z.string().optional().describe('Reproducibility must be a canonical English name: always, sometimes, random, have not tried, unable to reproduce, N/A. Call get_issue_enums to see localized labels.'),
249
+ reproducibility: z.string().optional().describe('Reproducibility: canonical English name or localized label (always, sometimes, random, have not tried, unable to reproduce, N/A). Use get_issue_enums to see all available values.'),
160
250
  view_state: z.enum(['public', 'private']).optional().describe('Visibility of the issue: "public" (default) or "private"'),
161
251
  }),
162
252
  annotations: {
@@ -196,11 +286,11 @@ export function registerIssueTools(server, client, cache) {
196
286
  project: { id: project_id },
197
287
  category: { name: category },
198
288
  };
199
- const priorityResolved = resolveEnum('priority', priority);
289
+ const priorityResolved = await resolveEnum('priority', priority, client);
200
290
  if (typeof priorityResolved === 'string')
201
291
  return { content: [{ type: 'text', text: errorText(priorityResolved) }], isError: true };
202
292
  body.priority = priorityResolved;
203
- const severityResolved = resolveEnum('severity', severity);
293
+ const severityResolved = await resolveEnum('severity', severity, client);
204
294
  if (typeof severityResolved === 'string')
205
295
  return { content: [{ type: 'text', text: errorText(severityResolved) }], isError: true };
206
296
  body.severity = severityResolved;
@@ -217,7 +307,7 @@ export function registerIssueTools(server, client, cache) {
217
307
  if (additional_information !== undefined)
218
308
  body.additional_information = additional_information;
219
309
  if (reproducibility !== undefined) {
220
- const reproducibilityResolved = resolveEnum('reproducibility', reproducibility);
310
+ const reproducibilityResolved = await resolveEnum('reproducibility', reproducibility, client);
221
311
  if (typeof reproducibilityResolved === 'string')
222
312
  return { content: [{ type: 'text', text: errorText(reproducibilityResolved) }], isError: true };
223
313
  body.reproducibility = reproducibilityResolved;
@@ -252,6 +342,7 @@ export function registerIssueTools(server, client, cache) {
252
342
  // ---------------------------------------------------------------------------
253
343
  // update_issue
254
344
  // ---------------------------------------------------------------------------
345
+ const coerceBool = (val) => val === 'true' ? true : val === 'false' ? false : val;
255
346
  // MantisBT reference shape: at least one of id or name must be provided
256
347
  const ref = z.object({ id: z.number().optional(), name: z.string().optional() })
257
348
  .refine(o => o.id !== undefined || o.name !== undefined, { message: "At least one of 'id' or 'name' must be provided" });
@@ -279,7 +370,17 @@ The "fields" object accepts any combination of:
279
370
  Important: when resolving an issue, always set BOTH status and resolution to avoid leaving resolution as "open".`,
280
371
  inputSchema: z.object({
281
372
  id: z.coerce.number().int().positive().describe('Numeric issue ID to update'),
282
- fields: z.object({
373
+ dry_run: z.preprocess(coerceBool, z.boolean().optional()).describe('If true, return the patch payload that would be sent without actually updating the issue. Useful for previewing changes before committing them.'),
374
+ fields: z.preprocess((v) => {
375
+ if (typeof v !== 'string')
376
+ return v;
377
+ try {
378
+ return JSON.parse(v);
379
+ }
380
+ catch {
381
+ return v;
382
+ }
383
+ }, z.object({
283
384
  summary: z.string().optional(),
284
385
  description: z.string().optional(),
285
386
  steps_to_reproduce: z.string().optional(),
@@ -295,16 +396,30 @@ Important: when resolving an issue, always set BOTH status and resolution to avo
295
396
  target_version: ref.optional(),
296
397
  fixed_in_version: ref.optional(),
297
398
  view_state: ref.optional(),
298
- }).strict().describe('Fields to update (partial update — only provided fields are changed; unknown keys are rejected)'),
399
+ }).strict().describe('Fields to update (partial update — only provided fields are changed; unknown keys are rejected)')),
299
400
  }),
300
401
  annotations: {
301
402
  readOnlyHint: false,
302
403
  destructiveHint: false,
303
404
  idempotentHint: false,
304
405
  },
305
- }, async ({ id, fields }) => {
406
+ }, async ({ id, fields, dry_run }) => {
407
+ if (dry_run) {
408
+ return {
409
+ content: [{ type: 'text', text: JSON.stringify({ dry_run: true, id, would_patch: fields }, null, 2) }],
410
+ };
411
+ }
306
412
  try {
307
- const result = await client.patch(`issues/${id}`, fields);
413
+ const patch = { ...fields };
414
+ for (const field of ['status', 'priority', 'severity', 'resolution', 'reproducibility']) {
415
+ const val = fields[field];
416
+ if (val?.name !== undefined && val.id === undefined) {
417
+ const resolved = await resolveEnum(field, val.name, client);
418
+ if (typeof resolved !== 'string')
419
+ patch[field] = resolved;
420
+ }
421
+ }
422
+ const result = await client.patch(`issues/${id}`, patch);
308
423
  return {
309
424
  content: [{ type: 'text', text: JSON.stringify(result.issue ?? result, null, 2) }],
310
425
  };
@@ -12,7 +12,7 @@ export function registerNoteTools(server, client) {
12
12
  // ---------------------------------------------------------------------------
13
13
  server.registerTool('list_notes', {
14
14
  title: 'List Issue Notes',
15
- description: 'List all notes (comments) attached to a MantisBT issue.',
15
+ description: 'List all notes (comments) attached to a MantisBT issue. Note: get_issue already includes notes in its response — use list_notes only when you need notes without fetching the full issue.',
16
16
  inputSchema: z.object({
17
17
  issue_id: z.coerce.number().int().positive().describe('Numeric issue ID'),
18
18
  }),
@@ -8,6 +8,7 @@ function errorText(msg) {
8
8
  const hint = vh?.getUpdateHint();
9
9
  return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
10
10
  }
11
+ const coerceBool = (val) => val === 'true' ? true : val === 'false' ? false : val;
11
12
  export function registerProjectTools(server, client, cache) {
12
13
  // ---------------------------------------------------------------------------
13
14
  // list_projects
@@ -72,8 +73,8 @@ export function registerProjectTools(server, client, cache) {
72
73
  description: 'List all versions defined for a MantisBT project.',
73
74
  inputSchema: z.object({
74
75
  project_id: z.coerce.number().int().positive().describe('Numeric project ID'),
75
- obsolete: z.boolean().default(false).describe('Include obsolete (deprecated) versions (default: false)'),
76
- inherit: z.boolean().default(false).describe('Include versions inherited from parent projects (default: false)'),
76
+ obsolete: z.preprocess(coerceBool, z.boolean()).default(false).describe('Include obsolete (deprecated) versions (default: false)'),
77
+ inherit: z.preprocess(coerceBool, z.boolean()).default(false).describe('Include versions inherited from parent projects (default: false)'),
77
78
  }),
78
79
  annotations: {
79
80
  readOnlyHint: true,
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { parseVersion, compareVersions } from '../version-hint.js';
3
+ const coerceBool = (val) => val === 'true' ? true : val === 'false' ? false : val;
3
4
  export function registerVersionTools(server, client, versionHint, mcpVersion) {
4
5
  // ---------------------------------------------------------------------------
5
6
  // get_mcp_version
@@ -26,7 +27,7 @@ export function registerVersionTools(server, client, versionHint, mcpVersion) {
26
27
  The version is read from the X-Mantis-Version response header sent by every API call.
27
28
  The GitHub comparison requires an outbound HTTPS request to the GitHub API.`,
28
29
  inputSchema: z.object({
29
- check_latest: z.boolean().default(true).describe('Whether to fetch the latest release from GitHub and compare (default: true)'),
30
+ check_latest: z.preprocess(coerceBool, z.boolean()).default(true).describe('Whether to fetch the latest release from GitHub and compare (default: true)'),
30
31
  }),
31
32
  annotations: {
32
33
  readOnlyHint: true,
@@ -145,7 +145,7 @@ Gibt die auf der eigenen MantisBT-Instanz konfigurierten Enum-Werte zurück. Vor
145
145
  }
146
146
  ```
147
147
 
148
- > **Hinweis:** Die von `get_issue_enums()` zurückgegebenen Namen können lokalisiert sein. `create_issue` und `update_issue` erwarten für `severity` und `priority` **kanonische englische Namen** (z.B. `minor`, `major`, `normal`) bei Unsicherheit das `canonical_name`-Feld in der Antwort prüfen.
148
+ > **Hinweis:** Auf lokalisierten Instanzen liefert `get_issue_enums()` im Feld `name` den lokalisierten Begriff und optional im Feld `canonical_name` den englischen Originalnamen. `create_issue` akzeptiert **beides** sowohl den kanonischen englischen Namen (z.B. `minor`) als auch den lokalisierten Namen (z.B. `Unschönheit`). Der Server löst den Wert automatisch auf.
149
149
 
150
150
  ---
151
151
 
@@ -347,6 +347,8 @@ Gibt nur Issues mit einem bestimmten Status zurück. Der Filter wird clientseiti
347
347
  }
348
348
  ```
349
349
 
350
+ > **Hinweis:** Kanonische Statusnamen (z.B. `"new"`, `"resolved"`) werden zur numerischen ID aufgelöst und per `issue.status.id` gefiltert — funktioniert auch auf lokalisierten Installationen, bei denen die API übersetzte Statusnamen zurückgibt. Direkt übergebene lokalisierte Namen (z.B. `"Neu"`) werden als Fallback über den Namen abgeglichen. Das Kürzel `"open"` (alle Status mit id < 80) steht unabhängig von der Installationssprache immer zur Verfügung.
351
+
350
352
  > **Hinweis:** Bei großen Projekten mit vielen Issues stattdessen einen vorgespeicherten MantisBT-Filter über `filter_id` verwenden — die clientseitige Filterung durchsucht nur die ersten 500 Issues (10 Seiten × 50).
351
353
 
352
354
  ---
@@ -456,8 +458,8 @@ Legt ein neues Issue in MantisBT an.
456
458
  - `project_id` — numerische Projekt-ID
457
459
  - `category` — Kategoriename als Zeichenkette
458
460
  - `description` — _(optional)_ ausführliche Beschreibung
459
- - `priority` — _(optional)_ kanonischer englischer Prioritätsname: `none`, `low`, `normal`, `high`, `urgent`, `immediate` lokalisierte Bezeichnungen über `get_issue_enums()` ermitteln
460
- - `severity` — _(optional)_ kanonischer englischer Schweregrad-Name: `feature`, `trivial`, `text`, `tweak`, `minor`, `major`, `crash`, `block`; Standard ist `"minor"` lokalisierte Bezeichnungen über `get_issue_enums()` ermitteln
461
+ - `priority` — _(optional)_ Priorität: kanonischer englischer Name (`none`, `low`, `normal`, `high`, `urgent`, `immediate`) oder lokalisierter Begriff. Standard: `"normal"`. Alle verfügbaren Werte über `get_issue_enums()` ermitteln.
462
+ - `severity` — _(optional)_ Schweregrad: kanonischer englischer Name (`feature`, `trivial`, `text`, `tweak`, `minor`, `major`, `crash`, `block`) oder lokalisierter Begriff. Standard: `"minor"`. Alle verfügbaren Werte über `get_issue_enums()` ermitteln.
461
463
  - `handler` — _(optional)_ Benutzername des Bearbeiters (wird automatisch in eine ID aufgelöst)
462
464
  - `handler_id` — _(optional)_ numerische Benutzer-ID des Bearbeiters (Alternative zu `handler`)
463
465
 
@@ -500,11 +502,11 @@ Legt ein neues Issue in MantisBT an.
500
502
 
501
503
  **Fehler: unbekannter Schweregrad oder unbekannte Priorität**
502
504
 
503
- Wird ein nicht erkannter Name für `severity` oder `priority` übergeben, gibt der Server einen Fehler zurück, der die gültigen Werte auflistet:
505
+ Der Server prüft zuerst kanonische englische Namen und fällt dann auf einen Live-`get_issue_enums`-Lookup zurück. Ein Fehler wird nur zurückgegeben, wenn der Wert weder kanonisch noch lokalisiert erkannt wird:
504
506
 
505
- > Error: Invalid severity "schwerer Fehler". Valid canonical names: feature, trivial, text, tweak, minor, major, crash, block. Call get_issue_enums to see localized labels.
507
+ > Error: Invalid severity "xyz". Valid canonical names: feature, trivial, text, tweak, minor, major, crash, block. Call get_issue_enums to see localized labels.
506
508
 
507
- Mit `get_issue_enums` lassen sich die kanonischen Namen ermitteln, die `create_issue` akzeptiert.
509
+ Mit `get_issue_enums` lassen sich alle akzeptierten Werte ermitteln sowohl kanonische als auch lokalisierte Namen funktionieren.
508
510
 
509
511
  ---
510
512
 
@@ -519,6 +521,8 @@ Löst ein Issue auf und schließt es. **Immer beide Felder** `status` und `resol
519
521
  - `fields.status` — Status-Objekt mit Name
520
522
  - `fields.resolution` — Auflösungs-Objekt mit ID
521
523
 
524
+ > **Hinweis:** Alle Enum-Felder (`status`, `priority`, `severity`, `resolution`, `reproducibility`) akzeptieren kanonische englische Namen, lokalisierte Namen oder numerische IDs. Der Server löst Namen automatisch zu IDs auf — dadurch ist die Übergabe sprachunabhängig.
525
+
522
526
  **Request:**
523
527
 
524
528
  ```json
@@ -1246,6 +1250,7 @@ Findet Issues, die einem natürlichsprachigen Suchbegriff semantisch ähnlich si
1246
1250
  **Parameter:**
1247
1251
  - `query` — Suchanfrage in natürlicher Sprache
1248
1252
  - `top_n` — _(optional)_ Anzahl zurückzugebender Ergebnisse; Standard 10
1253
+ - `highlight` — _(optional)_ bei `true` werden keyword-basierte Ausschnitte je Ergebnis ergänzt; Standard `false`
1249
1254
 
1250
1255
  **Request:**
1251
1256
 
@@ -1280,6 +1285,7 @@ Reichert Suchergebnisse mit bestimmten Feldern aus MantisBT an. Ohne `select` we
1280
1285
  - `query` — Suchanfrage in natürlicher Sprache
1281
1286
  - `top_n` — _(optional)_ Anzahl der Ergebnisse
1282
1287
  - `select` — kommagetrennte Feldnamen, die für jedes Ergebnis abgerufen werden
1288
+ - `highlight` — _(optional)_ bei `true` werden keyword-basierte Ausschnitte je Ergebnis ergänzt; Standard `false`
1283
1289
 
1284
1290
  **Request:**
1285
1291
 
@@ -1310,6 +1316,57 @@ Reichert Suchergebnisse mit bestimmten Feldern aus MantisBT an. Ohne `select` we
1310
1316
 
1311
1317
  ---
1312
1318
 
1319
+ ### Suche mit Keyword-Highlights
1320
+
1321
+ Zeigt, welcher Teil eines Issues mit der Suchanfrage übereinstimmt. Jedes Ergebnis, das lexikalisch mit der Anfrage übereinstimmt, erhält ein `highlights`-Feld mit fett hervorgehobenen Ausschnitten. Highlights sind keyword-basiert (lexikalisch), nicht semantisch — Ergebnisse ohne lexikalische Übereinstimmung haben kein `highlights`-Feld.
1322
+
1323
+ **Tool:** `search_issues`
1324
+
1325
+ **Parameter:**
1326
+ - `query` — Suchanfrage in natürlicher Sprache
1327
+ - `top_n` — _(optional)_ Anzahl der Ergebnisse; Standard 10
1328
+ - `highlight` — auf `true` setzen, um Highlights zu aktivieren
1329
+
1330
+ **Request:**
1331
+
1332
+ ```json
1333
+ {
1334
+ "query": "Login-Fehler nach Passwort-Reset",
1335
+ "top_n": 5,
1336
+ "highlight": true
1337
+ }
1338
+ ```
1339
+
1340
+ **Response:**
1341
+
1342
+ ```json
1343
+ [
1344
+ {
1345
+ "id": 1042,
1346
+ "score": 0.91,
1347
+ "highlights": {
1348
+ "summary": "**Login**-Button reagiert nach **Passwort**-**Reset** auf Mobile Safari nicht",
1349
+ "description": "…Benutzer tippt auf **Login** und es passiert nichts. Reproduzierbar nach einem **Passwort**-**Reset**-Vorgang…"
1350
+ }
1351
+ },
1352
+ {
1353
+ "id": 987,
1354
+ "score": 0.84,
1355
+ "highlights": {
1356
+ "summary": "**Login** schlägt mit 401 fehl — Token ungültig"
1357
+ }
1358
+ },
1359
+ {
1360
+ "id": 1015,
1361
+ "score": 0.79
1362
+ }
1363
+ ]
1364
+ ```
1365
+
1366
+ > **Hinweis:** Nur Ergebnisse mit mindestens einer Keyword-Übereinstimmung in `summary` oder `description` erhalten ein `highlights`-Feld. Der `description`-Ausschnitt umfasst ca. 300 Zeichen, zentriert um den ersten Treffer. Wenn zusätzlich `select` gesetzt ist und `summary` oder `description` enthält, werden die Highlights aus den abgerufenen Feldern generiert; andernfalls stammen sie aus den indizierten Metadaten.
1367
+
1368
+ ---
1369
+
1313
1370
  ## Projekte & Kategorien
1314
1371
 
1315
1372
  ### Projektkategorien auflisten
@@ -1635,7 +1692,7 @@ Gibt eine kombinierte Ansicht eines einzelnen Projekts zurück: Projektfelder so
1635
1692
 
1636
1693
  ### Issue-Enum-Werte lesen
1637
1694
 
1638
- Gibt gültige ID/Name-Paare für alle Issue-Enum-Felder zurück (Severity, Priority, Status, Resolution, Reproducibility). Vor `create_issue` oder `update_issue` verwenden, um kanonische englische Namen nachzuschlagen.
1695
+ Gibt gültige ID/Name-Paare für alle Issue-Enum-Felder zurück (Severity, Priority, Status, Resolution, Reproducibility). Bei `create_issue` werden sowohl kanonische englische Namen als auch lokalisierte `name`/`label`-Werte akzeptiert — diese Ressource hilft, alle verfügbaren Werte zu ermitteln.
1639
1696
 
1640
1697
  **Ressource-URI:** `mantis://enums`
1641
1698
 
package/docs/cookbook.md CHANGED
@@ -145,7 +145,7 @@ Returns the enum values configured on your specific MantisBT instance. Use this
145
145
  }
146
146
  ```
147
147
 
148
- > **Note:** The names returned by `get_issue_enums()` may be localized. `create_issue` and `update_issue` require **canonical English names** for `severity` and `priority` (e.g. `minor`, `major`, `normal`) look at the `canonical_name` field in the response if you are unsure.
148
+ > **Note:** On localized installations, `get_issue_enums()` returns a `name` in the local language and an optional `canonical_name` with the English original. `create_issue` accepts **both** — pass either the canonical English name (e.g. `minor`) or the localized name (e.g. `Unschönheit`). The server resolves the value automatically.
149
149
 
150
150
  ---
151
151
 
@@ -347,6 +347,8 @@ Returns only issues with a specific status. The filter is applied client-side
347
347
  }
348
348
  ```
349
349
 
350
+ > **Note:** Canonical status names (e.g. `"new"`, `"resolved"`) are resolved to their numeric ID and matched by `issue.status.id` — this works correctly even on localized installations where the API returns translated status names. Localized names passed directly (e.g. `"Neu"`) are matched by name as a fallback. The `"open"` shorthand (all statuses with id < 80) is always available regardless of installation language.
351
+
350
352
  > **Note:** For large projects with many issues, use a pre-saved MantisBT filter via `filter_id` instead — client-side filtering only scans the first 500 issues (10 pages × 50).
351
353
 
352
354
  ---
@@ -456,8 +458,8 @@ Creates a new issue in MantisBT.
456
458
  - `project_id` — numeric project ID
457
459
  - `category` — category name string
458
460
  - `description` — _(optional)_ detailed description
459
- - `priority` — _(optional)_ canonical English priority name: `none`, `low`, `normal`, `high`, `urgent`, `immediate` call `get_issue_enums()` to see localized labels
460
- - `severity` — _(optional)_ canonical English severity name: `feature`, `trivial`, `text`, `tweak`, `minor`, `major`, `crash`, `block`; default is `"minor"` call `get_issue_enums()` to see localized labels
461
+ - `priority` — _(optional)_ priority: canonical English name (`none`, `low`, `normal`, `high`, `urgent`, `immediate`) or localized label. Default: `"normal"`. Use `get_issue_enums()` to see all available values.
462
+ - `severity` — _(optional)_ severity: canonical English name (`feature`, `trivial`, `text`, `tweak`, `minor`, `major`, `crash`, `block`) or localized label. Default: `"minor"`. Use `get_issue_enums()` to see all available values.
461
463
  - `handler` — _(optional)_ assignee username (resolved to ID automatically)
462
464
  - `handler_id` — _(optional)_ assignee numeric user ID (alternative to `handler`)
463
465
 
@@ -500,11 +502,11 @@ Creates a new issue in MantisBT.
500
502
 
501
503
  **Error: unknown severity or priority**
502
504
 
503
- If an unrecognized name is passed for `severity` or `priority`, the server returns an error listing valid values:
505
+ The server first checks canonical English names, then falls back to a live `get_issue_enums` lookup. An error is only returned if the value matches neither:
504
506
 
505
- > Error: Invalid severity "schwerer Fehler". Valid canonical names: feature, trivial, text, tweak, minor, major, crash, block. Call get_issue_enums to see localized labels.
507
+ > Error: Invalid severity "xyz". Valid canonical names: feature, trivial, text, tweak, minor, major, crash, block. Call get_issue_enums to see localized labels.
506
508
 
507
- Use `get_issue_enums` to discover the canonical names accepted by `create_issue`.
509
+ Use `get_issue_enums` to discover all accepted values — both canonical and localized names work.
508
510
 
509
511
  ---
510
512
 
@@ -519,6 +521,8 @@ Resolves and closes an issue. Always set **both** `status` and `resolution` —
519
521
  - `fields.status` — status object with name
520
522
  - `fields.resolution` — resolution object with id
521
523
 
524
+ > **Note:** All enum fields (`status`, `priority`, `severity`, `resolution`, `reproducibility`) accept the canonical English name, a localized name, or a numeric `id`. The server resolves names to IDs automatically — IDs are always sent to the API, ensuring language-independence.
525
+
522
526
  **Request:**
523
527
 
524
528
  ```json
@@ -1246,6 +1250,7 @@ Finds issues semantically similar to a natural language query. Returns issue IDs
1246
1250
  **Parameters:**
1247
1251
  - `query` — natural language search query
1248
1252
  - `top_n` — _(optional)_ number of results to return; default 10
1253
+ - `highlight` — _(optional)_ when `true`, adds keyword-matched excerpts to each result; default `false`
1249
1254
 
1250
1255
  **Request:**
1251
1256
 
@@ -1280,6 +1285,7 @@ Enriches search results with specific fields fetched from MantisBT. Without `sel
1280
1285
  - `query` — natural language search query
1281
1286
  - `top_n` — _(optional)_ number of results
1282
1287
  - `select` — comma-separated field names to fetch for each result
1288
+ - `highlight` — _(optional)_ when `true`, adds keyword-matched excerpts to each result; default `false`
1283
1289
 
1284
1290
  **Request:**
1285
1291
 
@@ -1310,6 +1316,57 @@ Enriches search results with specific fields fetched from MantisBT. Without `sel
1310
1316
 
1311
1317
  ---
1312
1318
 
1319
+ ### Search with keyword highlights
1320
+
1321
+ Shows which part of an issue matched the search query. Each result that has keyword overlap with the query receives a `highlights` field with bold-marked excerpts. Highlights are keyword-based (lexical), not semantic — results with no lexical overlap will not have a `highlights` field.
1322
+
1323
+ **Tool:** `search_issues`
1324
+
1325
+ **Parameters:**
1326
+ - `query` — natural language search query
1327
+ - `top_n` — _(optional)_ number of results; default 10
1328
+ - `highlight` — set to `true` to enable highlights
1329
+
1330
+ **Request:**
1331
+
1332
+ ```json
1333
+ {
1334
+ "query": "login error after password reset",
1335
+ "top_n": 5,
1336
+ "highlight": true
1337
+ }
1338
+ ```
1339
+
1340
+ **Response:**
1341
+
1342
+ ```json
1343
+ [
1344
+ {
1345
+ "id": 1042,
1346
+ "score": 0.91,
1347
+ "highlights": {
1348
+ "summary": "**Login** button unresponsive after **password** **reset** on mobile Safari",
1349
+ "description": "…user taps **login** and nothing happens. Reproducible after a **password** **reset** flow…"
1350
+ }
1351
+ },
1352
+ {
1353
+ "id": 987,
1354
+ "score": 0.84,
1355
+ "highlights": {
1356
+ "summary": "**Login** fails with 401 — token invalidated"
1357
+ }
1358
+ },
1359
+ {
1360
+ "id": 1015,
1361
+ "score": 0.79
1362
+ }
1363
+ ]
1364
+ ```
1365
+
1366
+ > **Note:** Only results with at least one keyword match in `summary` or `description` receive a `highlights` field. The `description` excerpt is approximately 300 characters, centred around the first match. When `select` is also set and includes `summary` or `description`, highlights are generated from the fetched fields; otherwise they come from the indexed metadata.
1367
+
1368
+ ---
1369
+
1313
1370
  ## Projects & Categories
1314
1371
 
1315
1372
  ### List project categories
@@ -1635,7 +1692,7 @@ Returns a combined view of a single project: its fields plus all members, versio
1635
1692
 
1636
1693
  ### Read issue enum values
1637
1694
 
1638
- Returns valid ID/name pairs for all issue enum fields (severity, priority, status, resolution, reproducibility). Use this to look up canonical English names before calling `create_issue` or `update_issue`.
1695
+ Returns valid ID/name pairs for all issue enum fields (severity, priority, status, resolution, reproducibility). For `create_issue`, both the canonical English name and the localized `name`/`label` are accepted — this resource helps discover all available values.
1639
1696
 
1640
1697
  **Resource URI:** `mantis://enums`
1641
1698
 
@@ -155,6 +155,18 @@ Die semantische Suche versteht die *Bedeutung* deiner Frage — nicht nur einzel
155
155
 
156
156
  ---
157
157
 
158
+ ### Keyword-Highlights
159
+
160
+ > »Suche nach Login-Fehlern und zeige mir, welche Stelle im Ticket relevant ist.«
161
+
162
+ > »Finde Issues zum Passwort-Reset-Ablauf und hebe die passenden Stellen hervor, damit ich die Ergebnisse schnell überfliegen kann.«
163
+
164
+ > »Suche nach 'Rechnungsexport' und markiere die relevanten Ausschnitte in Titel und Beschreibung.«
165
+
166
+ Exakte Parameter und Response-Shape: [Cookbook — Suche mit Keyword-Highlights](cookbook.de.md#suche-mit-keyword-highlights).
167
+
168
+ ---
169
+
158
170
  ### Unscharfe / terminologieunabhängige Suche
159
171
 
160
172
  > »Finde Issues zu 'doppelten Einträgen' — sie könnten auch als 'zweimal angezeigt', 'doppelte Datensätze' oder 'Phantom-Zeilen' beschrieben sein.«