@dpesch/mantisbt-mcp-server 1.0.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 +2 -0
- package/CHANGELOG.md +68 -0
- package/LICENSE +21 -0
- package/README.de.md +177 -0
- package/README.md +177 -0
- package/dist/cache.js +52 -0
- package/dist/client.js +114 -0
- package/dist/config.js +54 -0
- package/dist/constants.js +23 -0
- package/dist/index.js +120 -0
- package/dist/tools/config.js +107 -0
- package/dist/tools/files.js +37 -0
- package/dist/tools/filters.js +35 -0
- package/dist/tools/issues.js +191 -0
- package/dist/tools/metadata.js +119 -0
- package/dist/tools/monitors.js +38 -0
- package/dist/tools/notes.js +96 -0
- package/dist/tools/projects.js +127 -0
- package/dist/tools/relationships.js +54 -0
- package/dist/tools/tags.js +78 -0
- package/dist/tools/users.js +34 -0
- package/dist/tools/version.js +58 -0
- package/dist/types.js +4 -0
- package/dist/version-hint.js +117 -0
- package/package.json +41 -0
- package/scripts/record-fixtures.ts +138 -0
- package/tests/cache.test.ts +149 -0
- package/tests/client.test.ts +241 -0
- package/tests/config.test.ts +164 -0
- package/tests/fixtures/get_current_user.json +39 -0
- package/tests/fixtures/get_issue.json +151 -0
- package/tests/fixtures/get_project_categories.json +60 -0
- package/tests/fixtures/get_project_versions.json +3 -0
- package/tests/fixtures/get_project_versions_with_data.json +28 -0
- package/tests/fixtures/list_issues.json +67 -0
- package/tests/fixtures/list_projects.json +65 -0
- package/tests/fixtures/recorded/get_current_user.json +108 -0
- package/tests/fixtures/recorded/get_issue.json +320 -0
- package/tests/fixtures/recorded/get_project_categories.json +241 -0
- package/tests/fixtures/recorded/get_project_versions.json +3 -0
- package/tests/fixtures/recorded/list_issues.json +824 -0
- package/tests/fixtures/recorded/list_projects.json +10641 -0
- package/tests/helpers/mock-server.ts +32 -0
- package/tests/tools/issues.test.ts +130 -0
- package/tests/tools/projects.test.ts +169 -0
- package/tests/tools/users.test.ts +76 -0
- package/tests/version-hint.test.ts +230 -0
- package/tsconfig.build.json +8 -0
- package/vitest.config.ts +8 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
5
|
+
import { createServer } from 'node:http';
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { join, dirname } from 'node:path';
|
|
9
|
+
import { getConfig } from './config.js';
|
|
10
|
+
import { MantisClient } from './client.js';
|
|
11
|
+
import { VersionHintService, setGlobalVersionHint } from './version-hint.js';
|
|
12
|
+
import { MetadataCache } from './cache.js';
|
|
13
|
+
import { registerIssueTools } from './tools/issues.js';
|
|
14
|
+
import { registerNoteTools } from './tools/notes.js';
|
|
15
|
+
import { registerFileTools } from './tools/files.js';
|
|
16
|
+
import { registerRelationshipTools } from './tools/relationships.js';
|
|
17
|
+
import { registerMonitorTools } from './tools/monitors.js';
|
|
18
|
+
import { registerProjectTools } from './tools/projects.js';
|
|
19
|
+
import { registerUserTools } from './tools/users.js';
|
|
20
|
+
import { registerFilterTools } from './tools/filters.js';
|
|
21
|
+
import { registerConfigTools } from './tools/config.js';
|
|
22
|
+
import { registerMetadataTools } from './tools/metadata.js';
|
|
23
|
+
import { registerTagTools } from './tools/tags.js';
|
|
24
|
+
import { registerVersionTools } from './tools/version.js';
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Read version from package.json
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
29
|
+
const __dirname = dirname(__filename);
|
|
30
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
31
|
+
const version = packageJson.version;
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Bootstrap
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
async function createMcpServer() {
|
|
36
|
+
const config = await getConfig();
|
|
37
|
+
const versionHint = new VersionHintService();
|
|
38
|
+
setGlobalVersionHint(versionHint);
|
|
39
|
+
const client = new MantisClient(config.baseUrl, config.apiKey, (response) => versionHint.onSuccessfulResponse(response));
|
|
40
|
+
const cache = new MetadataCache(config.cacheDir, config.cacheTtl);
|
|
41
|
+
const server = new McpServer({
|
|
42
|
+
name: 'mantisbt-mcp-server',
|
|
43
|
+
version,
|
|
44
|
+
});
|
|
45
|
+
registerIssueTools(server, client);
|
|
46
|
+
registerNoteTools(server, client);
|
|
47
|
+
registerFileTools(server, client);
|
|
48
|
+
registerRelationshipTools(server, client);
|
|
49
|
+
registerMonitorTools(server, client);
|
|
50
|
+
registerProjectTools(server, client);
|
|
51
|
+
registerUserTools(server, client);
|
|
52
|
+
registerFilterTools(server, client);
|
|
53
|
+
registerConfigTools(server, client);
|
|
54
|
+
registerMetadataTools(server, client, cache);
|
|
55
|
+
registerTagTools(server, client);
|
|
56
|
+
registerVersionTools(server, client, versionHint);
|
|
57
|
+
return server;
|
|
58
|
+
}
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Transport: stdio (default) or HTTP
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
async function runStdio() {
|
|
63
|
+
const server = await createMcpServer();
|
|
64
|
+
const transport = new StdioServerTransport();
|
|
65
|
+
await server.connect(transport);
|
|
66
|
+
console.error(`MantisBT MCP Server v${version} running (stdio)`);
|
|
67
|
+
}
|
|
68
|
+
async function runHttp() {
|
|
69
|
+
const server = await createMcpServer();
|
|
70
|
+
const port = parseInt(process.env.PORT ?? '3000', 10);
|
|
71
|
+
const httpServer = createServer(async (req, res) => {
|
|
72
|
+
if (req.method === 'POST' && req.url === '/mcp') {
|
|
73
|
+
const chunks = [];
|
|
74
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
75
|
+
req.on('end', async () => {
|
|
76
|
+
try {
|
|
77
|
+
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
78
|
+
const transport = new StreamableHTTPServerTransport({
|
|
79
|
+
sessionIdGenerator: undefined,
|
|
80
|
+
enableJsonResponse: true,
|
|
81
|
+
});
|
|
82
|
+
res.on('close', () => transport.close());
|
|
83
|
+
await server.connect(transport);
|
|
84
|
+
await transport.handleRequest(req, res, body);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
88
|
+
res.end(JSON.stringify({ error: 'Bad Request' }));
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
else if (req.method === 'GET' && req.url === '/health') {
|
|
93
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
94
|
+
res.end(JSON.stringify({ status: 'ok', server: 'mantisbt-mcp-server', version }));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
res.writeHead(404);
|
|
98
|
+
res.end();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
httpServer.listen(port, () => {
|
|
102
|
+
console.error(`MantisBT MCP Server v${version} running on http://localhost:${port}/mcp`);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Start
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
const transport = process.env.TRANSPORT ?? 'stdio';
|
|
109
|
+
if (transport === 'http') {
|
|
110
|
+
runHttp().catch((err) => {
|
|
111
|
+
console.error('Server startup error:', err instanceof Error ? err.message : err);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
runStdio().catch((err) => {
|
|
117
|
+
console.error('Server startup error:', err instanceof Error ? err.message : err);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getVersionHint } from '../version-hint.js';
|
|
3
|
+
function errorText(msg) {
|
|
4
|
+
const vh = getVersionHint();
|
|
5
|
+
vh?.triggerLatestVersionFetch();
|
|
6
|
+
const hint = vh?.getUpdateHint();
|
|
7
|
+
return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
|
|
8
|
+
}
|
|
9
|
+
export function registerConfigTools(server, client) {
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// get_config
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
server.registerTool('get_config', {
|
|
14
|
+
title: 'Get MantisBT Configuration',
|
|
15
|
+
description: `Retrieve one or more MantisBT configuration options.
|
|
16
|
+
|
|
17
|
+
Common option names:
|
|
18
|
+
- "status_enum_string" — issue status values and their IDs
|
|
19
|
+
- "priority_enum_string" — priority values
|
|
20
|
+
- "severity_enum_string" — severity values
|
|
21
|
+
- "resolution_enum_string" — resolution values
|
|
22
|
+
- "reproducibility_enum_string" — reproducibility values
|
|
23
|
+
- "view_state_enum_string" — view state values
|
|
24
|
+
- "access_levels_enum_string" — access level values`,
|
|
25
|
+
inputSchema: z.object({
|
|
26
|
+
options: z.array(z.string()).min(1).describe('Array of configuration option names to retrieve'),
|
|
27
|
+
}),
|
|
28
|
+
annotations: {
|
|
29
|
+
readOnlyHint: true,
|
|
30
|
+
destructiveHint: false,
|
|
31
|
+
idempotentHint: true,
|
|
32
|
+
},
|
|
33
|
+
}, async ({ options }) => {
|
|
34
|
+
try {
|
|
35
|
+
const params = {};
|
|
36
|
+
options.forEach((opt, i) => {
|
|
37
|
+
params[`option[${i}]`] = opt;
|
|
38
|
+
});
|
|
39
|
+
const result = await client.get('config', params);
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
46
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// list_languages
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
server.registerTool('list_languages', {
|
|
53
|
+
title: 'List Supported Languages',
|
|
54
|
+
description: 'List all languages supported by the MantisBT installation.',
|
|
55
|
+
inputSchema: z.object({}),
|
|
56
|
+
annotations: {
|
|
57
|
+
readOnlyHint: true,
|
|
58
|
+
destructiveHint: false,
|
|
59
|
+
idempotentHint: true,
|
|
60
|
+
},
|
|
61
|
+
}, async () => {
|
|
62
|
+
try {
|
|
63
|
+
const result = await client.get('lang');
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
70
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// list_tags
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
server.registerTool('list_tags', {
|
|
77
|
+
title: 'List Tags',
|
|
78
|
+
description: `List all tags defined in the MantisBT installation.
|
|
79
|
+
|
|
80
|
+
Note: The GET /tags endpoint is not available in MantisBT 2.25 and earlier.
|
|
81
|
+
If your MantisBT version does not support this endpoint, you will receive an error.
|
|
82
|
+
In that case, use get_issue to read the tags of a specific issue instead.`,
|
|
83
|
+
inputSchema: z.object({
|
|
84
|
+
page: z.number().int().positive().default(1).describe('Page number (default: 1)'),
|
|
85
|
+
page_size: z.number().int().min(1).max(200).default(50).describe('Tags per page (default: 50)'),
|
|
86
|
+
}),
|
|
87
|
+
annotations: {
|
|
88
|
+
readOnlyHint: true,
|
|
89
|
+
destructiveHint: false,
|
|
90
|
+
idempotentHint: true,
|
|
91
|
+
},
|
|
92
|
+
}, async ({ page, page_size }) => {
|
|
93
|
+
try {
|
|
94
|
+
const result = await client.get('tags', { page, page_size });
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
101
|
+
const hint = msg.includes('404')
|
|
102
|
+
? `${msg}\n\nThe GET /tags endpoint is not supported by this MantisBT version. Use get_issue to read tags of a specific issue instead.`
|
|
103
|
+
: msg;
|
|
104
|
+
return { content: [{ type: 'text', text: `Error: ${hint}` }], isError: true };
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getVersionHint } from '../version-hint.js';
|
|
3
|
+
function errorText(msg) {
|
|
4
|
+
const vh = getVersionHint();
|
|
5
|
+
vh?.triggerLatestVersionFetch();
|
|
6
|
+
const hint = vh?.getUpdateHint();
|
|
7
|
+
return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
|
|
8
|
+
}
|
|
9
|
+
export function registerFileTools(server, client) {
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// list_issue_files
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
server.registerTool('list_issue_files', {
|
|
14
|
+
title: 'List Issue File Attachments',
|
|
15
|
+
description: 'List all file attachments of a MantisBT issue.',
|
|
16
|
+
inputSchema: z.object({
|
|
17
|
+
issue_id: z.number().int().positive().describe('Numeric issue ID'),
|
|
18
|
+
}),
|
|
19
|
+
annotations: {
|
|
20
|
+
readOnlyHint: true,
|
|
21
|
+
destructiveHint: false,
|
|
22
|
+
idempotentHint: true,
|
|
23
|
+
},
|
|
24
|
+
}, async ({ issue_id }) => {
|
|
25
|
+
try {
|
|
26
|
+
const result = await client.get(`issues/${issue_id}`);
|
|
27
|
+
const attachments = result.issues?.[0]?.attachments ?? [];
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: 'text', text: JSON.stringify(attachments, null, 2) }],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
34
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getVersionHint } from '../version-hint.js';
|
|
3
|
+
function errorText(msg) {
|
|
4
|
+
const vh = getVersionHint();
|
|
5
|
+
vh?.triggerLatestVersionFetch();
|
|
6
|
+
const hint = vh?.getUpdateHint();
|
|
7
|
+
return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
|
|
8
|
+
}
|
|
9
|
+
export function registerFilterTools(server, client) {
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// list_filters
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
server.registerTool('list_filters', {
|
|
14
|
+
title: 'List Saved Filters',
|
|
15
|
+
description: 'List all saved MantisBT issue filters accessible to the current user. Filter IDs can be used with list_issues.',
|
|
16
|
+
inputSchema: z.object({}),
|
|
17
|
+
annotations: {
|
|
18
|
+
readOnlyHint: true,
|
|
19
|
+
destructiveHint: false,
|
|
20
|
+
idempotentHint: true,
|
|
21
|
+
},
|
|
22
|
+
}, async () => {
|
|
23
|
+
try {
|
|
24
|
+
const result = await client.get('filters');
|
|
25
|
+
const filters = Array.isArray(result) ? result : result.filters ?? result;
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: 'text', text: JSON.stringify(filters, null, 2) }],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
32
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getVersionHint } from '../version-hint.js';
|
|
3
|
+
function errorText(msg) {
|
|
4
|
+
const vh = getVersionHint();
|
|
5
|
+
vh?.triggerLatestVersionFetch();
|
|
6
|
+
const hint = vh?.getUpdateHint();
|
|
7
|
+
return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
|
|
8
|
+
}
|
|
9
|
+
export function registerIssueTools(server, client) {
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// get_issue
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
server.registerTool('get_issue', {
|
|
14
|
+
title: 'Get Issue',
|
|
15
|
+
description: 'Retrieve a single MantisBT issue by its numeric ID. Returns all issue fields including notes, attachments, and relationships.',
|
|
16
|
+
inputSchema: z.object({
|
|
17
|
+
id: z.number().int().positive().describe('Numeric issue ID'),
|
|
18
|
+
}),
|
|
19
|
+
annotations: {
|
|
20
|
+
readOnlyHint: true,
|
|
21
|
+
destructiveHint: false,
|
|
22
|
+
idempotentHint: true,
|
|
23
|
+
},
|
|
24
|
+
}, async ({ id }) => {
|
|
25
|
+
try {
|
|
26
|
+
const result = await client.get(`issues/${id}`);
|
|
27
|
+
const issue = result.issues?.[0] ?? result;
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: 'text', text: JSON.stringify(issue, null, 2) }],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
34
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// list_issues
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
server.registerTool('list_issues', {
|
|
41
|
+
title: 'List Issues',
|
|
42
|
+
description: 'List MantisBT issues with optional filtering. Returns a paginated list of issues.',
|
|
43
|
+
inputSchema: z.object({
|
|
44
|
+
project_id: z.number().int().positive().optional().describe('Filter by project ID'),
|
|
45
|
+
page: z.number().int().positive().default(1).describe('Page number (default: 1)'),
|
|
46
|
+
page_size: z.number().int().min(1).max(50).default(50).describe('Issues per page (default: 50, max: 50)'),
|
|
47
|
+
assigned_to: z.number().int().positive().optional().describe('Filter by handler/assignee user ID'),
|
|
48
|
+
reporter_id: z.number().int().positive().optional().describe('Filter by reporter user ID'),
|
|
49
|
+
filter_id: z.number().int().positive().optional().describe('Use a saved MantisBT filter ID'),
|
|
50
|
+
sort: z.string().optional().describe('Sort field (e.g. "last_updated", "id")'),
|
|
51
|
+
direction: z.enum(['ASC', 'DESC']).optional().describe('Sort direction'),
|
|
52
|
+
}),
|
|
53
|
+
annotations: {
|
|
54
|
+
readOnlyHint: true,
|
|
55
|
+
destructiveHint: false,
|
|
56
|
+
idempotentHint: true,
|
|
57
|
+
},
|
|
58
|
+
}, async ({ project_id, page, page_size, assigned_to, reporter_id, filter_id, sort, direction }) => {
|
|
59
|
+
try {
|
|
60
|
+
const params = {
|
|
61
|
+
page,
|
|
62
|
+
page_size,
|
|
63
|
+
project_id,
|
|
64
|
+
assigned_to,
|
|
65
|
+
reporter_id,
|
|
66
|
+
filter_id,
|
|
67
|
+
sort,
|
|
68
|
+
direction,
|
|
69
|
+
};
|
|
70
|
+
const result = await client.get('issues', params);
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
77
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// create_issue
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
server.registerTool('create_issue', {
|
|
84
|
+
title: 'Create Issue',
|
|
85
|
+
description: 'Create a new MantisBT issue. Returns the created issue including its assigned ID.',
|
|
86
|
+
inputSchema: z.object({
|
|
87
|
+
summary: z.string().min(1).describe('Issue summary/title'),
|
|
88
|
+
description: z.string().default('').describe('Detailed issue description'),
|
|
89
|
+
project_id: z.number().int().positive().describe('Project ID the issue belongs to'),
|
|
90
|
+
category: z.string().min(1).describe('Category name (use get_project_categories to list available categories)'),
|
|
91
|
+
priority: z.string().optional().describe('Priority name (e.g. "normal", "high", "urgent", "immediate", "low", "none")'),
|
|
92
|
+
severity: z.string().optional().describe('Severity name (e.g. "minor", "major", "crash", "block", "feature", "trivial", "text")'),
|
|
93
|
+
handler_id: z.number().int().positive().optional().describe('User ID of the person to assign the issue to'),
|
|
94
|
+
}),
|
|
95
|
+
annotations: {
|
|
96
|
+
readOnlyHint: false,
|
|
97
|
+
destructiveHint: false,
|
|
98
|
+
idempotentHint: false,
|
|
99
|
+
},
|
|
100
|
+
}, async ({ summary, description, project_id, category, priority, severity, handler_id }) => {
|
|
101
|
+
try {
|
|
102
|
+
const body = {
|
|
103
|
+
summary,
|
|
104
|
+
description,
|
|
105
|
+
project: { id: project_id },
|
|
106
|
+
category: { name: category },
|
|
107
|
+
};
|
|
108
|
+
if (priority)
|
|
109
|
+
body.priority = { name: priority };
|
|
110
|
+
if (severity)
|
|
111
|
+
body.severity = { name: severity };
|
|
112
|
+
if (handler_id)
|
|
113
|
+
body.handler = { id: handler_id };
|
|
114
|
+
const result = await client.post('issues', body);
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: 'text', text: JSON.stringify(result.issue ?? result, null, 2) }],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
121
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// update_issue
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
server.registerTool('update_issue', {
|
|
128
|
+
title: 'Update Issue',
|
|
129
|
+
description: `Update one or more fields of an existing MantisBT issue using a partial PATCH.
|
|
130
|
+
|
|
131
|
+
The "fields" object accepts any combination of:
|
|
132
|
+
- summary (string)
|
|
133
|
+
- description (string)
|
|
134
|
+
- status: { name: "new"|"feedback"|"acknowledged"|"confirmed"|"assigned"|"resolved"|"closed" }
|
|
135
|
+
- resolution: { id: 20 } (20 = fixed/resolved)
|
|
136
|
+
- handler: { id: <user_id> } or { name: "<username>" }
|
|
137
|
+
- priority: { name: "<priority_name>" }
|
|
138
|
+
- severity: { name: "<severity_name>" }
|
|
139
|
+
- category: { name: "<category_name>" }
|
|
140
|
+
- target_version: { name: "<version_name>" }
|
|
141
|
+
- fixed_in_version: { name: "<version_name>" }
|
|
142
|
+
|
|
143
|
+
Important: when resolving an issue, always set BOTH status and resolution to avoid leaving resolution as "open".`,
|
|
144
|
+
inputSchema: z.object({
|
|
145
|
+
id: z.number().int().positive().describe('Numeric issue ID to update'),
|
|
146
|
+
fields: z.record(z.unknown()).describe('Object containing the fields to update (partial update — only provided fields are changed)'),
|
|
147
|
+
}),
|
|
148
|
+
annotations: {
|
|
149
|
+
readOnlyHint: false,
|
|
150
|
+
destructiveHint: false,
|
|
151
|
+
idempotentHint: false,
|
|
152
|
+
},
|
|
153
|
+
}, async ({ id, fields }) => {
|
|
154
|
+
try {
|
|
155
|
+
const result = await client.patch(`issues/${id}`, fields);
|
|
156
|
+
return {
|
|
157
|
+
content: [{ type: 'text', text: JSON.stringify(result.issue ?? result, null, 2) }],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
162
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// delete_issue
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
server.registerTool('delete_issue', {
|
|
169
|
+
title: 'Delete Issue',
|
|
170
|
+
description: 'Permanently delete a MantisBT issue. This action is irreversible.',
|
|
171
|
+
inputSchema: z.object({
|
|
172
|
+
id: z.number().int().positive().describe('Numeric issue ID to delete'),
|
|
173
|
+
}),
|
|
174
|
+
annotations: {
|
|
175
|
+
readOnlyHint: false,
|
|
176
|
+
destructiveHint: true,
|
|
177
|
+
idempotentHint: true,
|
|
178
|
+
},
|
|
179
|
+
}, async ({ id }) => {
|
|
180
|
+
try {
|
|
181
|
+
await client.delete(`issues/${id}`);
|
|
182
|
+
return {
|
|
183
|
+
content: [{ type: 'text', text: `Issue #${id} deleted successfully.` }],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
188
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getVersionHint } from '../version-hint.js';
|
|
3
|
+
function errorText(msg) {
|
|
4
|
+
const vh = getVersionHint();
|
|
5
|
+
vh?.triggerLatestVersionFetch();
|
|
6
|
+
const hint = vh?.getUpdateHint();
|
|
7
|
+
return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
|
|
8
|
+
}
|
|
9
|
+
async function fetchAndCacheMetadata(client, cache) {
|
|
10
|
+
// Fetch all projects
|
|
11
|
+
const projectResult = await client.get('projects');
|
|
12
|
+
const projects = projectResult.projects ?? [];
|
|
13
|
+
const byProject = {};
|
|
14
|
+
// For each project, fetch users, versions, categories in parallel
|
|
15
|
+
await Promise.all(projects.map(async (project) => {
|
|
16
|
+
const [usersResult, versionsResult, categoriesResult] = await Promise.allSettled([
|
|
17
|
+
client.get(`projects/${project.id}/users`),
|
|
18
|
+
client.get(`projects/${project.id}/versions`),
|
|
19
|
+
client.get(`projects/${project.id}/categories`),
|
|
20
|
+
]);
|
|
21
|
+
const users = usersResult.status === 'fulfilled'
|
|
22
|
+
? (usersResult.value.users ?? [])
|
|
23
|
+
: [];
|
|
24
|
+
const versions = versionsResult.status === 'fulfilled'
|
|
25
|
+
? (versionsResult.value.versions ?? [])
|
|
26
|
+
: [];
|
|
27
|
+
const ALL_PROJECTS_PREFIX = '[All Projects] ';
|
|
28
|
+
const rawCategories = categoriesResult.status === 'fulfilled'
|
|
29
|
+
? (categoriesResult.value.categories ?? categoriesResult.value)
|
|
30
|
+
: [];
|
|
31
|
+
const categories = Array.isArray(rawCategories)
|
|
32
|
+
? rawCategories.map((cat) => ({
|
|
33
|
+
...cat,
|
|
34
|
+
name: cat.name.startsWith(ALL_PROJECTS_PREFIX)
|
|
35
|
+
? cat.name.slice(ALL_PROJECTS_PREFIX.length)
|
|
36
|
+
: cat.name,
|
|
37
|
+
}))
|
|
38
|
+
: rawCategories;
|
|
39
|
+
byProject[project.id] = { users, versions, categories };
|
|
40
|
+
}));
|
|
41
|
+
const data = {
|
|
42
|
+
timestamp: Date.now(),
|
|
43
|
+
projects,
|
|
44
|
+
byProject,
|
|
45
|
+
};
|
|
46
|
+
await cache.save(data);
|
|
47
|
+
return data;
|
|
48
|
+
}
|
|
49
|
+
export function registerMetadataTools(server, client, cache) {
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// sync_metadata
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
server.registerTool('sync_metadata', {
|
|
54
|
+
title: 'Sync Metadata Cache',
|
|
55
|
+
description: `Fetch all projects and their associated users, versions, and categories from MantisBT and store them in the local metadata cache.
|
|
56
|
+
|
|
57
|
+
This is useful for getting a complete overview of your MantisBT installation.
|
|
58
|
+
The cache is valid for 1 hour by default (configurable via MANTIS_CACHE_TTL env var).
|
|
59
|
+
Use this tool to refresh stale data.`,
|
|
60
|
+
inputSchema: z.object({}),
|
|
61
|
+
annotations: {
|
|
62
|
+
readOnlyHint: false,
|
|
63
|
+
destructiveHint: false,
|
|
64
|
+
idempotentHint: false,
|
|
65
|
+
},
|
|
66
|
+
}, async () => {
|
|
67
|
+
try {
|
|
68
|
+
const data = await fetchAndCacheMetadata(client, cache);
|
|
69
|
+
const projectCount = data.projects.length;
|
|
70
|
+
const summary = data.projects.map((p) => {
|
|
71
|
+
const meta = data.byProject[p.id];
|
|
72
|
+
return ` - ${p.name} (ID ${p.id}): ${meta?.users.length ?? 0} users, ${meta?.versions.length ?? 0} versions, ${meta?.categories.length ?? 0} categories`;
|
|
73
|
+
}).join('\n');
|
|
74
|
+
return {
|
|
75
|
+
content: [{
|
|
76
|
+
type: 'text',
|
|
77
|
+
text: `Metadata synced successfully.\n\n${projectCount} project(s):\n${summary}`,
|
|
78
|
+
}],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
83
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// get_metadata
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
server.registerTool('get_metadata', {
|
|
90
|
+
title: 'Get Cached Metadata',
|
|
91
|
+
description: `Return cached MantisBT metadata (projects, users, versions, categories).
|
|
92
|
+
|
|
93
|
+
If the cache does not exist or has expired (default TTL: 24 hours), it will automatically sync first.
|
|
94
|
+
Use sync_metadata to force a refresh.`,
|
|
95
|
+
inputSchema: z.object({}),
|
|
96
|
+
annotations: {
|
|
97
|
+
readOnlyHint: true,
|
|
98
|
+
destructiveHint: false,
|
|
99
|
+
idempotentHint: true,
|
|
100
|
+
},
|
|
101
|
+
}, async () => {
|
|
102
|
+
try {
|
|
103
|
+
let data = null;
|
|
104
|
+
if (await cache.isValid()) {
|
|
105
|
+
data = await cache.load();
|
|
106
|
+
}
|
|
107
|
+
if (!data) {
|
|
108
|
+
data = await fetchAndCacheMetadata(client, cache);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
116
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getVersionHint } from '../version-hint.js';
|
|
3
|
+
function errorText(msg) {
|
|
4
|
+
const vh = getVersionHint();
|
|
5
|
+
vh?.triggerLatestVersionFetch();
|
|
6
|
+
const hint = vh?.getUpdateHint();
|
|
7
|
+
return hint ? `Error: ${msg}\n\n${hint}` : `Error: ${msg}`;
|
|
8
|
+
}
|
|
9
|
+
export function registerMonitorTools(server, client) {
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// add_monitor
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
server.registerTool('add_monitor', {
|
|
14
|
+
title: 'Add Issue Monitor',
|
|
15
|
+
description: 'Add a user as a monitor (watcher) of a MantisBT issue. Monitors receive email notifications for issue updates.',
|
|
16
|
+
inputSchema: z.object({
|
|
17
|
+
issue_id: z.number().int().positive().describe('Numeric issue ID'),
|
|
18
|
+
username: z.string().min(1).describe('Username of the user to add as monitor'),
|
|
19
|
+
}),
|
|
20
|
+
annotations: {
|
|
21
|
+
readOnlyHint: false,
|
|
22
|
+
destructiveHint: false,
|
|
23
|
+
idempotentHint: false,
|
|
24
|
+
},
|
|
25
|
+
}, async ({ issue_id, username }) => {
|
|
26
|
+
try {
|
|
27
|
+
const body = { name: username };
|
|
28
|
+
const result = await client.post(`issues/${issue_id}/monitors`, body);
|
|
29
|
+
return {
|
|
30
|
+
content: [{ type: 'text', text: JSON.stringify(result ?? { success: true }, null, 2) }],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
35
|
+
return { content: [{ type: 'text', text: errorText(msg) }], isError: true };
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|