internaltool-mcp 1.0.0 → 1.3.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/index.js +436 -355
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* Usage (API key — recommended):
|
|
6
6
|
* npx internaltool-mcp --url https://your-server.com --token ilt_abc123...
|
|
7
7
|
*
|
|
8
|
+
* Usage (scoped to one project):
|
|
9
|
+
* npx internaltool-mcp --url https://your-server.com --token ilt_... --project <projectId>
|
|
10
|
+
*
|
|
8
11
|
* Usage (email/password):
|
|
9
12
|
* npx internaltool-mcp --url https://your-server.com --email you@x.com --password secret
|
|
10
13
|
*
|
|
@@ -15,10 +18,9 @@
|
|
|
15
18
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
16
19
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
17
20
|
import { z } from 'zod'
|
|
18
|
-
import { api, login, configure
|
|
21
|
+
import { api, login, configure } from './api-client.js'
|
|
19
22
|
|
|
20
23
|
// ── Parse CLI args ────────────────────────────────────────────────────────────
|
|
21
|
-
// Supports: --url, --token, --email, --password
|
|
22
24
|
const args = process.argv.slice(2)
|
|
23
25
|
const getArg = (flag) => {
|
|
24
26
|
const i = args.indexOf(flag)
|
|
@@ -29,19 +31,14 @@ const cliUrl = getArg('--url')
|
|
|
29
31
|
const cliToken = getArg('--token')
|
|
30
32
|
const cliEmail = getArg('--email')
|
|
31
33
|
const cliPassword = getArg('--password')
|
|
34
|
+
const cliProject = getArg('--project') // optional: scope to one project
|
|
32
35
|
|
|
33
|
-
// CLI args take precedence over env vars
|
|
34
36
|
if (cliEmail) process.env.INTERNALTOOL_EMAIL = cliEmail
|
|
35
37
|
if (cliPassword) process.env.INTERNALTOOL_PASSWORD = cliPassword
|
|
36
38
|
|
|
37
39
|
configure({ url: cliUrl, token: cliToken })
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
name: 'internaltool',
|
|
41
|
-
version: '1.0.0',
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
41
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
45
42
|
|
|
46
43
|
function text(value) {
|
|
47
44
|
const str = typeof value === 'string' ? value : JSON.stringify(value, null, 2)
|
|
@@ -64,353 +61,437 @@ async function call(fn) {
|
|
|
64
61
|
}
|
|
65
62
|
}
|
|
66
63
|
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
64
|
+
// Guard: reject tool calls that target a project outside the scoped projectId
|
|
65
|
+
function assertProjectScope(projectId) {
|
|
66
|
+
if (cliProject && projectId !== cliProject) {
|
|
67
|
+
throw new Error(`Access denied: this session is scoped to project ${cliProject}`)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Runtime permission guard ───────────────────────────────────────────────────
|
|
72
|
+
// Defense-in-depth: even if the startup role check is bypassed, these guards
|
|
73
|
+
// re-verify the user's role before executing privileged operations.
|
|
74
|
+
|
|
75
|
+
async function assertAdmin() {
|
|
76
|
+
const res = await api.get('/api/auth/me')
|
|
77
|
+
if (!res?.success) throw new Error('Unable to verify identity')
|
|
78
|
+
if (res.data.user.role !== 'admin') {
|
|
79
|
+
throw new Error('Access denied: admin role required')
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Tool registration functions ───────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
function registerAuthTools(server) {
|
|
86
|
+
server.tool(
|
|
87
|
+
'login',
|
|
88
|
+
'Authenticate with the InternalTool API. Not needed if INTERNALTOOL_TOKEN env var is set.',
|
|
89
|
+
{
|
|
90
|
+
email: z.string().email().describe('User email address'),
|
|
91
|
+
password: z.string().describe('User password'),
|
|
92
|
+
},
|
|
93
|
+
async ({ email, password }) => {
|
|
94
|
+
try {
|
|
95
|
+
const data = await login(email, password)
|
|
96
|
+
return text({ message: 'Login successful', user: data.user })
|
|
97
|
+
} catch (e) {
|
|
98
|
+
return errorText(e.message)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
server.tool(
|
|
104
|
+
'get_current_user',
|
|
105
|
+
'Get your profile — name, email, and role.',
|
|
106
|
+
{},
|
|
107
|
+
async () => call(() => api.get('/api/auth/me'))
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function registerUserTools(server) {
|
|
112
|
+
server.tool(
|
|
113
|
+
'list_users',
|
|
114
|
+
'List users in the directory. Use this to find user IDs for task assignment.',
|
|
115
|
+
{ search: z.string().optional().describe('Filter by name or email') },
|
|
116
|
+
async ({ search }) => {
|
|
117
|
+
const qs = search ? `?search=${encodeURIComponent(search)}` : ''
|
|
118
|
+
return call(() => api.get(`/api/users/directory${qs}`))
|
|
82
119
|
}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
server.tool(
|
|
123
|
+
'get_my_tasks',
|
|
124
|
+
'Get all tasks assigned to you. Optionally filter by column. Use this to find your active work before a hotfix or priority switch.',
|
|
125
|
+
{
|
|
126
|
+
column: z.enum(['backlog', 'todo', 'in_progress', 'in_review', 'done'])
|
|
127
|
+
.optional()
|
|
128
|
+
.describe('Filter to a specific column — e.g. "in_progress" to find your active task'),
|
|
129
|
+
},
|
|
130
|
+
async ({ column } = {}) => {
|
|
131
|
+
const qs = column ? `?column=${encodeURIComponent(column)}` : ''
|
|
132
|
+
return call(() => api.get(`/api/users/me/tasks${qs}`))
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function registerProjectTools(server, { isAdmin, scopedProjectId }) {
|
|
138
|
+
// Admins see all projects. Members see only their assigned ones (API enforces this).
|
|
139
|
+
// If --project flag is set, list_projects is locked to that one project.
|
|
140
|
+
server.tool(
|
|
141
|
+
'list_projects',
|
|
142
|
+
scopedProjectId
|
|
143
|
+
? `Show details of your assigned project (scoped to project ${scopedProjectId}).`
|
|
144
|
+
: 'List all projects you have access to.',
|
|
145
|
+
{},
|
|
146
|
+
async () => {
|
|
147
|
+
if (scopedProjectId) {
|
|
148
|
+
return call(() => api.get(`/api/projects/${scopedProjectId}`))
|
|
149
|
+
}
|
|
150
|
+
return call(() => api.get('/api/projects'))
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
server.tool(
|
|
155
|
+
'get_project',
|
|
156
|
+
'Get full project details including all tasks on the board.',
|
|
157
|
+
{ projectId: z.string().describe("Project's MongoDB ObjectId") },
|
|
158
|
+
async ({ projectId }) => {
|
|
159
|
+
try { assertProjectScope(projectId) } catch (e) { return errorText(e.message) }
|
|
160
|
+
return call(() => api.get(`/api/projects/${projectId}`))
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
// Project creation/management — admin or project owner only
|
|
165
|
+
if (isAdmin) {
|
|
166
|
+
server.tool(
|
|
167
|
+
'create_project',
|
|
168
|
+
'Create a new project.',
|
|
169
|
+
{
|
|
170
|
+
name: z.string().describe('Project name'),
|
|
171
|
+
memberIds: z.array(z.string()).optional().describe('Initial member user IDs'),
|
|
172
|
+
githubRepoFullName: z.string().optional().describe("GitHub repo in 'owner/repo' format"),
|
|
173
|
+
},
|
|
174
|
+
async (args) => {
|
|
175
|
+
try { await assertAdmin() } catch (e) { return errorText(e.message) }
|
|
176
|
+
return call(() => api.post('/api/projects', args))
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
server.tool(
|
|
181
|
+
'update_project',
|
|
182
|
+
"Update a project's GitHub integration settings.",
|
|
183
|
+
{
|
|
184
|
+
projectId: z.string().describe("Project's MongoDB ObjectId"),
|
|
185
|
+
githubRepoFullName: z.string().optional().describe("GitHub repo 'owner/repo'. Pass '' to unlink."),
|
|
186
|
+
githubDefaultBranch: z.string().optional().describe('Default branch (default: main)'),
|
|
187
|
+
},
|
|
188
|
+
async ({ projectId, ...fields }) => {
|
|
189
|
+
try { assertProjectScope(projectId); await assertAdmin() } catch (e) { return errorText(e.message) }
|
|
190
|
+
return call(() => api.patch(`/api/projects/${projectId}`, fields))
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
server.tool(
|
|
195
|
+
'update_project_members',
|
|
196
|
+
'Replace the project member list. Owner is always preserved.',
|
|
197
|
+
{
|
|
198
|
+
projectId: z.string().describe("Project's MongoDB ObjectId"),
|
|
199
|
+
memberIds: z.array(z.string()).describe('Full replacement list of user IDs'),
|
|
200
|
+
},
|
|
201
|
+
async ({ projectId, memberIds }) => {
|
|
202
|
+
try { assertProjectScope(projectId); await assertAdmin() } catch (e) { return errorText(e.message) }
|
|
203
|
+
return call(() => api.patch(`/api/projects/${projectId}/members`, { memberIds }))
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
server.tool(
|
|
208
|
+
'delete_project',
|
|
209
|
+
'Permanently delete a project and all its tasks. Cannot be undone.',
|
|
210
|
+
{ projectId: z.string().describe("Project's MongoDB ObjectId") },
|
|
211
|
+
async ({ projectId }) => {
|
|
212
|
+
try { assertProjectScope(projectId); await assertAdmin() } catch (e) { return errorText(e.message) }
|
|
213
|
+
return call(() => api.delete(`/api/projects/${projectId}`))
|
|
214
|
+
}
|
|
215
|
+
)
|
|
83
216
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
server
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function registerTaskTools(server, { isAdmin, scopedProjectId }) {
|
|
220
|
+
server.tool(
|
|
221
|
+
'get_task',
|
|
222
|
+
'Get full details of a task including assignees, column, approval status, and park note.',
|
|
223
|
+
{ taskId: z.string().describe("Task's MongoDB ObjectId") },
|
|
224
|
+
async ({ taskId }) => call(() => api.get(`/api/tasks/${taskId}`))
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
server.tool(
|
|
228
|
+
'create_task',
|
|
229
|
+
'Create a new task in a project. Project owner or admin only.',
|
|
230
|
+
{
|
|
231
|
+
projectId: z.string().describe("Project's MongoDB ObjectId"),
|
|
232
|
+
title: z.string().describe('Task title'),
|
|
233
|
+
description: z.string().optional().describe('Short description'),
|
|
234
|
+
readmeMarkdown: z.string().optional().describe('Markdown implementation plan'),
|
|
235
|
+
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
|
|
236
|
+
column: z.enum(['backlog', 'todo', 'in_progress', 'in_review', 'done']).optional(),
|
|
237
|
+
assignees: z.array(z.string()).optional().describe('User IDs to assign'),
|
|
238
|
+
},
|
|
239
|
+
async ({ projectId, ...taskData }) => {
|
|
240
|
+
try { assertProjectScope(projectId) } catch (e) { return errorText(e.message) }
|
|
241
|
+
return call(() => api.post(`/api/projects/${projectId}/tasks`, taskData))
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
server.tool(
|
|
246
|
+
'update_task',
|
|
247
|
+
'Update task fields. Cannot edit readmeMarkdown or subtasks while approval is pending.',
|
|
248
|
+
{
|
|
249
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
250
|
+
title: z.string().optional(),
|
|
251
|
+
description: z.string().optional(),
|
|
252
|
+
readmeMarkdown: z.string().optional().describe('Markdown implementation plan'),
|
|
253
|
+
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
|
|
254
|
+
assignees: z.array(z.string()).optional().describe('Replaces current assignees'),
|
|
255
|
+
subtasks: z.array(z.object({
|
|
256
|
+
_id: z.string().optional(),
|
|
257
|
+
title: z.string(),
|
|
258
|
+
done: z.boolean().optional(),
|
|
259
|
+
order: z.number().optional(),
|
|
260
|
+
})).optional(),
|
|
261
|
+
},
|
|
262
|
+
async ({ taskId, ...fields }) => call(() => api.patch(`/api/tasks/${taskId}`, fields))
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
server.tool(
|
|
266
|
+
'move_task',
|
|
267
|
+
'Move a task to a different column. Requires approved README to move from planning to execution columns.',
|
|
268
|
+
{
|
|
269
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
270
|
+
column: z.enum(['backlog', 'todo', 'in_progress', 'in_review', 'done']),
|
|
271
|
+
toIndex: z.number().int().min(0).default(0).describe('Position in column (0 = top)'),
|
|
272
|
+
},
|
|
273
|
+
async ({ taskId, column, toIndex }) =>
|
|
274
|
+
call(() => api.post(`/api/tasks/${taskId}/move`, { column, toIndex }))
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
server.tool(
|
|
278
|
+
'delete_task',
|
|
279
|
+
'Permanently delete a task. Project team member required.',
|
|
280
|
+
{ taskId: z.string().describe("Task's MongoDB ObjectId") },
|
|
281
|
+
async ({ taskId }) => call(() => api.delete(`/api/tasks/${taskId}`))
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
server.tool(
|
|
285
|
+
'park_task',
|
|
286
|
+
'Pause a task and save your current work context.',
|
|
287
|
+
{
|
|
288
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
289
|
+
summary: z.string().optional().describe('Work done so far'),
|
|
290
|
+
remaining: z.string().optional().describe('What still needs to be done'),
|
|
291
|
+
blockers: z.string().optional().describe('What is blocking progress'),
|
|
292
|
+
},
|
|
293
|
+
async ({ taskId, summary = '', remaining = '', blockers = '' }) =>
|
|
294
|
+
call(() => api.patch(`/api/tasks/${taskId}/park`, { summary, remaining, blockers }))
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
server.tool(
|
|
298
|
+
'unpark_task',
|
|
299
|
+
'Resume a parked task.',
|
|
300
|
+
{ taskId: z.string().describe("Task's MongoDB ObjectId") },
|
|
301
|
+
async ({ taskId }) => call(() => api.patch(`/api/tasks/${taskId}/unpark`, {}))
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function registerIssueTools(server) {
|
|
306
|
+
server.tool(
|
|
307
|
+
'create_task_issue',
|
|
308
|
+
'Create a sub-issue/bug under a task.',
|
|
309
|
+
{
|
|
310
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
311
|
+
title: z.string().describe('Issue title'),
|
|
312
|
+
severity: z.enum(['low', 'medium', 'high', 'critical']).optional(),
|
|
313
|
+
},
|
|
314
|
+
async ({ taskId, title, severity }) =>
|
|
315
|
+
call(() => api.post(`/api/tasks/${taskId}/issues`, { title, severity }))
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
server.tool(
|
|
319
|
+
'update_task_issue',
|
|
320
|
+
'Update a sub-issue status, severity, or title.',
|
|
321
|
+
{
|
|
322
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
323
|
+
issueId: z.string().describe("Issue's sub-document ID"),
|
|
324
|
+
title: z.string().optional(),
|
|
325
|
+
severity: z.enum(['low', 'medium', 'high', 'critical']).optional(),
|
|
326
|
+
status: z.enum(['open', 'closed']).optional(),
|
|
327
|
+
},
|
|
328
|
+
async ({ taskId, issueId, ...fields }) =>
|
|
329
|
+
call(() => api.patch(`/api/tasks/${taskId}/issues/${issueId}`, fields))
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function registerApprovalTools(server) {
|
|
334
|
+
server.tool(
|
|
335
|
+
'submit_task_for_approval',
|
|
336
|
+
'Submit a task implementation plan for review. README must be at least 80 characters.',
|
|
337
|
+
{
|
|
338
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
339
|
+
reviewerId: z.string().describe('User ID of the reviewer'),
|
|
340
|
+
},
|
|
341
|
+
async ({ taskId, reviewerId }) =>
|
|
342
|
+
call(() => api.post(`/api/tasks/${taskId}/approval/submit`, { reviewerId }))
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
server.tool(
|
|
346
|
+
'decide_task_approval',
|
|
347
|
+
'Approve or reject a task plan. Only the designated reviewer can call this.',
|
|
348
|
+
{
|
|
349
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
350
|
+
decision: z.enum(['approve', 'reject']),
|
|
351
|
+
note: z.string().optional().describe('Reason for the decision'),
|
|
352
|
+
},
|
|
353
|
+
async ({ taskId, decision, note }) =>
|
|
354
|
+
call(() => api.post(`/api/tasks/${taskId}/approval/decide`, { decision, note }))
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function registerCommentTools(server) {
|
|
359
|
+
server.tool(
|
|
360
|
+
'list_task_comments',
|
|
361
|
+
'List all comments on a task in chronological order.',
|
|
362
|
+
{ taskId: z.string().describe("Task's MongoDB ObjectId") },
|
|
363
|
+
async ({ taskId }) => call(() => api.get(`/api/tasks/${taskId}/comments`))
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
server.tool(
|
|
367
|
+
'add_task_comment',
|
|
368
|
+
'Post a markdown comment on a task. Use @email to mention users.',
|
|
369
|
+
{
|
|
370
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
371
|
+
body: z.string().describe('Comment in markdown'),
|
|
372
|
+
},
|
|
373
|
+
async ({ taskId, body }) =>
|
|
374
|
+
call(() => api.post(`/api/tasks/${taskId}/comments`, { body }))
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
server.tool(
|
|
378
|
+
'get_task_activity',
|
|
379
|
+
'Get the activity/audit log for a task (last 100 events).',
|
|
380
|
+
{ taskId: z.string().describe("Task's MongoDB ObjectId") },
|
|
381
|
+
async ({ taskId }) => call(() => api.get(`/api/tasks/${taskId}/activity`))
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function registerNotificationTools(server) {
|
|
386
|
+
server.tool(
|
|
387
|
+
'list_notifications',
|
|
388
|
+
'List your recent notifications and unread count.',
|
|
389
|
+
{},
|
|
390
|
+
async () => call(() => api.get('/api/notifications'))
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
server.tool(
|
|
394
|
+
'mark_all_notifications_read',
|
|
395
|
+
'Mark all notifications as read.',
|
|
396
|
+
{},
|
|
397
|
+
async () => call(() => api.patch('/api/notifications/read-all', {}))
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
server.tool(
|
|
401
|
+
'delete_all_notifications',
|
|
402
|
+
'Clear all read notifications.',
|
|
403
|
+
{},
|
|
404
|
+
async () => call(() => api.delete('/api/notifications/clear'))
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function registerGithubTools(server, { scopedProjectId }) {
|
|
409
|
+
server.tool(
|
|
410
|
+
'get_project_commits',
|
|
411
|
+
'Get recent GitHub commits for a project.',
|
|
412
|
+
{
|
|
413
|
+
projectId: z.string().describe("Project's MongoDB ObjectId"),
|
|
414
|
+
branch: z.string().optional(),
|
|
415
|
+
perPage: z.number().int().min(1).max(100).optional(),
|
|
416
|
+
},
|
|
417
|
+
async ({ projectId, branch, perPage }) => {
|
|
418
|
+
try { assertProjectScope(projectId) } catch (e) { return errorText(e.message) }
|
|
419
|
+
const params = new URLSearchParams()
|
|
420
|
+
if (branch) params.set('sha', branch)
|
|
421
|
+
if (perPage) params.set('per_page', String(perPage))
|
|
422
|
+
const qs = params.toString() ? `?${params}` : ''
|
|
423
|
+
return call(() => api.get(`/api/projects/${projectId}/github/commits${qs}`))
|
|
424
|
+
}
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
server.tool(
|
|
428
|
+
'get_project_branches',
|
|
429
|
+
'Get GitHub branches for a project.',
|
|
430
|
+
{ projectId: z.string().describe("Project's MongoDB ObjectId") },
|
|
431
|
+
async ({ projectId }) => {
|
|
432
|
+
try { assertProjectScope(projectId) } catch (e) { return errorText(e.message) }
|
|
433
|
+
return call(() => api.get(`/api/projects/${projectId}/github/branches`))
|
|
434
|
+
}
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function registerAdminTools(server) {
|
|
439
|
+
server.tool(
|
|
440
|
+
'admin_list_users',
|
|
441
|
+
'List all users on the platform. Admin only.',
|
|
442
|
+
{},
|
|
443
|
+
async () => {
|
|
444
|
+
try { await assertAdmin() } catch (e) { return errorText(e.message) }
|
|
445
|
+
return call(() => api.get('/api/admin/users'))
|
|
446
|
+
}
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
server.tool(
|
|
450
|
+
'admin_get_analytics',
|
|
451
|
+
'Get platform-wide analytics. Admin only.',
|
|
452
|
+
{},
|
|
453
|
+
async () => {
|
|
454
|
+
try { await assertAdmin() } catch (e) { return errorText(e.message) }
|
|
455
|
+
return call(() => api.get('/api/admin/analytics'))
|
|
456
|
+
}
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
461
|
+
|
|
462
|
+
async function main() {
|
|
463
|
+
const server = new McpServer({ name: 'internaltool', version: '1.0.0' })
|
|
464
|
+
|
|
465
|
+
// Fetch the current user's role to determine which tools to expose
|
|
466
|
+
let role = 'member'
|
|
467
|
+
try {
|
|
468
|
+
const res = await api.get('/api/auth/me')
|
|
469
|
+
if (res?.success) role = res.data.user.role
|
|
470
|
+
} catch {
|
|
471
|
+
// If we can't reach the server yet, default to member (most restrictive)
|
|
104
472
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
server
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
server.tool(
|
|
126
|
-
'create_project',
|
|
127
|
-
'Create a new project. Only admins and members can create projects.',
|
|
128
|
-
{
|
|
129
|
-
name: z.string().describe('Project name (required)'),
|
|
130
|
-
memberIds: z.array(z.string()).optional().describe('Array of user IDs to add as initial members'),
|
|
131
|
-
githubRepoFullName: z.string().optional().describe("GitHub repo in 'owner/repo' format, e.g. 'acme/backend'"),
|
|
132
|
-
},
|
|
133
|
-
async (args) => call(() => api.post('/api/projects', args))
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
server.tool(
|
|
137
|
-
'update_project',
|
|
138
|
-
'Update a project\'s GitHub integration settings (owner or admin only).',
|
|
139
|
-
{
|
|
140
|
-
projectId: z.string().describe("Project's MongoDB ObjectId"),
|
|
141
|
-
githubRepoFullName: z.string().optional().describe("GitHub repo in 'owner/repo' format. Pass empty string to unlink."),
|
|
142
|
-
githubDefaultBranch: z.string().optional().describe("Default branch for merge-to-done automation (default: 'main')"),
|
|
143
|
-
},
|
|
144
|
-
async ({ projectId, ...fields }) => call(() => api.patch(`/api/projects/${projectId}`, fields))
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
server.tool(
|
|
148
|
-
'update_project_members',
|
|
149
|
-
'Replace the project member list. Owner is always preserved. Admin or project owner only.',
|
|
150
|
-
{
|
|
151
|
-
projectId: z.string().describe("Project's MongoDB ObjectId"),
|
|
152
|
-
memberIds: z.array(z.string()).describe('Array of user IDs that should be members (replaces current list)'),
|
|
153
|
-
},
|
|
154
|
-
async ({ projectId, memberIds }) =>
|
|
155
|
-
call(() => api.patch(`/api/projects/${projectId}/members`, { memberIds }))
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
server.tool(
|
|
159
|
-
'delete_project',
|
|
160
|
-
'Permanently delete a project and all its tasks, comments, and activity. Owner or admin only. Cannot be undone.',
|
|
161
|
-
{
|
|
162
|
-
projectId: z.string().describe("Project's MongoDB ObjectId"),
|
|
163
|
-
},
|
|
164
|
-
async ({ projectId }) => call(() => api.delete(`/api/projects/${projectId}`))
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
// ─── Tasks ───────────────────────────────────────────────────────────────────
|
|
168
|
-
|
|
169
|
-
server.tool(
|
|
170
|
-
'get_task',
|
|
171
|
-
'Get full details of a task including title, description, column, priority, assignees, subtasks, issues, approval status, park note, and GitHub merge info.',
|
|
172
|
-
{
|
|
173
|
-
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
174
|
-
},
|
|
175
|
-
async ({ taskId }) => call(() => api.get(`/api/tasks/${taskId}`))
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
server.tool(
|
|
179
|
-
'create_task',
|
|
180
|
-
'Create a new task in a project. Only the project owner or admin can create tasks.',
|
|
181
|
-
{
|
|
182
|
-
projectId: z.string().describe("Project's MongoDB ObjectId"),
|
|
183
|
-
title: z.string().describe('Task title (required)'),
|
|
184
|
-
description: z.string().optional().describe('Short task description'),
|
|
185
|
-
readmeMarkdown: z.string().optional().describe('Full markdown implementation plan for the task'),
|
|
186
|
-
priority: z.enum(['low', 'medium', 'high', 'critical']).optional().describe('Priority level (default: medium)'),
|
|
187
|
-
column: z.enum(['backlog', 'todo', 'in_progress', 'in_review', 'done']).optional().describe('Initial column (default: backlog)'),
|
|
188
|
-
assignees: z.array(z.string()).optional().describe('Array of user IDs to assign'),
|
|
189
|
-
},
|
|
190
|
-
async ({ projectId, ...taskData }) =>
|
|
191
|
-
call(() => api.post(`/api/projects/${projectId}/tasks`, taskData))
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
server.tool(
|
|
195
|
-
'update_task',
|
|
196
|
-
'Update a task\'s fields. Assignee or project team member required. Cannot edit readmeMarkdown or subtasks while approval is pending.',
|
|
197
|
-
{
|
|
198
|
-
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
199
|
-
title: z.string().optional().describe('New title'),
|
|
200
|
-
description: z.string().optional().describe('New short description'),
|
|
201
|
-
readmeMarkdown: z.string().optional().describe('Full markdown implementation plan (blocked during pending approval)'),
|
|
202
|
-
priority: z.enum(['low', 'medium', 'high', 'critical']).optional().describe('New priority level'),
|
|
203
|
-
assignees: z.array(z.string()).optional().describe('Array of user IDs — replaces current assignees (project members only)'),
|
|
204
|
-
subtasks: z.array(z.object({
|
|
205
|
-
_id: z.string().optional().describe('Existing subtask ID, or omit to create new'),
|
|
206
|
-
title: z.string(),
|
|
207
|
-
done: z.boolean().optional(),
|
|
208
|
-
order: z.number().optional(),
|
|
209
|
-
})).optional().describe('Full replacement list of subtasks'),
|
|
210
|
-
},
|
|
211
|
-
async ({ taskId, ...fields }) => call(() => api.patch(`/api/tasks/${taskId}`, fields))
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
server.tool(
|
|
215
|
-
'move_task',
|
|
216
|
-
'Move a task to a different column on the kanban board. Moving from planning (backlog/todo) to execution columns requires an approved README plan. GitHub-linked projects block manual moves to "done".',
|
|
217
|
-
{
|
|
218
|
-
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
219
|
-
column: z.enum(['backlog', 'todo', 'in_progress', 'in_review', 'done']).describe('Target column'),
|
|
220
|
-
toIndex: z.number().int().min(0).default(0).describe('Position in the column (0 = top)'),
|
|
221
|
-
},
|
|
222
|
-
async ({ taskId, column, toIndex }) =>
|
|
223
|
-
call(() => api.post(`/api/tasks/${taskId}/move`, { column, toIndex }))
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
server.tool(
|
|
227
|
-
'delete_task',
|
|
228
|
-
'Permanently delete a task and its comments, activity, and notifications. Project team member required. Cannot be undone.',
|
|
229
|
-
{
|
|
230
|
-
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
231
|
-
},
|
|
232
|
-
async ({ taskId }) => call(() => api.delete(`/api/tasks/${taskId}`))
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
// ─── Park / Unpark ───────────────────────────────────────────────────────────
|
|
236
|
-
|
|
237
|
-
server.tool(
|
|
238
|
-
'park_task',
|
|
239
|
-
'Park a task to temporarily pause it, capturing the current work context.',
|
|
240
|
-
{
|
|
241
|
-
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
242
|
-
summary: z.string().optional().describe('Summary of work done so far (max 1000 chars)'),
|
|
243
|
-
remaining: z.string().optional().describe('What still needs to be done (max 1000 chars)'),
|
|
244
|
-
blockers: z.string().optional().describe('What is blocking progress (max 500 chars)'),
|
|
245
|
-
},
|
|
246
|
-
async ({ taskId, summary = '', remaining = '', blockers = '' }) =>
|
|
247
|
-
call(() => api.patch(`/api/tasks/${taskId}/park`, { summary, remaining, blockers }))
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
server.tool(
|
|
251
|
-
'unpark_task',
|
|
252
|
-
'Unpark a previously parked task to resume work on it.',
|
|
253
|
-
{
|
|
254
|
-
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
255
|
-
},
|
|
256
|
-
async ({ taskId }) => call(() => api.patch(`/api/tasks/${taskId}/unpark`, {}))
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
// ─── Issues ──────────────────────────────────────────────────────────────────
|
|
260
|
-
|
|
261
|
-
server.tool(
|
|
262
|
-
'create_task_issue',
|
|
263
|
-
'Create a sub-issue/bug report under a task.',
|
|
264
|
-
{
|
|
265
|
-
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
266
|
-
title: z.string().describe('Issue title'),
|
|
267
|
-
severity: z.enum(['low', 'medium', 'high', 'critical']).optional().describe('Issue severity (default: medium)'),
|
|
268
|
-
},
|
|
269
|
-
async ({ taskId, title, severity }) =>
|
|
270
|
-
call(() => api.post(`/api/tasks/${taskId}/issues`, { title, severity }))
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
server.tool(
|
|
274
|
-
'update_task_issue',
|
|
275
|
-
'Update a sub-issue under a task (title, severity, or status).',
|
|
276
|
-
{
|
|
277
|
-
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
278
|
-
issueId: z.string().describe("Issue's sub-document ID"),
|
|
279
|
-
title: z.string().optional().describe('New issue title'),
|
|
280
|
-
severity: z.enum(['low', 'medium', 'high', 'critical']).optional().describe('New severity'),
|
|
281
|
-
status: z.enum(['open', 'closed']).optional().describe('New status'),
|
|
282
|
-
},
|
|
283
|
-
async ({ taskId, issueId, ...fields }) =>
|
|
284
|
-
call(() => api.patch(`/api/tasks/${taskId}/issues/${issueId}`, fields))
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
// ─── Approval ────────────────────────────────────────────────────────────────
|
|
288
|
-
|
|
289
|
-
server.tool(
|
|
290
|
-
'submit_task_for_approval',
|
|
291
|
-
'Submit a task\'s implementation plan (readmeMarkdown) for review. The README must be at least 50 characters.',
|
|
292
|
-
{
|
|
293
|
-
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
294
|
-
reviewerId: z.string().describe("User ID of the designated reviewer"),
|
|
295
|
-
},
|
|
296
|
-
async ({ taskId, reviewerId }) =>
|
|
297
|
-
call(() => api.post(`/api/tasks/${taskId}/approval/submit`, { reviewerId }))
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
server.tool(
|
|
301
|
-
'decide_task_approval',
|
|
302
|
-
'Approve or reject a task\'s implementation plan. Only the designated reviewer can call this.',
|
|
303
|
-
{
|
|
304
|
-
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
305
|
-
decision: z.enum(['approve', 'reject']).describe('Approval decision'),
|
|
306
|
-
note: z.string().optional().describe('Optional note explaining the decision'),
|
|
307
|
-
},
|
|
308
|
-
async ({ taskId, decision, note }) =>
|
|
309
|
-
call(() => api.post(`/api/tasks/${taskId}/approval/decide`, { decision, note }))
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
// ─── Comments ────────────────────────────────────────────────────────────────
|
|
313
|
-
|
|
314
|
-
server.tool(
|
|
315
|
-
'list_task_comments',
|
|
316
|
-
'List all comments on a task in chronological order.',
|
|
317
|
-
{
|
|
318
|
-
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
319
|
-
},
|
|
320
|
-
async ({ taskId }) => call(() => api.get(`/api/tasks/${taskId}/comments`))
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
server.tool(
|
|
324
|
-
'add_task_comment',
|
|
325
|
-
'Add a markdown comment to a task. Mention users with @their@email.com to notify them.',
|
|
326
|
-
{
|
|
327
|
-
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
328
|
-
body: z.string().describe('Comment body in markdown. Use @email to mention users.'),
|
|
329
|
-
},
|
|
330
|
-
async ({ taskId, body }) =>
|
|
331
|
-
call(() => api.post(`/api/tasks/${taskId}/comments`, { body }))
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
// ─── Activity ────────────────────────────────────────────────────────────────
|
|
335
|
-
|
|
336
|
-
server.tool(
|
|
337
|
-
'get_task_activity',
|
|
338
|
-
'Get the full activity/audit log for a task (last 100 events, newest first).',
|
|
339
|
-
{
|
|
340
|
-
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
341
|
-
},
|
|
342
|
-
async ({ taskId }) => call(() => api.get(`/api/tasks/${taskId}/activity`))
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
// ─── Notifications ───────────────────────────────────────────────────────────
|
|
346
|
-
|
|
347
|
-
server.tool(
|
|
348
|
-
'list_notifications',
|
|
349
|
-
'List recent notifications for the current user including unread count.',
|
|
350
|
-
{},
|
|
351
|
-
async () => call(() => api.get('/api/notifications'))
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
server.tool(
|
|
355
|
-
'mark_all_notifications_read',
|
|
356
|
-
'Mark all notifications as read.',
|
|
357
|
-
{},
|
|
358
|
-
async () => call(() => api.patch('/api/notifications/read-all', {}))
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
server.tool(
|
|
362
|
-
'delete_all_notifications',
|
|
363
|
-
'Clear/delete all notifications for the current user.',
|
|
364
|
-
{},
|
|
365
|
-
async () => call(() => api.delete('/api/notifications/clear'))
|
|
366
|
-
)
|
|
367
|
-
|
|
368
|
-
// ─── GitHub ──────────────────────────────────────────────────────────────────
|
|
369
|
-
|
|
370
|
-
server.tool(
|
|
371
|
-
'get_project_commits',
|
|
372
|
-
'Get recent GitHub commits for a project. Requires the project to have a GitHub repo linked.',
|
|
373
|
-
{
|
|
374
|
-
projectId: z.string().describe("Project's MongoDB ObjectId"),
|
|
375
|
-
branch: z.string().optional().describe('Branch name (defaults to project default branch)'),
|
|
376
|
-
perPage: z.number().int().min(1).max(100).optional().describe('Number of commits to return (default: 20)'),
|
|
377
|
-
},
|
|
378
|
-
async ({ projectId, branch, perPage }) => {
|
|
379
|
-
const params = new URLSearchParams()
|
|
380
|
-
if (branch) params.set('sha', branch)
|
|
381
|
-
if (perPage) params.set('per_page', String(perPage))
|
|
382
|
-
const qs = params.toString() ? `?${params}` : ''
|
|
383
|
-
return call(() => api.get(`/api/projects/${projectId}/github/commits${qs}`))
|
|
473
|
+
|
|
474
|
+
const isAdmin = role === 'admin'
|
|
475
|
+
const scopedProjectId = cliProject || null
|
|
476
|
+
const ctx = { isAdmin, scopedProjectId }
|
|
477
|
+
|
|
478
|
+
// Register tools — admins get everything, members get their work scope only
|
|
479
|
+
registerAuthTools(server)
|
|
480
|
+
registerUserTools(server)
|
|
481
|
+
registerProjectTools(server, ctx)
|
|
482
|
+
registerTaskTools(server, ctx)
|
|
483
|
+
registerIssueTools(server)
|
|
484
|
+
registerApprovalTools(server)
|
|
485
|
+
registerCommentTools(server)
|
|
486
|
+
registerNotificationTools(server)
|
|
487
|
+
registerGithubTools(server, ctx)
|
|
488
|
+
|
|
489
|
+
if (isAdmin) {
|
|
490
|
+
registerAdminTools(server)
|
|
384
491
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
server.
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
projectId: z.string().describe("Project's MongoDB ObjectId"),
|
|
392
|
-
},
|
|
393
|
-
async ({ projectId }) =>
|
|
394
|
-
call(() => api.get(`/api/projects/${projectId}/github/branches`))
|
|
395
|
-
)
|
|
396
|
-
|
|
397
|
-
// ─── Admin ───────────────────────────────────────────────────────────────────
|
|
398
|
-
|
|
399
|
-
server.tool(
|
|
400
|
-
'admin_list_users',
|
|
401
|
-
'List all users in the system. Admin role required.',
|
|
402
|
-
{},
|
|
403
|
-
async () => call(() => api.get('/api/admin/users'))
|
|
404
|
-
)
|
|
405
|
-
|
|
406
|
-
server.tool(
|
|
407
|
-
'admin_get_analytics',
|
|
408
|
-
'Get platform-wide analytics. Admin role required.',
|
|
409
|
-
{},
|
|
410
|
-
async () => call(() => api.get('/api/admin/analytics'))
|
|
411
|
-
)
|
|
412
|
-
|
|
413
|
-
// ─── Start ───────────────────────────────────────────────────────────────────
|
|
414
|
-
|
|
415
|
-
const transport = new StdioServerTransport()
|
|
416
|
-
await server.connect(transport)
|
|
492
|
+
|
|
493
|
+
const transport = new StdioServerTransport()
|
|
494
|
+
await server.connect(transport)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
main()
|
package/package.json
CHANGED