@blackenedd18/planio-connector 2026.623.2
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.example +9 -0
- package/LICENSE +15 -0
- package/README.md +101 -0
- package/Redmine-functions/README.md +14 -0
- package/Redmine-functions/index.js +648 -0
- package/Redmine-functions/package.json +5 -0
- package/package.json +48 -0
- package/src/index.js +16 -0
- package/src/modules/hours/index.js +195 -0
- package/src/modules/issues/index.js +281 -0
- package/src/modules/projects/index.js +62 -0
- package/src/modules/time-entries/index.js +267 -0
- package/src/modules/users/index.js +87 -0
- package/src/server.js +87 -0
- package/src/shared/config.js +84 -0
- package/src/shared/date-validation.js +45 -0
- package/src/shared/logger.js +48 -0
- package/src/shared/redmine-functions-adapter.js +58 -0
- package/src/shared/redmine-functions-contract.js +4 -0
- package/src/shared/redmine-functions-entry.js +3 -0
- package/src/shared/request.js +97 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { RedmineServer } from './server.js';
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
const app = new RedmineServer();
|
|
7
|
+
await app.run();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
main().catch((error) => {
|
|
11
|
+
console.error('[MCP Error] Fatal startup error', {
|
|
12
|
+
message: error?.message,
|
|
13
|
+
stack: error?.stack,
|
|
14
|
+
});
|
|
15
|
+
process.exit(1);
|
|
16
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { logDebug, logError, logWarn } from '../../shared/logger.js';
|
|
2
|
+
import { makeRequest } from '../../shared/request.js';
|
|
3
|
+
import { ensureOptionalIsoDate, resolveDateAlias } from '../../shared/date-validation.js';
|
|
4
|
+
|
|
5
|
+
const SHARED_OPTIONAL_FIELDS = ['project_id', 'issue_id', 'limit'];
|
|
6
|
+
const USER_AWARE_FIELDS = [...SHARED_OPTIONAL_FIELDS, 'user_id'];
|
|
7
|
+
const DATE_FIELDS = ['from', 'to', 'from_date', 'to_date'];
|
|
8
|
+
|
|
9
|
+
export const TOOLS = [
|
|
10
|
+
{
|
|
11
|
+
name: 'get_total_hours_by_weekly',
|
|
12
|
+
description: 'Return weekly total hours with optional filters.',
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
user_id: { type: 'number' },
|
|
17
|
+
project_id: { type: 'number' },
|
|
18
|
+
issue_id: { type: 'number' },
|
|
19
|
+
from: { type: 'string' },
|
|
20
|
+
to: { type: 'string' },
|
|
21
|
+
from_date: { type: 'string' },
|
|
22
|
+
to_date: { type: 'string' },
|
|
23
|
+
limit: { type: 'number' },
|
|
24
|
+
},
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'get_total_hours_by_monthly',
|
|
30
|
+
description: 'Return monthly total hours with optional filters.',
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
user_id: { type: 'number' },
|
|
35
|
+
project_id: { type: 'number' },
|
|
36
|
+
issue_id: { type: 'number' },
|
|
37
|
+
from: { type: 'string' },
|
|
38
|
+
to: { type: 'string' },
|
|
39
|
+
from_date: { type: 'string' },
|
|
40
|
+
to_date: { type: 'string' },
|
|
41
|
+
limit: { type: 'number' },
|
|
42
|
+
},
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'get_total_hours_by_weekly_per_people',
|
|
48
|
+
description: 'Return weekly totals per user with optional filters.',
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
project_id: { type: 'number' },
|
|
53
|
+
issue_id: { type: 'number' },
|
|
54
|
+
from: { type: 'string' },
|
|
55
|
+
to: { type: 'string' },
|
|
56
|
+
from_date: { type: 'string' },
|
|
57
|
+
to_date: { type: 'string' },
|
|
58
|
+
limit: { type: 'number' },
|
|
59
|
+
},
|
|
60
|
+
additionalProperties: false,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'get_total_hours_by_monthly_per_people',
|
|
65
|
+
description: 'Return monthly totals per user with optional filters.',
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
properties: {
|
|
69
|
+
project_id: { type: 'number' },
|
|
70
|
+
issue_id: { type: 'number' },
|
|
71
|
+
from: { type: 'string' },
|
|
72
|
+
to: { type: 'string' },
|
|
73
|
+
from_date: { type: 'string' },
|
|
74
|
+
to_date: { type: 'string' },
|
|
75
|
+
limit: { type: 'number' },
|
|
76
|
+
},
|
|
77
|
+
additionalProperties: false,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'get_total_hours_by_day',
|
|
82
|
+
description: 'Return total hours by day with optional filters.',
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {
|
|
86
|
+
user_id: { type: 'number' },
|
|
87
|
+
project_id: { type: 'number' },
|
|
88
|
+
issue_id: { type: 'number' },
|
|
89
|
+
from: { type: 'string' },
|
|
90
|
+
to: { type: 'string' },
|
|
91
|
+
from_date: { type: 'string' },
|
|
92
|
+
to_date: { type: 'string' },
|
|
93
|
+
limit: { type: 'number' },
|
|
94
|
+
},
|
|
95
|
+
additionalProperties: false,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
function logToolStart(name, args) {
|
|
101
|
+
logDebug('[hours.dispatch] Handling tool', {
|
|
102
|
+
toolName: name,
|
|
103
|
+
argumentKeys: Object.keys(args || {}),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildParams(args, fields) {
|
|
108
|
+
const params = {};
|
|
109
|
+
|
|
110
|
+
for (const field of fields) {
|
|
111
|
+
if (args[field] !== undefined && args[field] !== null) {
|
|
112
|
+
params[field] = args[field];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const fromDate = resolveDateAlias(args, 'from', 'from_date');
|
|
117
|
+
const toDate = resolveDateAlias(args, 'to', 'to_date');
|
|
118
|
+
|
|
119
|
+
if (fromDate.value !== undefined && fromDate.value !== null) {
|
|
120
|
+
ensureOptionalIsoDate({ from: fromDate.value }, 'from');
|
|
121
|
+
params.from = fromDate.value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (toDate.value !== undefined && toDate.value !== null) {
|
|
125
|
+
ensureOptionalIsoDate({ to: toDate.value }, 'to');
|
|
126
|
+
params.to = toDate.value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (fromDate.conflict || toDate.conflict) {
|
|
130
|
+
logWarn('[hours.dispatch] Conflicting canonical and legacy date aliases', {
|
|
131
|
+
fromConflict: fromDate.conflict,
|
|
132
|
+
toConflict: toDate.conflict,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
logDebug('[hours.dispatch] Date aliases mapped', {
|
|
137
|
+
fromSource: fromDate.source,
|
|
138
|
+
toSource: toDate.source,
|
|
139
|
+
hadLegacyFromDate: fromDate.hadLegacy,
|
|
140
|
+
hadLegacyToDate: toDate.hadLegacy,
|
|
141
|
+
mappedKeys: Object.keys(params),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return params;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function dispatch(name, args = {}) {
|
|
148
|
+
try {
|
|
149
|
+
switch (name) {
|
|
150
|
+
case 'get_total_hours_by_weekly':
|
|
151
|
+
logToolStart(name, args);
|
|
152
|
+
return makeRequest(
|
|
153
|
+
'GET',
|
|
154
|
+
'hours/list-total-weeks',
|
|
155
|
+
buildParams(args, [...USER_AWARE_FIELDS, ...DATE_FIELDS])
|
|
156
|
+
);
|
|
157
|
+
case 'get_total_hours_by_monthly':
|
|
158
|
+
logToolStart(name, args);
|
|
159
|
+
return makeRequest(
|
|
160
|
+
'GET',
|
|
161
|
+
'hours/list-total-monthly',
|
|
162
|
+
buildParams(args, [...USER_AWARE_FIELDS, ...DATE_FIELDS])
|
|
163
|
+
);
|
|
164
|
+
case 'get_total_hours_by_weekly_per_people':
|
|
165
|
+
logToolStart(name, args);
|
|
166
|
+
return makeRequest(
|
|
167
|
+
'GET',
|
|
168
|
+
'hours/list-total-weeks-per-people',
|
|
169
|
+
buildParams(args, [...SHARED_OPTIONAL_FIELDS, ...DATE_FIELDS])
|
|
170
|
+
);
|
|
171
|
+
case 'get_total_hours_by_monthly_per_people':
|
|
172
|
+
logToolStart(name, args);
|
|
173
|
+
return makeRequest(
|
|
174
|
+
'GET',
|
|
175
|
+
'hours/list-total-monthly-per-people',
|
|
176
|
+
buildParams(args, [...SHARED_OPTIONAL_FIELDS, ...DATE_FIELDS])
|
|
177
|
+
);
|
|
178
|
+
case 'get_total_hours_by_day':
|
|
179
|
+
logToolStart(name, args);
|
|
180
|
+
return makeRequest(
|
|
181
|
+
'GET',
|
|
182
|
+
'hours/list-total',
|
|
183
|
+
buildParams(args, [...USER_AWARE_FIELDS, ...DATE_FIELDS])
|
|
184
|
+
);
|
|
185
|
+
default:
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
logError('[hours.dispatch] Tool execution failed', {
|
|
190
|
+
toolName: name,
|
|
191
|
+
message: error?.message,
|
|
192
|
+
});
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { logDebug, logError } from '../../shared/logger.js';
|
|
3
|
+
import { makeRequest } from '../../shared/request.js';
|
|
4
|
+
import { ensureOptionalIsoDate } from '../../shared/date-validation.js';
|
|
5
|
+
|
|
6
|
+
const OPTIONAL_NUMBER_FIELDS_FOR_UPDATE = [
|
|
7
|
+
'status_id',
|
|
8
|
+
'tracker_id',
|
|
9
|
+
'priority_id',
|
|
10
|
+
'assigned_to_id',
|
|
11
|
+
'parent_id',
|
|
12
|
+
'estimated_hours',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const OPTIONAL_STRING_FIELDS_FOR_UPDATE = ['subject', 'description', 'start_date', 'due_date'];
|
|
16
|
+
|
|
17
|
+
// Mirrors IssueController validation rules.
|
|
18
|
+
const STATUS_IDS = [1, 4, 27, 8, 34, 39, 2, 38, 36, 3, 29, 10, 7, 37, 5, 6, 9, 11, 15, 16, 19, 20, 21, 22, 23, 24, 25, 26, 41, 42];
|
|
19
|
+
const TRACKER_IDS = [14, 8, 1];
|
|
20
|
+
const PRIORITY_IDS = [3, 4, 5];
|
|
21
|
+
|
|
22
|
+
export const TOOLS = [
|
|
23
|
+
{
|
|
24
|
+
name: 'get_all_issue_statuses',
|
|
25
|
+
description: 'Returns a list of all defined issue statuses within the system.',
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {},
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'get_all_issue_trackers',
|
|
34
|
+
description: 'Retrieves a list of all issue trackers available in the system.',
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {},
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'get_all_issue_priorities',
|
|
43
|
+
description: 'Retrieves a list of all defined issue priorities in the system.',
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {},
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'get_my_issues',
|
|
52
|
+
description:
|
|
53
|
+
'Retrieves all issues currently assigned to the authenticated user. Optional filters: project_id, status_id, subject (partial match), description (partial match), created_on_from / created_on_to (YYYY-MM-DD), and limit (default 100). Each item includes type, project_name, status_name, priority_name, author_name, assigned_to_name, dates, spent hours and a link.',
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
project_id: { type: 'number' },
|
|
58
|
+
status_id: { type: 'number' },
|
|
59
|
+
subject: { type: 'string' },
|
|
60
|
+
description: { type: 'string' },
|
|
61
|
+
created_on_from: { type: 'string', description: 'YYYY-MM-DD' },
|
|
62
|
+
created_on_to: { type: 'string', description: 'YYYY-MM-DD' },
|
|
63
|
+
limit: { type: 'number' },
|
|
64
|
+
},
|
|
65
|
+
additionalProperties: false,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'get_issues_with_filter',
|
|
70
|
+
description:
|
|
71
|
+
'Returns all issues filtered by project_id, status_id, author_id, assigned_to_id, subject (partial match), description (partial match), created_on_from / created_on_to (YYYY-MM-DD), and limit (default 100).',
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
project_id: { type: 'number' },
|
|
76
|
+
status_id: { type: 'number' },
|
|
77
|
+
author_id: { type: 'number' },
|
|
78
|
+
assigned_to_id: { type: 'number' },
|
|
79
|
+
subject: { type: 'string' },
|
|
80
|
+
description: { type: 'string' },
|
|
81
|
+
created_on_from: { type: 'string', description: 'YYYY-MM-DD' },
|
|
82
|
+
created_on_to: { type: 'string', description: 'YYYY-MM-DD' },
|
|
83
|
+
limit: { type: 'number' },
|
|
84
|
+
},
|
|
85
|
+
additionalProperties: false,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'get_issue_by_id',
|
|
90
|
+
description: 'Retrieve full information about an issue by its ID.',
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
issue_id: { type: 'number' },
|
|
95
|
+
},
|
|
96
|
+
required: ['issue_id'],
|
|
97
|
+
additionalProperties: false,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'update_issue_by_id',
|
|
102
|
+
description: 'Update the details of an issue by its ID.',
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
issue_id: { type: 'number' },
|
|
107
|
+
subject: { type: 'string' },
|
|
108
|
+
status_id: { type: 'number', enum: STATUS_IDS, description: 'Issue status. Get from get_all_issue_statuses.' },
|
|
109
|
+
tracker_id: { type: 'number', enum: TRACKER_IDS, description: 'Issue tracker. Get from get_all_issue_trackers.' },
|
|
110
|
+
priority_id: { type: 'number', enum: PRIORITY_IDS, description: 'Issue priority. Get from get_all_issue_priorities.' },
|
|
111
|
+
description: { type: 'string' },
|
|
112
|
+
assigned_to_id: { type: 'number' },
|
|
113
|
+
parent_id: { type: 'number' },
|
|
114
|
+
start_date: { type: 'string', description: 'YYYY-MM-DD' },
|
|
115
|
+
due_date: { type: 'string', description: 'YYYY-MM-DD' },
|
|
116
|
+
estimated_hours: { type: 'number' },
|
|
117
|
+
},
|
|
118
|
+
required: ['issue_id'],
|
|
119
|
+
additionalProperties: false,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'create_issue',
|
|
124
|
+
description: 'Create a new issue in the system.',
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
properties: {
|
|
128
|
+
project_id: { type: 'number' },
|
|
129
|
+
subject: { type: 'string' },
|
|
130
|
+
status_id: { type: 'number', enum: STATUS_IDS, description: 'Issue status. Get from get_all_issue_statuses.' },
|
|
131
|
+
tracker_id: { type: 'number', enum: TRACKER_IDS, description: 'Issue tracker. Get from get_all_issue_trackers.' },
|
|
132
|
+
priority_id: { type: 'number', enum: PRIORITY_IDS, description: 'Issue priority. Get from get_all_issue_priorities.' },
|
|
133
|
+
description: { type: 'string' },
|
|
134
|
+
assigned_to_id: { type: 'number' },
|
|
135
|
+
parent_id: { type: 'number' },
|
|
136
|
+
start_date: { type: 'string', description: 'YYYY-MM-DD' },
|
|
137
|
+
due_date: { type: 'string', description: 'YYYY-MM-DD' },
|
|
138
|
+
estimated_hours: { type: 'number' },
|
|
139
|
+
},
|
|
140
|
+
required: ['project_id', 'subject'],
|
|
141
|
+
additionalProperties: false,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
function logToolStart(name, args) {
|
|
147
|
+
logDebug('[issues.dispatch] Handling tool', {
|
|
148
|
+
toolName: name,
|
|
149
|
+
argumentKeys: Object.keys(args || {}),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function ensureRequiredNumber(args, fieldName) {
|
|
154
|
+
if (typeof args[fieldName] !== 'number') {
|
|
155
|
+
throw new McpError(ErrorCode.InvalidParams, `Tool requires numeric field "${fieldName}".`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function ensureRequiredString(args, fieldName) {
|
|
160
|
+
if (typeof args[fieldName] !== 'string' || !args[fieldName].trim()) {
|
|
161
|
+
throw new McpError(
|
|
162
|
+
ErrorCode.InvalidParams,
|
|
163
|
+
`Tool requires non-empty string field "${fieldName}".`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function pickOptionalParams(args, keys) {
|
|
169
|
+
const result = {};
|
|
170
|
+
for (const key of keys) {
|
|
171
|
+
if (args[key] !== undefined && args[key] !== null) {
|
|
172
|
+
result[key] = args[key];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildIssueUpdatePayload(args) {
|
|
179
|
+
const payload = {};
|
|
180
|
+
|
|
181
|
+
for (const key of OPTIONAL_NUMBER_FIELDS_FOR_UPDATE) {
|
|
182
|
+
if (args[key] !== undefined && args[key] !== null) {
|
|
183
|
+
payload[key] = args[key];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const key of OPTIONAL_STRING_FIELDS_FOR_UPDATE) {
|
|
188
|
+
if (args[key] !== undefined && args[key] !== null) {
|
|
189
|
+
payload[key] = args[key];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return payload;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function validateDateFilters(args) {
|
|
197
|
+
ensureOptionalIsoDate(args, 'created_on_from');
|
|
198
|
+
ensureOptionalIsoDate(args, 'created_on_to');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function validateIssueDates(args) {
|
|
202
|
+
ensureOptionalIsoDate(args, 'start_date');
|
|
203
|
+
ensureOptionalIsoDate(args, 'due_date');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function dispatch(name, args = {}) {
|
|
207
|
+
try {
|
|
208
|
+
switch (name) {
|
|
209
|
+
case 'get_all_issue_statuses':
|
|
210
|
+
logToolStart(name, args);
|
|
211
|
+
return makeRequest('GET', 'issues/statuses');
|
|
212
|
+
case 'get_all_issue_trackers':
|
|
213
|
+
logToolStart(name, args);
|
|
214
|
+
return makeRequest('GET', 'issues/trackers');
|
|
215
|
+
case 'get_all_issue_priorities':
|
|
216
|
+
logToolStart(name, args);
|
|
217
|
+
return makeRequest('GET', 'issues/priorities');
|
|
218
|
+
case 'get_my_issues': {
|
|
219
|
+
logToolStart(name, args);
|
|
220
|
+
validateDateFilters(args);
|
|
221
|
+
const params = pickOptionalParams(args, [
|
|
222
|
+
'project_id',
|
|
223
|
+
'status_id',
|
|
224
|
+
'subject',
|
|
225
|
+
'description',
|
|
226
|
+
'created_on_from',
|
|
227
|
+
'created_on_to',
|
|
228
|
+
'limit',
|
|
229
|
+
]);
|
|
230
|
+
return makeRequest('GET', 'issues/my', params);
|
|
231
|
+
}
|
|
232
|
+
case 'get_issues_with_filter': {
|
|
233
|
+
logToolStart(name, args);
|
|
234
|
+
validateDateFilters(args);
|
|
235
|
+
const params = pickOptionalParams(args, [
|
|
236
|
+
'project_id',
|
|
237
|
+
'status_id',
|
|
238
|
+
'author_id',
|
|
239
|
+
'assigned_to_id',
|
|
240
|
+
'subject',
|
|
241
|
+
'description',
|
|
242
|
+
'created_on_from',
|
|
243
|
+
'created_on_to',
|
|
244
|
+
'limit',
|
|
245
|
+
]);
|
|
246
|
+
return makeRequest('GET', 'issues/filter', params);
|
|
247
|
+
}
|
|
248
|
+
case 'get_issue_by_id':
|
|
249
|
+
logToolStart(name, args);
|
|
250
|
+
ensureRequiredNumber(args, 'issue_id');
|
|
251
|
+
return makeRequest('GET', `issues/${args.issue_id}`);
|
|
252
|
+
case 'update_issue_by_id': {
|
|
253
|
+
logToolStart(name, args);
|
|
254
|
+
ensureRequiredNumber(args, 'issue_id');
|
|
255
|
+
validateIssueDates(args);
|
|
256
|
+
const payload = buildIssueUpdatePayload(args);
|
|
257
|
+
return makeRequest('PUT', `issues/${args.issue_id}`, undefined, payload);
|
|
258
|
+
}
|
|
259
|
+
case 'create_issue': {
|
|
260
|
+
logToolStart(name, args);
|
|
261
|
+
ensureRequiredNumber(args, 'project_id');
|
|
262
|
+
ensureRequiredString(args, 'subject');
|
|
263
|
+
validateIssueDates(args);
|
|
264
|
+
const payload = {
|
|
265
|
+
project_id: args.project_id,
|
|
266
|
+
subject: args.subject,
|
|
267
|
+
...buildIssueUpdatePayload(args),
|
|
268
|
+
};
|
|
269
|
+
return makeRequest('POST', 'issues', undefined, payload);
|
|
270
|
+
}
|
|
271
|
+
default:
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
} catch (error) {
|
|
275
|
+
logError('[issues.dispatch] Tool execution failed', {
|
|
276
|
+
toolName: name,
|
|
277
|
+
message: error?.message,
|
|
278
|
+
});
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { logDebug, logError } from '../../shared/logger.js';
|
|
3
|
+
import { makeRequest } from '../../shared/request.js';
|
|
4
|
+
|
|
5
|
+
export const TOOLS = [
|
|
6
|
+
{
|
|
7
|
+
name: 'get_all_projects',
|
|
8
|
+
description: 'Returns all available projects.',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {},
|
|
12
|
+
additionalProperties: false,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'get_project_by_id',
|
|
17
|
+
description: 'Return a project by its unique ID.',
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
project_id: { type: 'number' },
|
|
22
|
+
},
|
|
23
|
+
required: ['project_id'],
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function logToolStart(name, args) {
|
|
30
|
+
logDebug('[projects.dispatch] Handling tool', {
|
|
31
|
+
toolName: name,
|
|
32
|
+
argumentKeys: Object.keys(args || {}),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ensureRequiredNumber(args, fieldName) {
|
|
37
|
+
if (typeof args[fieldName] !== 'number') {
|
|
38
|
+
throw new McpError(ErrorCode.InvalidParams, `Tool requires numeric field "${fieldName}".`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function dispatch(name, args = {}) {
|
|
43
|
+
try {
|
|
44
|
+
switch (name) {
|
|
45
|
+
case 'get_all_projects':
|
|
46
|
+
logToolStart(name, args);
|
|
47
|
+
return makeRequest('GET', 'projects/all');
|
|
48
|
+
case 'get_project_by_id':
|
|
49
|
+
logToolStart(name, args);
|
|
50
|
+
ensureRequiredNumber(args, 'project_id');
|
|
51
|
+
return makeRequest('GET', `projects/${args.project_id}`);
|
|
52
|
+
default:
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
logError('[projects.dispatch] Tool execution failed', {
|
|
57
|
+
toolName: name,
|
|
58
|
+
message: error?.message,
|
|
59
|
+
});
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|