brioright-mcp 1.3.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 +7 -0
  2. package/index.js +262 -83
  3. package/package.json +3 -2
  4. package/.env +0 -3
package/README.md CHANGED
@@ -82,9 +82,16 @@ 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 |
88
89
  | `create_project` | Create a new project |
89
90
  | `list_members` | List workspace members (for finding assignee IDs) |
90
91
  | `get_workspace_summary` | Dashboard stats: task counts by status/priority |
92
+ | `bulk_create_tasks` | Create multiple tasks at once |
93
+ | `add_comment` | Add a comment to a task |
94
+ | `get_task_comments` | Get all comments for a given task |
95
+ | `log_time` | Log time entry for a project/task |
96
+ | `add_task_attachment` | Upload a file attachment to a task using a Base64 encoded string |
97
+ | `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 {
@@ -71,8 +76,9 @@ function buildServer() {
71
76
  const server = new McpServer({ name: 'brioright', version: '1.0.0' })
72
77
 
73
78
  // ── list_workspaces ───────────────────────────────────────────────────────
74
- server.tool('list_workspaces', 'List all Brioright workspaces the API key has access to',
75
- { 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.') },
76
82
  async ({ apiKey }) => {
77
83
  const data = await call('GET', '/workspaces', null, apiKey)
78
84
  const workspaces = data.workspaces || data
@@ -81,10 +87,11 @@ function buildServer() {
81
87
  )
82
88
 
83
89
  // ── list_projects ─────────────────────────────────────────────────────────
84
- 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.',
85
92
  {
86
- workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
87
- 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.')
88
95
  },
89
96
  async ({ workspaceId, apiKey }) => {
90
97
  const ws = workspaceId || DEFAULT_WORKSPACE
@@ -96,14 +103,15 @@ function buildServer() {
96
103
  )
97
104
 
98
105
  // ── list_tasks ────────────────────────────────────────────────────────────
99
- 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.',
100
108
  {
101
- projectId: z.string().describe('Project ID'),
102
- workspaceId: z.string().optional(),
103
- status: z.enum(['todo', 'in_progress', 'in_review', 'done', 'cancelled']).optional(),
104
- priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
105
- limit: z.number().optional().default(20),
106
- 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.')
107
115
  },
108
116
  async ({ projectId, workspaceId, status, priority, limit, apiKey }) => {
109
117
  const ws = workspaceId || DEFAULT_WORKSPACE
@@ -119,8 +127,9 @@ function buildServer() {
119
127
  )
120
128
 
121
129
  // ── get_task ──────────────────────────────────────────────────────────────
122
- server.tool('get_task', 'Get full details of a single task',
123
- { 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.') },
124
133
  async ({ taskId, workspaceId, apiKey }) => {
125
134
  const ws = workspaceId || DEFAULT_WORKSPACE
126
135
  if (!ws) throw new Error('workspaceId is required')
@@ -129,18 +138,64 @@ function buildServer() {
129
138
  }
130
139
  )
131
140
 
141
+ // ── get_task_attachments ──────────────────────────────────────────────────
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.',
144
+ {
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.')
148
+ },
149
+ async ({ taskId, workspaceId, apiKey }) => {
150
+ const ws = workspaceId || DEFAULT_WORKSPACE
151
+ if (!ws) throw new Error('workspaceId is required')
152
+ const data = await call('GET', `/workspaces/${ws}/tasks/${taskId}/attachments`, null, apiKey)
153
+ const attachments = data.attachments || data
154
+ return { content: [{ type: 'text', text: JSON.stringify(attachments, null, 2) }] }
155
+ }
156
+ )
157
+
158
+ // ── add_task_attachment ───────────────────────────────────────────────────
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.',
161
+ {
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.')
168
+ },
169
+ async ({ taskId, fileName, fileContent, mimeType, workspaceId, apiKey }) => {
170
+ const ws = workspaceId || DEFAULT_WORKSPACE
171
+ if (!ws) throw new Error('workspaceId is required')
172
+
173
+ // Convert base64 to Blob
174
+ const buffer = Buffer.from(fileContent, 'base64')
175
+ const blob = new Blob([buffer], { type: mimeType || 'application/octet-stream' })
176
+
177
+ const formData = new FormData()
178
+ formData.append('file', blob, fileName)
179
+
180
+ const data = await call('POST', `/workspaces/${ws}/tasks/${taskId}/attachments`, formData, apiKey, { 'Content-Type': 'multipart/form-data' })
181
+ const attachment = data.attachment || data
182
+ return { content: [{ type: 'text', text: `✅ File attached successfully!\n\n${JSON.stringify({ id: attachment.id, name: attachment.name, url: attachment.url }, null, 2)}` }] }
183
+ }
184
+ )
185
+
132
186
  // ── create_task ───────────────────────────────────────────────────────────
133
- 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.',
134
189
  {
135
- projectId: z.string().describe('Project ID'),
136
- title: z.string().describe('Task title'),
137
- description: z.string().optional(),
138
- status: z.enum(['todo', 'in_progress', 'in_review', 'done']).optional().default('todo'),
139
- priority: z.enum(['low', 'medium', 'high', 'urgent']).optional().default('medium'),
140
- dueDate: z.string().optional().describe('ISO date string e.g. 2026-03-15'),
141
- assigneeId: z.string().optional(),
142
- workspaceId: z.string().optional(),
143
- 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.')
144
199
  },
145
200
  async ({ projectId, title, description, status, priority, dueDate, assigneeId, workspaceId, apiKey }) => {
146
201
  const ws = workspaceId || DEFAULT_WORKSPACE
@@ -148,38 +203,63 @@ function buildServer() {
148
203
  const data = await call('POST', `/workspaces/${ws}/projects/${projectId}/tasks`, {
149
204
  title, description, status, priority,
150
205
  dueDate: dueDate ? new Date(dueDate).toISOString() : undefined,
151
- assigneeId,
206
+ assigneeId: assigneeId || undefined,
152
207
  }, apiKey)
153
208
  const task = data.task || data
154
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)}` }] }
155
210
  }
156
211
  )
157
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
+
158
235
  // ── bulk_create_tasks ─────────────────────────────────────────────────────
159
- 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.',
160
238
  {
161
- projectId: z.string().describe('Project ID'),
239
+ projectId: z.string().describe('Project ID — use list_projects first if unknown.'),
162
240
  tasks: z.array(z.object({
163
- title: z.string().describe('Task title'),
164
- description: z.string().optional(),
165
- status: z.enum(['todo', 'in_progress', 'in_review', 'done']).optional().default('todo'),
166
- priority: z.enum(['low', 'medium', 'high', 'urgent']).optional().default('medium'),
167
- dueDate: z.string().optional().describe('ISO date string e.g. 2026-03-15'),
168
- assigneeId: z.string().optional(),
169
- parentTaskId: z.string().optional(),
170
- tags: z.array(z.string()).optional()
171
- })).describe('Array of tasks to create'),
172
- workspaceId: z.string().optional(),
173
- 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.')
174
252
  },
175
253
  async ({ projectId, tasks, workspaceId, apiKey }) => {
176
254
  const ws = workspaceId || DEFAULT_WORKSPACE
177
255
  if (!ws) throw new Error('workspaceId is required')
178
256
 
179
- // Format dates
257
+ // Format dates and sanitise strings
180
258
  const formattedTasks = tasks.map(t => ({
181
259
  ...t,
182
- 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
183
263
  }))
184
264
 
185
265
  const data = await call('POST', `/workspaces/${ws}/projects/${projectId}/tasks/bulk`, {
@@ -191,22 +271,23 @@ function buildServer() {
191
271
  )
192
272
 
193
273
  // ── update_task ───────────────────────────────────────────────────────────
194
- 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.',
195
276
  {
196
- taskId: z.string(),
197
- title: z.string().optional(),
198
- description: z.string().optional(),
199
- status: z.enum(['todo', 'in_progress', 'in_review', 'done', 'cancelled']).optional(),
200
- priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
201
- dueDate: z.string().optional(),
202
- assigneeId: z.string().optional(),
203
- workspaceId: z.string().optional(),
204
- 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.')
205
286
  },
206
287
  async ({ taskId, workspaceId, apiKey, ...fields }) => {
207
288
  const ws = workspaceId || DEFAULT_WORKSPACE
208
289
  if (!ws) throw new Error('workspaceId is required')
209
- const updates = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined))
290
+ const updates = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined && v !== ''))
210
291
  if (updates.dueDate) updates.dueDate = new Date(updates.dueDate).toISOString()
211
292
  const data = await call('PATCH', `/workspaces/${ws}/tasks/${taskId}`, updates, apiKey)
212
293
  const task = data.task || data
@@ -215,8 +296,9 @@ function buildServer() {
215
296
  )
216
297
 
217
298
  // ── complete_task ─────────────────────────────────────────────────────────
218
- server.tool('complete_task', 'Mark a task as completed',
219
- { 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.') },
220
302
  async ({ taskId, workspaceId, apiKey }) => {
221
303
  const ws = workspaceId || DEFAULT_WORKSPACE
222
304
  if (!ws) throw new Error('workspaceId is required')
@@ -226,13 +308,14 @@ function buildServer() {
226
308
  )
227
309
 
228
310
  // ── create_project ────────────────────────────────────────────────────────
229
- 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.',
230
313
  {
231
- name: z.string(),
232
- description: z.string().optional(),
233
- color: z.string().optional().default('#6366f1'),
234
- workspaceId: z.string().optional(),
235
- 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.')
236
319
  },
237
320
  async ({ name, description, color, workspaceId, apiKey }) => {
238
321
  const ws = workspaceId || DEFAULT_WORKSPACE
@@ -244,8 +327,9 @@ function buildServer() {
244
327
  )
245
328
 
246
329
  // ── list_members ──────────────────────────────────────────────────────────
247
- server.tool('list_members', 'List workspace members (useful for finding assignee IDs)',
248
- { 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.') },
249
333
  async ({ workspaceId, apiKey }) => {
250
334
  const ws = workspaceId || DEFAULT_WORKSPACE
251
335
  if (!ws) throw new Error('workspaceId is required')
@@ -256,8 +340,9 @@ function buildServer() {
256
340
  )
257
341
 
258
342
  // ── get_workspace_summary ─────────────────────────────────────────────────
259
- server.tool('get_workspace_summary', 'Dashboard stats: task counts by status and priority',
260
- { 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.') },
261
346
  async ({ workspaceId, apiKey }) => {
262
347
  const ws = workspaceId || DEFAULT_WORKSPACE
263
348
  if (!ws) throw new Error('workspaceId is required')
@@ -267,12 +352,13 @@ function buildServer() {
267
352
  )
268
353
 
269
354
  // ── add_comment ───────────────────────────────────────────────────────────
270
- 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.',
271
357
  {
272
- taskId: z.string(),
273
- text: z.string().describe('The content of the comment'),
274
- workspaceId: z.string().optional(),
275
- 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.')
276
362
  },
277
363
  async ({ taskId, text, workspaceId, apiKey }) => {
278
364
  const ws = workspaceId || DEFAULT_WORKSPACE
@@ -284,11 +370,12 @@ function buildServer() {
284
370
  )
285
371
 
286
372
  // ── get_task_comments ─────────────────────────────────────────────────────
287
- 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.',
288
375
  {
289
- taskId: z.string(),
290
- workspaceId: z.string().optional(),
291
- 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.')
292
379
  },
293
380
  async ({ taskId, workspaceId, apiKey }) => {
294
381
  const ws = workspaceId || DEFAULT_WORKSPACE
@@ -299,16 +386,34 @@ function buildServer() {
299
386
  }
300
387
  )
301
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
+
302
406
  // ── log_time ──────────────────────────────────────────────────────────────
303
- 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.',
304
409
  {
305
- projectId: z.string(),
306
- taskId: z.string().optional(),
307
- description: z.string().optional(),
308
- startTime: z.string().optional().describe('ISO date string for start time. Defaults to now.'),
309
- endTime: z.string().optional().describe('ISO date string for end time. If omitted, starts a running timer.'),
310
- workspaceId: z.string().optional(),
311
- 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.')
312
417
  },
313
418
  async ({ projectId, taskId, description, startTime, endTime, workspaceId, apiKey }) => {
314
419
  const ws = workspaceId || DEFAULT_WORKSPACE
@@ -325,7 +430,81 @@ function buildServer() {
325
430
  }
326
431
  )
327
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
+
328
506
  return server
507
+
329
508
  }
330
509
 
331
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.3.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