brioright-mcp 1.4.0 → 1.5.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 +1 -0
  2. package/index.js +222 -91
  3. package/package.json +3 -2
  4. package/.env +0 -3
package/README.md CHANGED
@@ -82,6 +82,7 @@ And pass the Authorization Header: `Bearer your_secure_bearer_token`.
82
82
  | `list_projects` | List projects in a workspace |
83
83
  | `list_tasks` | List tasks with optional status/priority filter |
84
84
  | `get_task` | Get full task details |
85
+ | `duplicate_task` | Duplicate an existing task |
85
86
  | `create_task` | Create a task with title, priority, due date, assignee |
86
87
  | `update_task` | Update any fields on a task |
87
88
  | `complete_task` | Mark a task as done |
package/index.js CHANGED
@@ -76,8 +76,9 @@ function buildServer() {
76
76
  const server = new McpServer({ name: 'brioright', version: '1.0.0' })
77
77
 
78
78
  // ── list_workspaces ───────────────────────────────────────────────────────
79
- server.tool('list_workspaces', 'List all Brioright workspaces the API key has access to',
80
- { apiKey: z.string().optional().describe('Brioright API Key') },
79
+ server.tool('list_workspaces',
80
+ 'Returns all Brioright workspaces accessible via the API key. Call this FIRST when the user has not provided a workspaceId, or when they ask "what workspaces do I have?". The response includes each workspace id, slug, and name — use the slug as workspaceId in subsequent tool calls.',
81
+ { apiKey: z.string().optional().describe('Brioright API key. Only needed if not set in environment.') },
81
82
  async ({ apiKey }) => {
82
83
  const data = await call('GET', '/workspaces', null, apiKey)
83
84
  const workspaces = data.workspaces || data
@@ -86,10 +87,11 @@ function buildServer() {
86
87
  )
87
88
 
88
89
  // ── list_projects ─────────────────────────────────────────────────────────
89
- server.tool('list_projects', 'List all projects in a workspace',
90
+ server.tool('list_projects',
91
+ 'Returns all projects inside a workspace. Use this to discover project IDs before calling list_tasks, create_task, or bulk_create_tasks. Call this when the user mentions a project by name but you need its ID. Returns each project id, name, and status.',
90
92
  {
91
- workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
92
- apiKey: z.string().optional().describe('Brioright API Key')
93
+ workspaceId: z.string().optional().describe('Workspace slug (e.g. "my-team"). Use list_workspaces first if unknown. Defaults to BRIORIGHT_WORKSPACE_ID env var.'),
94
+ apiKey: z.string().optional().describe('Brioright API key. Only needed if not set in environment.')
93
95
  },
94
96
  async ({ workspaceId, apiKey }) => {
95
97
  const ws = workspaceId || DEFAULT_WORKSPACE
@@ -101,14 +103,15 @@ function buildServer() {
101
103
  )
102
104
 
103
105
  // ── list_tasks ────────────────────────────────────────────────────────────
104
- server.tool('list_tasks', 'List tasks in a project with optional filters',
106
+ server.tool('list_tasks',
107
+ 'Fetch tasks from a project with optional filters. Use this when the user asks to see tasks, check what is in progress, find tasks by status/priority, or before updating/completing tasks. Filter by status (todo, in_progress, in_review, done, cancelled) or priority (low, medium, high, urgent). Returns task id, title, status, priority, dueDate, and assignee name.',
105
108
  {
106
- projectId: z.string().describe('Project ID'),
107
- workspaceId: z.string().optional(),
108
- status: z.enum(['todo', 'in_progress', 'in_review', 'done', 'cancelled']).optional(),
109
- priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
110
- limit: z.number().optional().default(20),
111
- apiKey: z.string().optional().describe('Brioright API Key')
109
+ projectId: z.string().describe('Project ID — use list_projects first if you only know the project name.'),
110
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
111
+ status: z.enum(['todo', 'in_progress', 'in_review', 'done', 'cancelled']).optional().describe('Filter by task status. Omit to return all statuses.'),
112
+ priority: z.enum(['low', 'medium', 'high', 'urgent']).optional().describe('Filter by priority level. Omit to return all priorities.'),
113
+ limit: z.number().optional().default(20).describe('Max number of tasks to return. Default 20, max recommended 100.'),
114
+ apiKey: z.string().optional().describe('Brioright API key. Only needed if not set in environment.')
112
115
  },
113
116
  async ({ projectId, workspaceId, status, priority, limit, apiKey }) => {
114
117
  const ws = workspaceId || DEFAULT_WORKSPACE
@@ -124,8 +127,9 @@ function buildServer() {
124
127
  )
125
128
 
126
129
  // ── get_task ──────────────────────────────────────────────────────────────
127
- server.tool('get_task', 'Get full details of a single task',
128
- { taskId: z.string(), workspaceId: z.string().optional(), apiKey: z.string().optional().describe('Brioright API Key') },
130
+ server.tool('get_task',
131
+ 'Fetches complete details for a single task by its ID, including description, status, priority, assignee, due date, tags, subtasks, and dependencies. Use this when you need the full task data before updating it, summarising it, or answering detailed questions about it. Prefer this over list_tasks when you already have the task ID.',
132
+ { taskId: z.string().describe('The unique task ID (UUID). Use list_tasks to find it if unknown.'), workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'), apiKey: z.string().optional().describe('Brioright API key.') },
129
133
  async ({ taskId, workspaceId, apiKey }) => {
130
134
  const ws = workspaceId || DEFAULT_WORKSPACE
131
135
  if (!ws) throw new Error('workspaceId is required')
@@ -135,11 +139,12 @@ function buildServer() {
135
139
  )
136
140
 
137
141
  // ── get_task_attachments ──────────────────────────────────────────────────
138
- server.tool('get_task_attachments', 'List all file attachments for a specific task',
142
+ server.tool('get_task_attachments',
143
+ 'Lists all files attached to a task. Use this when the user asks "what files are on this task?" or before deciding to add a new attachment. Returns file name, URL, size, and uploader.',
139
144
  {
140
- taskId: z.string(),
141
- workspaceId: z.string().optional(),
142
- apiKey: z.string().optional().describe('Brioright API Key')
145
+ taskId: z.string().describe('ID of the task to list attachments for.'),
146
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
147
+ apiKey: z.string().optional().describe('Brioright API key.')
143
148
  },
144
149
  async ({ taskId, workspaceId, apiKey }) => {
145
150
  const ws = workspaceId || DEFAULT_WORKSPACE
@@ -151,14 +156,15 @@ function buildServer() {
151
156
  )
152
157
 
153
158
  // ── add_task_attachment ───────────────────────────────────────────────────
154
- server.tool('add_task_attachment', 'Upload a file attachment to a task using a Base64 encoded string',
159
+ server.tool('add_task_attachment',
160
+ 'Uploads a file to a task as an attachment. Use this when the user wants to attach a document, screenshot, or report to a task. The file must be Base64-encoded. Returns the attachment id, file name, and public URL.',
155
161
  {
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
+ taskId: z.string().describe('ID of the task to attach the file to.'),
163
+ fileName: z.string().describe('File name with extension, e.g. "report.pdf" or "screenshot.png".'),
164
+ fileContent: z.string().describe('Full Base64-encoded content of the file.'),
165
+ mimeType: z.string().optional().describe('MIME type, e.g. "image/png", "application/pdf". Defaults to application/octet-stream.'),
166
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
167
+ apiKey: z.string().optional().describe('Brioright API key.')
162
168
  },
163
169
  async ({ taskId, fileName, fileContent, mimeType, workspaceId, apiKey }) => {
164
170
  const ws = workspaceId || DEFAULT_WORKSPACE
@@ -178,17 +184,18 @@ function buildServer() {
178
184
  )
179
185
 
180
186
  // ── create_task ───────────────────────────────────────────────────────────
181
- server.tool('create_task', 'Create a new task in a Brioright project',
187
+ server.tool('create_task',
188
+ 'Creates a single new task in a project. Use this when the user asks to add one task. For creating multiple tasks at once, prefer bulk_create_tasks instead. Use list_projects to get projectId and list_members to get assigneeId if needed. Returns the created task id, title, status, priority, and due date.',
182
189
  {
183
- projectId: z.string().describe('Project ID'),
184
- title: z.string().describe('Task title'),
185
- description: z.string().optional(),
186
- status: z.enum(['todo', 'in_progress', 'in_review', 'done']).optional().default('todo'),
187
- priority: z.enum(['low', 'medium', 'high', 'urgent']).optional().default('medium'),
188
- dueDate: z.string().optional().describe('ISO date string e.g. 2026-03-15'),
189
- assigneeId: z.string().optional(),
190
- workspaceId: z.string().optional(),
191
- apiKey: z.string().optional().describe('Brioright API Key')
190
+ projectId: z.string().describe('Project ID — use list_projects if you only know the project name.'),
191
+ title: z.string().describe('Clear, concise task title describing the work to be done.'),
192
+ description: z.string().optional().describe('Detailed description, acceptance criteria, or context for the task. Supports markdown.'),
193
+ status: z.enum(['todo', 'in_progress', 'in_review', 'done']).optional().default('todo').describe('Initial task status. Default is todo.'),
194
+ priority: z.enum(['low', 'medium', 'high', 'urgent']).optional().default('medium').describe('Task priority. Use urgent only for blockers or critical issues.'),
195
+ dueDate: z.string().optional().describe('Due date as ISO string, e.g. "2026-04-01". Omit if no deadline.'),
196
+ assigneeId: z.string().optional().describe('User ID of the assignee — use list_members to find IDs. Omit to leave unassigned.'),
197
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
198
+ apiKey: z.string().optional().describe('Brioright API key.')
192
199
  },
193
200
  async ({ projectId, title, description, status, priority, dueDate, assigneeId, workspaceId, apiKey }) => {
194
201
  const ws = workspaceId || DEFAULT_WORKSPACE
@@ -196,38 +203,63 @@ function buildServer() {
196
203
  const data = await call('POST', `/workspaces/${ws}/projects/${projectId}/tasks`, {
197
204
  title, description, status, priority,
198
205
  dueDate: dueDate ? new Date(dueDate).toISOString() : undefined,
199
- assigneeId,
206
+ assigneeId: assigneeId || undefined,
200
207
  }, apiKey)
201
208
  const task = data.task || data
202
209
  return { content: [{ type: 'text', text: `✅ Task created!\n\n${JSON.stringify({ id: task.id, title: task.title, status: task.status, priority: task.priority, dueDate: task.dueDate }, null, 2)}` }] }
203
210
  }
204
211
  )
205
212
 
213
+ // ── duplicate_task ────────────────────────────────────────────────────────
214
+ server.tool('duplicate_task',
215
+ 'Creates an exact copy of an existing task, including its title, description, priority, and assignee. Use this when the user wants to repeat a task, clone a template task, or create a similar task quickly. The duplicate is placed in the same project. Returns the new task id and title.',
216
+ {
217
+ taskId: z.string().describe('ID of the original task to copy.'),
218
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
219
+ apiKey: z.string().optional().describe('Brioright API key.')
220
+ },
221
+ async ({ taskId, workspaceId, apiKey }) => {
222
+ const ws = workspaceId || DEFAULT_WORKSPACE
223
+ if (!ws) throw new Error('workspaceId is required')
224
+
225
+ // We need to fetch the task first to find its projectId, as the /duplicate endpoint requires project ID via the URL path
226
+ const taskData = await call('GET', `/workspaces/${ws}/tasks/${taskId}`, null, apiKey)
227
+ const originalTask = taskData.task || taskData
228
+
229
+ const data = await call('POST', `/workspaces/${ws}/projects/${originalTask.projectId}/tasks/${taskId}/duplicate`, null, apiKey)
230
+ const clonedTask = data.task || data
231
+ return { content: [{ type: 'text', text: `✅ Task duplicated!\n\n${JSON.stringify({ id: clonedTask.id, title: clonedTask.title, status: clonedTask.status, position: clonedTask.position }, null, 2)}` }] }
232
+ }
233
+ )
234
+
206
235
  // ── bulk_create_tasks ─────────────────────────────────────────────────────
207
- server.tool('bulk_create_tasks', 'Create multiple tasks at once in a Brioright project',
236
+ server.tool('bulk_create_tasks',
237
+ 'Creates multiple tasks in one API call — far more efficient than calling create_task repeatedly. Use this when the user provides a list of tasks, a sprint plan, a feature breakdown, or asks you to set up a project. Each task can have its own title, description, status, priority, due date, and assignee. Use parentTaskId to create subtasks under an existing task. Returns the count of created tasks.',
208
238
  {
209
- projectId: z.string().describe('Project ID'),
239
+ projectId: z.string().describe('Project ID — use list_projects first if unknown.'),
210
240
  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')
241
+ title: z.string().describe('Clear task title.'),
242
+ description: z.string().optional().describe('Detailed context or acceptance criteria for the task.'),
243
+ status: z.enum(['todo', 'in_progress', 'in_review', 'done']).optional().default('todo').describe('Task status. Default: todo.'),
244
+ priority: z.enum(['low', 'medium', 'high', 'urgent']).optional().default('medium').describe('Priority level.'),
245
+ dueDate: z.string().optional().describe('ISO date string e.g. "2026-04-01". Omit if no deadline.'),
246
+ assigneeId: z.string().optional().describe('User ID from list_members. Omit to leave unassigned.'),
247
+ parentTaskId: z.string().optional().describe('ID of a parent task to nest this as a subtask. Omit for top-level tasks.'),
248
+ tags: z.array(z.string()).optional().describe('Label tags for categorisation, e.g. ["backend", "auth"].')
249
+ })).describe('Array of task objects to create. Minimum 1, no hard maximum.'),
250
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
251
+ apiKey: z.string().optional().describe('Brioright API key.')
222
252
  },
223
253
  async ({ projectId, tasks, workspaceId, apiKey }) => {
224
254
  const ws = workspaceId || DEFAULT_WORKSPACE
225
255
  if (!ws) throw new Error('workspaceId is required')
226
256
 
227
- // Format dates
257
+ // Format dates and sanitise strings
228
258
  const formattedTasks = tasks.map(t => ({
229
259
  ...t,
230
- dueDate: t.dueDate ? new Date(t.dueDate).toISOString() : undefined
260
+ dueDate: t.dueDate ? new Date(t.dueDate).toISOString() : undefined,
261
+ assigneeId: t.assigneeId || undefined,
262
+ parentTaskId: t.parentTaskId || undefined
231
263
  }))
232
264
 
233
265
  const data = await call('POST', `/workspaces/${ws}/projects/${projectId}/tasks/bulk`, {
@@ -239,22 +271,23 @@ function buildServer() {
239
271
  )
240
272
 
241
273
  // ── update_task ───────────────────────────────────────────────────────────
242
- server.tool('update_task', 'Update fields on an existing task',
274
+ server.tool('update_task',
275
+ 'Updates one or more fields on an existing task. Use this when the user asks to change a task title, reassign it, change priority or status, update the description, or set/change a due date. Only provide the fields you want to change — omit the rest. To mark a task done, you can either use this tool with status=done or use complete_task. Returns the updated task.',
243
276
  {
244
- taskId: z.string(),
245
- title: z.string().optional(),
246
- description: z.string().optional(),
247
- status: z.enum(['todo', 'in_progress', 'in_review', 'done', 'cancelled']).optional(),
248
- priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
249
- dueDate: z.string().optional(),
250
- assigneeId: z.string().optional(),
251
- workspaceId: z.string().optional(),
252
- apiKey: z.string().optional().describe('Brioright API Key')
277
+ taskId: z.string().describe('ID of the task to update — use list_tasks or get_task to find it.'),
278
+ title: z.string().optional().describe('New task title. Omit to keep current.'),
279
+ description: z.string().optional().describe('New description. Omit to keep current. Supports markdown.'),
280
+ status: z.enum(['todo', 'in_progress', 'in_review', 'done', 'cancelled']).optional().describe('New status. Use cancelled for tasks that will not be done.'),
281
+ priority: z.enum(['low', 'medium', 'high', 'urgent']).optional().describe('New priority level.'),
282
+ dueDate: z.string().optional().describe('New due date as ISO string e.g. "2026-04-15". Omit to keep current.'),
283
+ assigneeId: z.string().optional().describe('User ID of new assignee — use list_members to find. Omit to keep current.'),
284
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
285
+ apiKey: z.string().optional().describe('Brioright API key.')
253
286
  },
254
287
  async ({ taskId, workspaceId, apiKey, ...fields }) => {
255
288
  const ws = workspaceId || DEFAULT_WORKSPACE
256
289
  if (!ws) throw new Error('workspaceId is required')
257
- const updates = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined))
290
+ const updates = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined && v !== ''))
258
291
  if (updates.dueDate) updates.dueDate = new Date(updates.dueDate).toISOString()
259
292
  const data = await call('PATCH', `/workspaces/${ws}/tasks/${taskId}`, updates, apiKey)
260
293
  const task = data.task || data
@@ -263,8 +296,9 @@ function buildServer() {
263
296
  )
264
297
 
265
298
  // ── complete_task ─────────────────────────────────────────────────────────
266
- server.tool('complete_task', 'Mark a task as completed',
267
- { taskId: z.string(), workspaceId: z.string().optional(), apiKey: z.string().optional().describe('Brioright API Key') },
299
+ server.tool('complete_task',
300
+ 'Marks a task as done (status=done). Use this as a shorthand when the user says "complete", "finish", "mark as done", or "close" a task. Equivalent to update_task with status=done but more concise. Returns a confirmation.',
301
+ { taskId: z.string().describe('ID of the task to complete.'), workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'), apiKey: z.string().optional().describe('Brioright API key.') },
268
302
  async ({ taskId, workspaceId, apiKey }) => {
269
303
  const ws = workspaceId || DEFAULT_WORKSPACE
270
304
  if (!ws) throw new Error('workspaceId is required')
@@ -274,13 +308,14 @@ function buildServer() {
274
308
  )
275
309
 
276
310
  // ── create_project ────────────────────────────────────────────────────────
277
- server.tool('create_project', 'Create a new project in a workspace',
311
+ server.tool('create_project',
312
+ 'Creates a new project (board) inside a workspace. Use this when the user asks to set up a new project, initiative, or area of work. After creating the project, you can immediately use bulk_create_tasks to populate it with tasks. Returns the new project id and name.',
278
313
  {
279
- name: z.string(),
280
- description: z.string().optional(),
281
- color: z.string().optional().default('#6366f1'),
282
- workspaceId: z.string().optional(),
283
- apiKey: z.string().optional().describe('Brioright API Key')
314
+ name: z.string().describe('Project name, e.g. "Q2 Marketing Campaign" or "Mobile App Redesign".'),
315
+ description: z.string().optional().describe('Brief summary of the project goals or scope.'),
316
+ color: z.string().optional().default('#6366f1').describe('Hex color for visual identification, e.g. "#f59e0b". Default is indigo (#6366f1).'),
317
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
318
+ apiKey: z.string().optional().describe('Brioright API key.')
284
319
  },
285
320
  async ({ name, description, color, workspaceId, apiKey }) => {
286
321
  const ws = workspaceId || DEFAULT_WORKSPACE
@@ -292,8 +327,9 @@ function buildServer() {
292
327
  )
293
328
 
294
329
  // ── list_members ──────────────────────────────────────────────────────────
295
- server.tool('list_members', 'List workspace members (useful for finding assignee IDs)',
296
- { workspaceId: z.string().optional(), apiKey: z.string().optional().describe('Brioright API Key') },
330
+ server.tool('list_members',
331
+ 'Returns all members of a workspace with their user IDs, names, emails, and roles. Call this BEFORE create_task or update_task when the user mentions assigning a task to someone by name — use this to resolve the name to a user ID. Also use it to answer "who is on this workspace?" or "what is [person]s user ID?".',
332
+ { workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'), apiKey: z.string().optional().describe('Brioright API key.') },
297
333
  async ({ workspaceId, apiKey }) => {
298
334
  const ws = workspaceId || DEFAULT_WORKSPACE
299
335
  if (!ws) throw new Error('workspaceId is required')
@@ -304,8 +340,9 @@ function buildServer() {
304
340
  )
305
341
 
306
342
  // ── get_workspace_summary ─────────────────────────────────────────────────
307
- server.tool('get_workspace_summary', 'Dashboard stats: task counts by status and priority',
308
- { workspaceId: z.string().optional(), apiKey: z.string().optional().describe('Brioright API Key') },
343
+ server.tool('get_workspace_summary',
344
+ 'Returns high-level dashboard statistics for a workspace: total tasks broken down by status (todo, in_progress, done, etc.) and by priority (low, medium, high, urgent). Use this to answer questions like "how many tasks are pending?", "what is our workload?", or "give me a project health overview". Does NOT return individual task details — use list_tasks for that.',
345
+ { workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'), apiKey: z.string().optional().describe('Brioright API key.') },
309
346
  async ({ workspaceId, apiKey }) => {
310
347
  const ws = workspaceId || DEFAULT_WORKSPACE
311
348
  if (!ws) throw new Error('workspaceId is required')
@@ -315,12 +352,13 @@ function buildServer() {
315
352
  )
316
353
 
317
354
  // ── add_comment ───────────────────────────────────────────────────────────
318
- server.tool('add_comment', 'Add a comment to a task',
355
+ server.tool('add_comment',
356
+ 'Posts a comment on a task. Use this to leave a status update, ask a question on a task, provide context, or summarise findings. Supports markdown formatting in the text. Returns the posted comment with its ID and timestamp.',
319
357
  {
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')
358
+ taskId: z.string().describe('ID of the task to comment on.'),
359
+ text: z.string().describe('Comment content. Supports markdown — use **bold**, bullet lists, code blocks etc. for clarity.'),
360
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
361
+ apiKey: z.string().optional().describe('Brioright API key.')
324
362
  },
325
363
  async ({ taskId, text, workspaceId, apiKey }) => {
326
364
  const ws = workspaceId || DEFAULT_WORKSPACE
@@ -332,11 +370,12 @@ function buildServer() {
332
370
  )
333
371
 
334
372
  // ── get_task_comments ─────────────────────────────────────────────────────
335
- server.tool('get_task_comments', 'Get all comments for a given task',
373
+ server.tool('get_task_comments',
374
+ 'Retrieves the full comment thread for a task in chronological order. Use this when the user asks "what has been discussed on this task?", wants a summary of activity, or before adding a new comment to avoid duplicating information. Returns each comment id, text, author name, and timestamp.',
336
375
  {
337
- taskId: z.string(),
338
- workspaceId: z.string().optional(),
339
- apiKey: z.string().optional().describe('Brioright API Key')
376
+ taskId: z.string().describe('ID of the task to fetch comments for.'),
377
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
378
+ apiKey: z.string().optional().describe('Brioright API key.')
340
379
  },
341
380
  async ({ taskId, workspaceId, apiKey }) => {
342
381
  const ws = workspaceId || DEFAULT_WORKSPACE
@@ -347,16 +386,34 @@ function buildServer() {
347
386
  }
348
387
  )
349
388
 
389
+ // ── get_task_activities ───────────────────────────────────────────────────
390
+ server.tool('get_task_activities',
391
+ 'Returns the full audit trail of changes made to a task: who changed what field, when status changed, when it was assigned, due date updates, etc. Use this when the user asks "what changed on this task?", "when was this assigned?", or "show me the task history". Different from get_task_comments — this is system-generated activity, not user comments.',
392
+ {
393
+ taskId: z.string().describe('ID of the task to fetch activity history for.'),
394
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
395
+ apiKey: z.string().optional().describe('Brioright API key.')
396
+ },
397
+ async ({ taskId, workspaceId, apiKey }) => {
398
+ const ws = workspaceId || DEFAULT_WORKSPACE
399
+ if (!ws) throw new Error('workspaceId is required')
400
+ const data = await call('GET', `/workspaces/${ws}/tasks/${taskId}/activities`, null, apiKey)
401
+ const activities = data.activities || data
402
+ return { content: [{ type: 'text', text: JSON.stringify(activities.map(a => ({ id: a.id, type: a.type, message: a.message, user: a.user?.name, createdAt: a.createdAt })), null, 2) }] }
403
+ }
404
+ )
405
+
350
406
  // ── log_time ──────────────────────────────────────────────────────────────
351
- server.tool('log_time', 'Log time entry for a project/task',
407
+ server.tool('log_time',
408
+ 'Records a time entry against a project or specific task. Use this when the user says "log 2 hours on task X", "I spent 30 minutes on the auth feature", or "start a timer". If endTime is omitted, a running timer is started. If both startTime and endTime are provided, a completed time block is logged. projectId is required; taskId narrows it to a specific task.',
352
409
  {
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')
410
+ projectId: z.string().describe('Project ID to log time against — use list_projects if unknown.'),
411
+ taskId: z.string().optional().describe('Optional task ID to associate the time entry with a specific task.'),
412
+ description: z.string().optional().describe('What was worked on during this time, e.g. "Fixed login bug", "Code review for PR #42".'),
413
+ startTime: z.string().optional().describe('Start of the work period as ISO string, e.g. "2026-03-22T09:00:00Z". Defaults to now.'),
414
+ endTime: z.string().optional().describe('End of the work period as ISO string. Omit to start a running timer instead of logging a completed block.'),
415
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
416
+ apiKey: z.string().optional().describe('Brioright API key.')
360
417
  },
361
418
  async ({ projectId, taskId, description, startTime, endTime, workspaceId, apiKey }) => {
362
419
  const ws = workspaceId || DEFAULT_WORKSPACE
@@ -373,7 +430,81 @@ function buildServer() {
373
430
  }
374
431
  )
375
432
 
433
+
434
+ // ── delete_task ───────────────────────────────────────────────────────────
435
+ server.tool('delete_task',
436
+ 'Permanently deletes a task and all its subtasks, comments, and attachments. This action is IRREVERSIBLE. Use this when the user explicitly asks to delete or remove a task. Do NOT use this to cancel a task — use update_task with status=cancelled instead. Always confirm with the user before calling this if there is any ambiguity. Returns a confirmation message.',
437
+ {
438
+ taskId: z.string().describe('ID of the task to permanently delete. Use list_tasks or get_task to find it.'),
439
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
440
+ apiKey: z.string().optional().describe('Brioright API key.')
441
+ },
442
+ async ({ taskId, workspaceId, apiKey }) => {
443
+ const ws = workspaceId || DEFAULT_WORKSPACE
444
+ if (!ws) throw new Error('workspaceId is required')
445
+ await call('DELETE', `/workspaces/${ws}/tasks/${taskId}`, null, apiKey)
446
+ return { content: [{ type: 'text', text: `🗑️ Task ${taskId} has been permanently deleted.` }] }
447
+ }
448
+ )
449
+
450
+ // ── search_workspace ──────────────────────────────────────────────────────
451
+ server.tool('search_workspace',
452
+ 'Searches across the entire workspace and returns matching tasks, projects, and members in a single call. Use this when the user asks to find something by keyword, e.g. "find tasks about login", "search for the auth project", or "who is named John?". Returns up to 5 results per category (tasks, projects, users). Requires at least 2 characters in the query.',
453
+ {
454
+ query: z.string().describe('Search keyword or phrase — minimum 2 characters. Searches task titles/descriptions, project names/descriptions, and member names/emails.'),
455
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
456
+ apiKey: z.string().optional().describe('Brioright API key.')
457
+ },
458
+ async ({ query, workspaceId, apiKey }) => {
459
+ const ws = workspaceId || DEFAULT_WORKSPACE
460
+ if (!ws) throw new Error('workspaceId is required')
461
+ if (!query || query.trim().length < 2) throw new Error('Search query must be at least 2 characters')
462
+ const data = await call('GET', `/workspaces/${ws}/search?q=${encodeURIComponent(query.trim())}`, null, apiKey)
463
+ const { projects = [], tasks = [], users = [] } = data
464
+ const result = {
465
+ summary: `Found ${tasks.length} task(s), ${projects.length} project(s), ${users.length} member(s) for "${query}"`,
466
+ tasks: tasks.map(t => ({ id: t.id, title: t.title, status: t.status, priority: t.priority, projectId: t.projectId })),
467
+ projects: projects.map(p => ({ id: p.id, name: p.name, description: p.description })),
468
+ users: users.map(u => ({ id: u.id, name: u.name, email: u.email }))
469
+ }
470
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
471
+ }
472
+ )
473
+
474
+ // ── get_workspace_analytics ───────────────────────────────────────────────
475
+ server.tool('get_workspace_analytics',
476
+ 'Returns detailed analytics for a workspace including: task completion rate, tasks by status and priority, completion trends over time, task creation trends, top performers (leaderboard), member workload distribution, and per-project progress. Use this when the user asks for a report, "how are we doing?", "show me the analytics", "who completed the most tasks?", or "what is our completion rate?". Use get_workspace_summary instead for just basic task counts.',
477
+ {
478
+ range: z.enum(['7d', '30d', '90d', 'all']).optional().default('30d').describe('Time range for trend data. 7d=last week, 30d=last month (default), 90d=last quarter, all=all time.'),
479
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
480
+ apiKey: z.string().optional().describe('Brioright API key.')
481
+ },
482
+ async ({ range, workspaceId, apiKey }) => {
483
+ const ws = workspaceId || DEFAULT_WORKSPACE
484
+ if (!ws) throw new Error('workspaceId is required')
485
+ const data = await call('GET', `/workspaces/${ws}/analytics?range=${range || '30d'}`, null, apiKey)
486
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
487
+ }
488
+ )
489
+
490
+ // ── get_overdue_tasks ─────────────────────────────────────────────────────
491
+ server.tool('get_overdue_tasks',
492
+ 'Returns all tasks that are past their due date and still not done. Use this when the user asks "what is overdue?", "what tasks are late?", or "show me missed deadlines". Results include task id, title, due date, assignee, and project. This is a dedicated endpoint — do not try to replicate this by filtering list_tasks manually.',
493
+ {
494
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
495
+ apiKey: z.string().optional().describe('Brioright API key.')
496
+ },
497
+ async ({ workspaceId, apiKey }) => {
498
+ const ws = workspaceId || DEFAULT_WORKSPACE
499
+ if (!ws) throw new Error('workspaceId is required')
500
+ const data = await call('GET', `/workspaces/${ws}/tasks/overdue`, null, apiKey)
501
+ const tasks = data.tasks || data
502
+ return { content: [{ type: 'text', text: JSON.stringify(tasks.map(t => ({ id: t.id, title: t.title, dueDate: t.dueDate, assignee: t.assignee?.name, projectId: t.projectId, status: t.status })), null, 2) }] }
503
+ }
504
+ )
505
+
376
506
  return server
507
+
377
508
  }
378
509
 
379
510
  // ── HTTP/SSE transport (for cloud AI clients) ─────────────────────────────────
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "brioright-mcp",
3
- "version": "1.4.0",
4
- "description": "MCP server for Brioright — lets AI assistants create and manage tasks via natural language",
3
+ "version": "1.5.0",
4
+ "description": "MCP server for Brioright — lets AI assistants (Claude, Cursor, ChatGPT) create, search, analyse and manage tasks via natural language",
5
5
  "type": "module",
6
+
6
7
  "main": "index.js",
7
8
  "bin": {
8
9
  "brioright-mcp": "index.js"
package/.env DELETED
@@ -1,3 +0,0 @@
1
- BRIORIGHT_API_URL=http://localhost:3001/api
2
- BRIORIGHT_API_KEY=brio_9e4e5415fd92d4a3768918a938149f5533c16ae7ac2157ba786cf33dd815a63d
3
- BRIORIGHT_WORKSPACE_ID=welcomet