brioright-mcp 1.2.1 → 1.4.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/README.md CHANGED
@@ -88,3 +88,9 @@ And pass the Authorization Header: `Bearer your_secure_bearer_token`.
88
88
  | `create_project` | Create a new project |
89
89
  | `list_members` | List workspace members (for finding assignee IDs) |
90
90
  | `get_workspace_summary` | Dashboard stats: task counts by status/priority |
91
+ | `bulk_create_tasks` | Create multiple tasks at once |
92
+ | `add_comment` | Add a comment to a task |
93
+ | `get_task_comments` | Get all comments for a given task |
94
+ | `log_time` | Log time entry for a project/task |
95
+ | `add_task_attachment` | Upload a file attachment to a task using a Base64 encoded string |
96
+ | `get_task_attachments` | List all file attachments for a specific task |
package/index.js CHANGED
@@ -41,16 +41,21 @@ const MCP_SECRET = process.env.MCP_SECRET // Optional bearer token for HTTP mode
41
41
  const USE_HTTP = process.env.MCP_TRANSPORT === 'http'
42
42
 
43
43
  // ── Axios client factory ──────────────────────────────────────────────────────
44
- async function call(method, path, data, overrideApiKey) {
44
+ async function call(method, path, data, overrideApiKey, customHeaders = {}) {
45
45
  const key = overrideApiKey || ENV_API_KEY;
46
46
  if (!key) {
47
47
  throw new Error('Brioright API error: No API Key provided. Either set BRIORIGHT_API_KEY environment variable or provide apiKey in the tool arguments.');
48
48
  }
49
49
 
50
+ const headers = { 'X-API-Key': key, 'Content-Type': 'application/json', ...customHeaders }
51
+ if (headers['Content-Type'] === 'multipart/form-data' || headers['Content-Type'] === null) {
52
+ delete headers['Content-Type']; // Let axios auto-generate the multipart boundary
53
+ }
54
+
50
55
  const api = axios.create({
51
56
  baseURL: API_URL,
52
- headers: { 'X-API-Key': key, 'Content-Type': 'application/json' },
53
- timeout: 10000,
57
+ headers,
58
+ timeout: 30000,
54
59
  })
55
60
 
56
61
  try {
@@ -129,6 +134,49 @@ function buildServer() {
129
134
  }
130
135
  )
131
136
 
137
+ // ── get_task_attachments ──────────────────────────────────────────────────
138
+ server.tool('get_task_attachments', 'List all file attachments for a specific task',
139
+ {
140
+ taskId: z.string(),
141
+ workspaceId: z.string().optional(),
142
+ apiKey: z.string().optional().describe('Brioright API Key')
143
+ },
144
+ async ({ taskId, workspaceId, apiKey }) => {
145
+ const ws = workspaceId || DEFAULT_WORKSPACE
146
+ if (!ws) throw new Error('workspaceId is required')
147
+ const data = await call('GET', `/workspaces/${ws}/tasks/${taskId}/attachments`, null, apiKey)
148
+ const attachments = data.attachments || data
149
+ return { content: [{ type: 'text', text: JSON.stringify(attachments, null, 2) }] }
150
+ }
151
+ )
152
+
153
+ // ── add_task_attachment ───────────────────────────────────────────────────
154
+ server.tool('add_task_attachment', 'Upload a file attachment to a task using a Base64 encoded string',
155
+ {
156
+ taskId: z.string(),
157
+ fileName: z.string().describe('Name of the file to attach (e.g. image.png)'),
158
+ fileContent: z.string().describe('Base64 encoded string of the file content'),
159
+ mimeType: z.string().optional().describe('MIME type of the file (e.g. image/png)'),
160
+ workspaceId: z.string().optional(),
161
+ apiKey: z.string().optional().describe('Brioright API Key')
162
+ },
163
+ async ({ taskId, fileName, fileContent, mimeType, workspaceId, apiKey }) => {
164
+ const ws = workspaceId || DEFAULT_WORKSPACE
165
+ if (!ws) throw new Error('workspaceId is required')
166
+
167
+ // Convert base64 to Blob
168
+ const buffer = Buffer.from(fileContent, 'base64')
169
+ const blob = new Blob([buffer], { type: mimeType || 'application/octet-stream' })
170
+
171
+ const formData = new FormData()
172
+ formData.append('file', blob, fileName)
173
+
174
+ const data = await call('POST', `/workspaces/${ws}/tasks/${taskId}/attachments`, formData, apiKey, { 'Content-Type': 'multipart/form-data' })
175
+ const attachment = data.attachment || data
176
+ return { content: [{ type: 'text', text: `✅ File attached successfully!\n\n${JSON.stringify({ id: attachment.id, name: attachment.name, url: attachment.url }, null, 2)}` }] }
177
+ }
178
+ )
179
+
132
180
  // ── create_task ───────────────────────────────────────────────────────────
133
181
  server.tool('create_task', 'Create a new task in a Brioright project',
134
182
  {
@@ -155,6 +203,41 @@ function buildServer() {
155
203
  }
156
204
  )
157
205
 
206
+ // ── bulk_create_tasks ─────────────────────────────────────────────────────
207
+ server.tool('bulk_create_tasks', 'Create multiple tasks at once in a Brioright project',
208
+ {
209
+ projectId: z.string().describe('Project ID'),
210
+ tasks: z.array(z.object({
211
+ title: z.string().describe('Task title'),
212
+ description: z.string().optional(),
213
+ status: z.enum(['todo', 'in_progress', 'in_review', 'done']).optional().default('todo'),
214
+ priority: z.enum(['low', 'medium', 'high', 'urgent']).optional().default('medium'),
215
+ dueDate: z.string().optional().describe('ISO date string e.g. 2026-03-15'),
216
+ assigneeId: z.string().optional(),
217
+ parentTaskId: z.string().optional(),
218
+ tags: z.array(z.string()).optional()
219
+ })).describe('Array of tasks to create'),
220
+ workspaceId: z.string().optional(),
221
+ apiKey: z.string().optional().describe('Brioright API Key')
222
+ },
223
+ async ({ projectId, tasks, workspaceId, apiKey }) => {
224
+ const ws = workspaceId || DEFAULT_WORKSPACE
225
+ if (!ws) throw new Error('workspaceId is required')
226
+
227
+ // Format dates
228
+ const formattedTasks = tasks.map(t => ({
229
+ ...t,
230
+ dueDate: t.dueDate ? new Date(t.dueDate).toISOString() : undefined
231
+ }))
232
+
233
+ const data = await call('POST', `/workspaces/${ws}/projects/${projectId}/tasks/bulk`, {
234
+ tasks: formattedTasks
235
+ }, apiKey)
236
+
237
+ return { content: [{ type: 'text', text: `✅ Successfully created ${data.count} tasks!` }] }
238
+ }
239
+ )
240
+
158
241
  // ── update_task ───────────────────────────────────────────────────────────
159
242
  server.tool('update_task', 'Update fields on an existing task',
160
243
  {
@@ -175,7 +258,7 @@ function buildServer() {
175
258
  if (updates.dueDate) updates.dueDate = new Date(updates.dueDate).toISOString()
176
259
  const data = await call('PATCH', `/workspaces/${ws}/tasks/${taskId}`, updates, apiKey)
177
260
  const task = data.task || data
178
- return { content: [{ type: 'text', text: `✅ Task updated!\n\n${JSON.stringify({ id: task.id, title: task.title, status: task.status, priority: task.priority }, null, 2)}` }] }
261
+ return { content: [{ type: 'text', text: `✅ Task updated!\n\n${JSON.stringify({ id: task.id, title: task.title, status: task.status, priority: task.priority, description: task.description, dueDate: task.dueDate }, null, 2)}` }] }
179
262
  }
180
263
  )
181
264
 
@@ -231,6 +314,65 @@ function buildServer() {
231
314
  }
232
315
  )
233
316
 
317
+ // ── add_comment ───────────────────────────────────────────────────────────
318
+ server.tool('add_comment', 'Add a comment to a task',
319
+ {
320
+ taskId: z.string(),
321
+ text: z.string().describe('The content of the comment'),
322
+ workspaceId: z.string().optional(),
323
+ apiKey: z.string().optional().describe('Brioright API Key')
324
+ },
325
+ async ({ taskId, text, workspaceId, apiKey }) => {
326
+ const ws = workspaceId || DEFAULT_WORKSPACE
327
+ if (!ws) throw new Error('workspaceId is required')
328
+ const data = await call('POST', `/workspaces/${ws}/tasks/${taskId}/comments`, { text }, apiKey)
329
+ const comment = data.comment || data
330
+ return { content: [{ type: 'text', text: `✅ Comment added!\n\n${JSON.stringify(comment, null, 2)}` }] }
331
+ }
332
+ )
333
+
334
+ // ── get_task_comments ─────────────────────────────────────────────────────
335
+ server.tool('get_task_comments', 'Get all comments for a given task',
336
+ {
337
+ taskId: z.string(),
338
+ workspaceId: z.string().optional(),
339
+ apiKey: z.string().optional().describe('Brioright API Key')
340
+ },
341
+ async ({ taskId, workspaceId, apiKey }) => {
342
+ const ws = workspaceId || DEFAULT_WORKSPACE
343
+ if (!ws) throw new Error('workspaceId is required')
344
+ const data = await call('GET', `/workspaces/${ws}/tasks/${taskId}/comments`, null, apiKey)
345
+ const comments = data.comments || data
346
+ return { content: [{ type: 'text', text: JSON.stringify(comments.map(c => ({ id: c.id, text: c.text, author: c.author?.name, createdAt: c.createdAt })), null, 2) }] }
347
+ }
348
+ )
349
+
350
+ // ── log_time ──────────────────────────────────────────────────────────────
351
+ server.tool('log_time', 'Log time entry for a project/task',
352
+ {
353
+ projectId: z.string(),
354
+ taskId: z.string().optional(),
355
+ description: z.string().optional(),
356
+ startTime: z.string().optional().describe('ISO date string for start time. Defaults to now.'),
357
+ endTime: z.string().optional().describe('ISO date string for end time. If omitted, starts a running timer.'),
358
+ workspaceId: z.string().optional(),
359
+ apiKey: z.string().optional().describe('Brioright API Key')
360
+ },
361
+ async ({ projectId, taskId, description, startTime, endTime, workspaceId, apiKey }) => {
362
+ const ws = workspaceId || DEFAULT_WORKSPACE
363
+ if (!ws) throw new Error('workspaceId is required')
364
+ const start = startTime ? new Date(startTime).toISOString() : new Date().toISOString()
365
+ const end = endTime ? new Date(endTime).toISOString() : undefined
366
+
367
+ const payload = { projectId, description, startTime: start, endTime: end }
368
+ if (taskId) payload.taskId = taskId
369
+
370
+ const data = await call('POST', `/workspaces/${ws}/time-entries`, payload, apiKey)
371
+ const entry = data.entry || data
372
+ return { content: [{ type: 'text', text: `✅ Time logged (Duration: ${entry.duration ? entry.duration + ' seconds' : 'Running timer ...'})!\n\n${JSON.stringify({ id: entry.id, description: entry.description, startTime: entry.startTime, endTime: entry.endTime, duration: entry.duration }, null, 2)}` }] }
373
+ }
374
+ )
375
+
234
376
  return server
235
377
  }
236
378
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brioright-mcp",
3
- "version": "1.2.1",
3
+ "version": "1.4.0",
4
4
  "description": "MCP server for Brioright — lets AI assistants create and manage tasks via natural language",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/test-axios.mjs DELETED
@@ -1,20 +0,0 @@
1
- import axios from 'axios';
2
-
3
- async function test() {
4
- const api = axios.create({
5
- baseURL: 'https://brioright.online/api',
6
- headers: { 'X-API-Key': 'brio_9847f9c385632d4d1c888afc42c8c3906ffad22bf7713c062a2d1c6db90adadf', 'Content-Type': 'application/json' },
7
- timeout: 10000,
8
- });
9
-
10
- try {
11
- const res = await api({ method: 'GET', url: '/workspaces', data: null });
12
- console.log("SUCCESS:");
13
- console.log(res.data);
14
- } catch (err) {
15
- console.log("ERROR:");
16
- console.log(err.response?.data || err.message);
17
- }
18
- }
19
-
20
- test();
package/test-axios2.mjs DELETED
@@ -1,20 +0,0 @@
1
- import axios from 'axios';
2
-
3
- async function test() {
4
- const api = axios.create({
5
- baseURL: 'https://brioright.online/api',
6
- headers: { 'X-API-Key': 'brio_9847f9c385632d4d1c888afc42c8c3906ffad22bf7713c062a2d1c6db90adadf', 'Content-Type': 'application/json' },
7
- timeout: 10000,
8
- });
9
-
10
- try {
11
- const res = await api({ method: 'GET', url: '/workspaces' }); // data: undefined
12
- console.log("SUCCESS:");
13
- console.log(res.data);
14
- } catch (err) {
15
- console.log("ERROR:");
16
- console.log(err.response?.data || err.message);
17
- }
18
- }
19
-
20
- test();