internaltool-mcp 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.
Files changed (4) hide show
  1. package/README.md +103 -0
  2. package/api-client.js +110 -0
  3. package/index.js +416 -0
  4. package/package.json +37 -0
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # internaltool-mcp
2
+
3
+ MCP server for [InternalTool](https://github.com/your-org/internaltool) — connect AI assistants like **Claude Code** and **Cursor** directly to your project and task management platform.
4
+
5
+ ## Quick start
6
+
7
+ ### 1. Generate an API key
8
+
9
+ In InternalTool, generate a personal API key:
10
+
11
+ ```bash
12
+ curl -X POST https://your-server.com/api/auth/api-keys \
13
+ -H "Authorization: Bearer <your-jwt>" \
14
+ -H "Content-Type: application/json" \
15
+ -d '{"label": "Claude Code - MacBook"}'
16
+ ```
17
+
18
+ Copy the `token` from the response — it starts with `ilt_` and is shown **once only**.
19
+
20
+ ### 2. Add to Claude Code
21
+
22
+ ```bash
23
+ claude mcp add internaltool \
24
+ -e INTERNALTOOL_TOKEN=ilt_your_token_here \
25
+ -- npx -y internaltool-mcp --url https://your-server.com
26
+ ```
27
+
28
+ ### 3. Add to Cursor
29
+
30
+ Create or edit `~/.cursor/mcp.json`:
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "internaltool": {
36
+ "command": "npx",
37
+ "args": ["-y", "internaltool-mcp", "--url", "https://your-server.com"],
38
+ "env": {
39
+ "INTERNALTOOL_TOKEN": "ilt_your_token_here"
40
+ }
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ That's it. No clone. No install. Just `npx`.
47
+
48
+ ---
49
+
50
+ ## CLI flags
51
+
52
+ | Flag | Env var equivalent | Description |
53
+ |---|---|---|
54
+ | `--url <url>` | `INTERNALTOOL_URL` | Base URL of your InternalTool server |
55
+ | `--token <token>` | `INTERNALTOOL_TOKEN` | Personal API key (`ilt_...`) — recommended |
56
+ | `--email <email>` | `INTERNALTOOL_EMAIL` | Email (alternative to API key) |
57
+ | `--password <pw>` | `INTERNALTOOL_PASSWORD` | Password (alternative to API key) |
58
+
59
+ CLI flags take precedence over environment variables.
60
+
61
+ ---
62
+
63
+ ## Managing API keys
64
+
65
+ ```bash
66
+ # List your keys (no raw tokens shown)
67
+ GET /api/auth/api-keys
68
+
69
+ # Generate a new key
70
+ POST /api/auth/api-keys { "label": "Cursor - Work Laptop" }
71
+
72
+ # Revoke a key
73
+ DELETE /api/auth/api-keys/:keyId
74
+ ```
75
+
76
+ - Maximum **10 keys** per user
77
+ - Keys show `lastUsedAt` so you know which ones are active
78
+ - Revoke instantly — no session to wait for
79
+
80
+ ---
81
+
82
+ ## Available tools (25 total)
83
+
84
+ | Category | Tools |
85
+ |---|---|
86
+ | Auth | `login`, `get_current_user` |
87
+ | Users | `list_users` |
88
+ | Projects | `list_projects`, `get_project`, `create_project`, `update_project`, `update_project_members`, `delete_project` |
89
+ | Tasks | `create_task`, `get_task`, `update_task`, `move_task`, `delete_task`, `park_task`, `unpark_task` |
90
+ | Issues | `create_task_issue`, `update_task_issue` |
91
+ | Approval | `submit_task_for_approval`, `decide_task_approval` |
92
+ | Comments | `list_task_comments`, `add_task_comment`, `get_task_activity` |
93
+ | Notifications | `list_notifications`, `mark_all_notifications_read`, `delete_all_notifications` |
94
+ | GitHub | `get_project_commits`, `get_project_branches` |
95
+ | Admin | `admin_list_users`, `admin_get_analytics` |
96
+
97
+ ## Example prompts
98
+
99
+ - *"List my projects"*
100
+ - *"Create a task called 'Fix login bug' in HRMS with critical priority"*
101
+ - *"Move task 683abc... to in_progress"*
102
+ - *"What are my unread notifications?"*
103
+ - *"Show me all comments on task 683abc..."*
package/api-client.js ADDED
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Lightweight HTTP client for InternalTool API.
3
+ *
4
+ * Auth priority:
5
+ * 1. INTERNALTOOL_TOKEN (ilt_...) — API key, no login needed
6
+ * 2. INTERNALTOOL_EMAIL + INTERNALTOOL_PASSWORD — auto-login with JWT
7
+ * 3. Manual login via the `login` tool
8
+ */
9
+
10
+ export let BASE_URL = (process.env.INTERNALTOOL_URL || 'http://localhost:5001').replace(/\/$/, '')
11
+
12
+ let apiToken = process.env.INTERNALTOOL_TOKEN || null // ilt_... personal API key
13
+ let accessToken = null
14
+ let refreshToken = null
15
+
16
+ /** Called from index.js after CLI args are parsed */
17
+ export function configure({ url, token } = {}) {
18
+ if (url) BASE_URL = url.replace(/\/$/, '')
19
+ if (token) apiToken = token
20
+ }
21
+
22
+ function authHeader() {
23
+ if (apiToken) return `Bearer ${apiToken}`
24
+ if (accessToken) return `Bearer ${accessToken}`
25
+ return null
26
+ }
27
+
28
+ async function apiFetch(method, path, body, authenticated = true) {
29
+ const headers = { 'Content-Type': 'application/json' }
30
+
31
+ if (authenticated) {
32
+ const auth = authHeader()
33
+ if (!auth) {
34
+ // No API key and no JWT — try auto-login with env credentials
35
+ await autoLogin()
36
+ }
37
+ headers['Authorization'] = authHeader()
38
+ }
39
+
40
+ const res = await fetch(`${BASE_URL}${path}`, {
41
+ method,
42
+ headers,
43
+ body: body !== undefined ? JSON.stringify(body) : undefined,
44
+ })
45
+
46
+ // Only JWT tokens expire — API keys don't need refresh
47
+ if (res.status === 401 && !apiToken && authenticated && refreshToken) {
48
+ const refreshed = await tryRefresh()
49
+ if (refreshed) {
50
+ headers['Authorization'] = authHeader()
51
+ const retry = await fetch(`${BASE_URL}${path}`, {
52
+ method,
53
+ headers,
54
+ body: body !== undefined ? JSON.stringify(body) : undefined,
55
+ })
56
+ return retry.json()
57
+ }
58
+ }
59
+
60
+ return res.json()
61
+ }
62
+
63
+ async function autoLogin() {
64
+ const email = process.env.INTERNALTOOL_EMAIL
65
+ const password = process.env.INTERNALTOOL_PASSWORD
66
+ if (!email || !password) {
67
+ throw new Error(
68
+ 'Not authenticated. Set INTERNALTOOL_TOKEN (recommended) or INTERNALTOOL_EMAIL + INTERNALTOOL_PASSWORD, or call the "login" tool.'
69
+ )
70
+ }
71
+ await login(email, password)
72
+ }
73
+
74
+ export async function login(email, password) {
75
+ const res = await fetch(`${BASE_URL}/api/auth/login`, {
76
+ method: 'POST',
77
+ headers: { 'Content-Type': 'application/json' },
78
+ body: JSON.stringify({ email, password }),
79
+ })
80
+ const data = await res.json()
81
+ if (!data.success) throw new Error(data.message || 'Login failed')
82
+ accessToken = data.data.accessToken
83
+ refreshToken = data.data.refreshToken
84
+ return data.data
85
+ }
86
+
87
+ async function tryRefresh() {
88
+ try {
89
+ const res = await fetch(`${BASE_URL}/api/auth/refresh`, {
90
+ method: 'POST',
91
+ headers: { 'Content-Type': 'application/json' },
92
+ body: JSON.stringify({ refreshToken }),
93
+ })
94
+ const data = await res.json()
95
+ if (data.success) {
96
+ accessToken = data.data.accessToken
97
+ refreshToken = data.data.refreshToken
98
+ return true
99
+ }
100
+ } catch { /* fall through */ }
101
+ return false
102
+ }
103
+
104
+ export const api = {
105
+ get: (path) => apiFetch('GET', path),
106
+ post: (path, body) => apiFetch('POST', path, body),
107
+ patch: (path, body) => apiFetch('PATCH', path, body),
108
+ delete: (path) => apiFetch('DELETE', path),
109
+ isUsingApiKey: () => !!apiToken,
110
+ }
package/index.js ADDED
@@ -0,0 +1,416 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * InternalTool MCP Server
4
+ *
5
+ * Usage (API key — recommended):
6
+ * npx internaltool-mcp --url https://your-server.com --token ilt_abc123...
7
+ *
8
+ * Usage (email/password):
9
+ * npx internaltool-mcp --url https://your-server.com --email you@x.com --password secret
10
+ *
11
+ * Usage (env vars):
12
+ * INTERNALTOOL_TOKEN=ilt_abc123 npx internaltool-mcp
13
+ */
14
+
15
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
16
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
17
+ import { z } from 'zod'
18
+ import { api, login, configure, BASE_URL } from './api-client.js'
19
+
20
+ // ── Parse CLI args ────────────────────────────────────────────────────────────
21
+ // Supports: --url, --token, --email, --password
22
+ const args = process.argv.slice(2)
23
+ const getArg = (flag) => {
24
+ const i = args.indexOf(flag)
25
+ return i !== -1 && args[i + 1] ? args[i + 1] : null
26
+ }
27
+
28
+ const cliUrl = getArg('--url')
29
+ const cliToken = getArg('--token')
30
+ const cliEmail = getArg('--email')
31
+ const cliPassword = getArg('--password')
32
+
33
+ // CLI args take precedence over env vars
34
+ if (cliEmail) process.env.INTERNALTOOL_EMAIL = cliEmail
35
+ if (cliPassword) process.env.INTERNALTOOL_PASSWORD = cliPassword
36
+
37
+ configure({ url: cliUrl, token: cliToken })
38
+
39
+ const server = new McpServer({
40
+ name: 'internaltool',
41
+ version: '1.0.0',
42
+ })
43
+
44
+ // ─── Helpers ────────────────────────────────────────────────────────────────
45
+
46
+ function text(value) {
47
+ const str = typeof value === 'string' ? value : JSON.stringify(value, null, 2)
48
+ return { content: [{ type: 'text', text: str }] }
49
+ }
50
+
51
+ function errorText(message) {
52
+ return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true }
53
+ }
54
+
55
+ async function call(fn) {
56
+ try {
57
+ const result = await fn()
58
+ if (result && result.success === false) {
59
+ return errorText(result.message || 'API returned an error')
60
+ }
61
+ return text(result.data !== undefined ? result.data : result)
62
+ } catch (e) {
63
+ return errorText(e.message)
64
+ }
65
+ }
66
+
67
+ // ─── Auth ────────────────────────────────────────────────────────────────────
68
+
69
+ server.tool(
70
+ 'login',
71
+ 'Authenticate with the InternalTool API. Required before using other tools unless INTERNALTOOL_EMAIL and INTERNALTOOL_PASSWORD env vars are configured.',
72
+ {
73
+ email: z.string().email().describe('User email address'),
74
+ password: z.string().describe('User password'),
75
+ },
76
+ async ({ email, password }) => {
77
+ try {
78
+ const data = await login(email, password)
79
+ return text({ message: 'Login successful', user: data.user })
80
+ } catch (e) {
81
+ return errorText(e.message)
82
+ }
83
+ }
84
+ )
85
+
86
+ server.tool(
87
+ 'get_current_user',
88
+ 'Get the currently authenticated user profile including name, email, and role.',
89
+ {},
90
+ async () => call(() => api.get('/api/auth/me'))
91
+ )
92
+
93
+ // ─── Users ───────────────────────────────────────────────────────────────────
94
+
95
+ server.tool(
96
+ 'list_users',
97
+ 'List all users in the directory. Useful for finding user IDs when assigning tasks or adding project members.',
98
+ {
99
+ search: z.string().optional().describe('Filter users by name or email'),
100
+ },
101
+ async ({ search }) => {
102
+ const qs = search ? `?search=${encodeURIComponent(search)}` : ''
103
+ return call(() => api.get(`/api/users/directory${qs}`))
104
+ }
105
+ )
106
+
107
+ // ─── Projects ────────────────────────────────────────────────────────────────
108
+
109
+ server.tool(
110
+ 'list_projects',
111
+ 'List all projects visible to the authenticated user with owner info and task counts.',
112
+ {},
113
+ async () => call(() => api.get('/api/projects'))
114
+ )
115
+
116
+ server.tool(
117
+ 'get_project',
118
+ 'Get full details of a project including members, GitHub integration settings, and ALL tasks on the board (all columns).',
119
+ {
120
+ projectId: z.string().describe("Project's MongoDB ObjectId"),
121
+ },
122
+ async ({ projectId }) => call(() => api.get(`/api/projects/${projectId}`))
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}`))
384
+ }
385
+ )
386
+
387
+ server.tool(
388
+ 'get_project_branches',
389
+ 'Get GitHub branches for a project. Requires the project to have a GitHub repo linked.',
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)
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "internaltool-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for InternalTool — connect AI assistants (Claude Code, Cursor) to your project and task management platform",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "internaltool-mcp": "index.js"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "api-client.js",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "start": "node index.js",
17
+ "dev": "node --watch index.js",
18
+ "prepublishOnly": "node --check index.js && node --check api-client.js"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "internaltool",
24
+ "project-management",
25
+ "claude",
26
+ "cursor",
27
+ "ai"
28
+ ],
29
+ "license": "MIT",
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^1.12.0",
35
+ "zod": "^3.23.8"
36
+ }
37
+ }