@dpesch/mantisbt-mcp-server 1.5.8 → 1.6.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.
@@ -0,0 +1,29 @@
1
+ import { fetchIssueEnums } from '../tools/config.js';
2
+ function jsonResource(uri, data) {
3
+ return {
4
+ contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(data, null, 2) }],
5
+ };
6
+ }
7
+ export function registerResources(server, client, cache) {
8
+ server.registerResource('current-user', 'mantis://me', {
9
+ title: 'Current User',
10
+ description: 'Profile of the user associated with the configured API key.',
11
+ mimeType: 'application/json',
12
+ }, async (uri) => jsonResource(uri, await client.get('users/me')));
13
+ server.registerResource('projects', 'mantis://projects', {
14
+ title: 'Projects',
15
+ description: 'All MantisBT projects accessible to the current API user. Served from local cache when fresh; falls back to live fetch. Refresh via the sync_metadata tool.',
16
+ mimeType: 'application/json',
17
+ }, async (uri) => {
18
+ const cached = await cache.loadIfValid();
19
+ const projects = cached?.projects
20
+ ?? (await client.get('projects')).projects
21
+ ?? [];
22
+ return jsonResource(uri, projects);
23
+ });
24
+ server.registerResource('issue-enums', 'mantis://enums', {
25
+ title: 'Issue Enums',
26
+ description: 'Valid values for issue enum fields: severity, priority, status, resolution, and reproducibility. Use these to look up IDs or names before creating or updating issues.',
27
+ mimeType: 'application/json',
28
+ }, async (uri) => jsonResource(uri, await fetchIssueEnums(client)));
29
+ }
@@ -25,6 +25,49 @@ function resolveCanonicalName(id, name, canonicalMap) {
25
25
  const canonical = canonicalMap[id];
26
26
  return canonical !== undefined && canonical !== name ? canonical : undefined;
27
27
  }
28
+ export async function fetchIssueEnums(client) {
29
+ const params = {};
30
+ ISSUE_ENUM_OPTIONS.forEach((opt, i) => {
31
+ params[`option[${i}]`] = opt;
32
+ });
33
+ const result = await client.get('config', params);
34
+ const configs = result.configs ?? [];
35
+ const keyMap = {
36
+ severity_enum_string: 'severity',
37
+ status_enum_string: 'status',
38
+ priority_enum_string: 'priority',
39
+ resolution_enum_string: 'resolution',
40
+ reproducibility_enum_string: 'reproducibility',
41
+ };
42
+ const enums = {};
43
+ for (const { option, value } of configs) {
44
+ const key = keyMap[option];
45
+ if (!key)
46
+ continue;
47
+ const canonicalMap = MANTIS_CANONICAL_ENUM_NAMES[key] ?? {};
48
+ if (typeof value === 'string') {
49
+ enums[key] = parseEnumString(value).map(({ id, name }) => {
50
+ const entry = { id, name };
51
+ const canonical_name = resolveCanonicalName(id, name, canonicalMap);
52
+ if (canonical_name !== undefined)
53
+ entry.canonical_name = canonical_name;
54
+ return entry;
55
+ });
56
+ }
57
+ else if (Array.isArray(value)) {
58
+ enums[key] = value.map(({ id, name, label }) => {
59
+ const entry = { id, name };
60
+ if (label && label !== name)
61
+ entry.label = label;
62
+ const canonical_name = resolveCanonicalName(id, name, canonicalMap);
63
+ if (canonical_name !== undefined)
64
+ entry.canonical_name = canonical_name;
65
+ return entry;
66
+ });
67
+ }
68
+ }
69
+ return enums;
70
+ }
28
71
  export function registerConfigTools(server, client, cache) {
29
72
  // ---------------------------------------------------------------------------
30
73
  // get_config
@@ -116,46 +159,7 @@ to use for API calls — regardless of language.`,
116
159
  },
117
160
  }, async () => {
118
161
  try {
119
- const params = {};
120
- ISSUE_ENUM_OPTIONS.forEach((opt, i) => {
121
- params[`option[${i}]`] = opt;
122
- });
123
- const result = await client.get('config', params);
124
- const configs = result.configs ?? [];
125
- const keyMap = {
126
- severity_enum_string: 'severity',
127
- status_enum_string: 'status',
128
- priority_enum_string: 'priority',
129
- resolution_enum_string: 'resolution',
130
- reproducibility_enum_string: 'reproducibility',
131
- };
132
- const enums = {};
133
- for (const { option, value } of configs) {
134
- const key = keyMap[option];
135
- if (!key)
136
- continue;
137
- const canonicalMap = MANTIS_CANONICAL_ENUM_NAMES[key] ?? {};
138
- if (typeof value === 'string') {
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
- });
146
- }
147
- else if (Array.isArray(value)) {
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
- });
157
- }
158
- }
162
+ const enums = await fetchIssueEnums(client);
159
163
  return {
160
164
  content: [{ type: 'text', text: JSON.stringify(enums, null, 2) }],
161
165
  };
@@ -1,12 +1,21 @@
1
1
  import { z } from 'zod';
2
2
  import { getVersionHint } from '../version-hint.js';
3
- import { MANTIS_RESOLVED_STATUS_ID } from '../constants.js';
3
+ import { MANTIS_CANONICAL_ENUM_NAMES, MANTIS_RESOLVED_STATUS_ID, resolveEnumId } from '../constants.js';
4
4
  function errorText(msg) {
5
5
  const vh = getVersionHint();
6
6
  vh?.triggerLatestVersionFetch();
7
7
  const hint = vh?.getUpdateHint();
8
8
  return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
9
9
  }
10
+ // Resolves a canonical enum name to { id } or returns an error string.
11
+ function resolveEnum(group, value) {
12
+ 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.`;
16
+ }
17
+ return { id };
18
+ }
10
19
  export function registerIssueTools(server, client, cache) {
11
20
  // ---------------------------------------------------------------------------
12
21
  // get_issue
@@ -138,8 +147,8 @@ export function registerIssueTools(server, client, cache) {
138
147
  description: z.string().default('').describe('Detailed issue description'),
139
148
  project_id: z.coerce.number().int().positive().describe('Project ID the issue belongs to'),
140
149
  category: z.string().min(1).describe('Category name (use get_project_categories to list available categories)'),
141
- priority: z.string().optional().describe('Priority name (e.g. "normal", "high", "urgent", "immediate", "low", "none")'),
142
- severity: z.string().default('minor').describe('Severity name (e.g. "minor", "major", "crash", "block", "feature", "trivial", "text") default: "minor"'),
150
+ priority: z.string().optional().describe('Priority name must be a canonical English name: none, low, normal, high, urgent, immediate. 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.'),
143
152
  handler_id: z.coerce.number().int().positive().optional().describe('User ID of the person to assign the issue to'),
144
153
  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.'),
145
154
  }),
@@ -180,9 +189,16 @@ export function registerIssueTools(server, client, cache) {
180
189
  project: { id: project_id },
181
190
  category: { name: category },
182
191
  };
183
- if (priority)
184
- body.priority = { name: priority };
185
- body.severity = { name: severity };
192
+ if (priority) {
193
+ const priorityResolved = resolveEnum('priority', priority);
194
+ if (typeof priorityResolved === 'string')
195
+ return { content: [{ type: 'text', text: errorText(priorityResolved) }], isError: true };
196
+ body.priority = priorityResolved;
197
+ }
198
+ const severityResolved = resolveEnum('severity', severity);
199
+ if (typeof severityResolved === 'string')
200
+ return { content: [{ type: 'text', text: errorText(severityResolved) }], isError: true };
201
+ body.severity = severityResolved;
186
202
  if (resolvedHandlerId)
187
203
  body.handler = { id: resolvedHandlerId };
188
204
  const raw = await client.post('issues', body);