brioright-mcp 1.4.0 → 1.6.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
@@ -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,279 @@ 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
+
506
+ // ── get_project_analytics ────────────────────────────────────────────────
507
+ server.tool('get_project_analytics',
508
+ 'Returns detailed analytics for a specific project including: task completion rate, task counts by status/priority, and daily completion trends. Use this when the user asks "how is this project doing?", "show me the project health", or "what is the completion rate for the Lumi project?".',
509
+ {
510
+ projectId: z.string().describe('ID of the project to fetch analytics for.'),
511
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
512
+ apiKey: z.string().optional().describe('Brioright API key.')
513
+ },
514
+ async ({ projectId, workspaceId, apiKey }) => {
515
+ const ws = workspaceId || DEFAULT_WORKSPACE
516
+ if (!ws) throw new Error('workspaceId is required')
517
+ const data = await call('GET', `/workspaces/${ws}/projects/${projectId}/analytics`, null, apiKey)
518
+ return { content: [{ type: 'text', text: JSON.stringify(data.analytics || data, null, 2) }] }
519
+ }
520
+ )
521
+
522
+ // ── get_project_activity ──────────────────────────────────────────────────
523
+ server.tool('get_project_activity',
524
+ 'Returns the recent activity log for a specific project. Use this when the user asks "what has happened recently in this project?" or "show me the history of the Lumi app project". Returns up to 50 recent actions.',
525
+ {
526
+ projectId: z.string().describe('ID of the project to fetch activity history for.'),
527
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
528
+ apiKey: z.string().optional().describe('Brioright API key.')
529
+ },
530
+ async ({ projectId, workspaceId, apiKey }) => {
531
+ const ws = workspaceId || DEFAULT_WORKSPACE
532
+ if (!ws) throw new Error('workspaceId is required')
533
+ const data = await call('GET', `/workspaces/${ws}/projects/${projectId}/activity`, null, apiKey)
534
+ const activities = data.activities || data
535
+ 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) }] }
536
+ }
537
+ )
538
+
539
+ // ── get_active_timers ─────────────────────────────────────────────────────
540
+ server.tool('get_active_timers',
541
+ 'Returns all running (active) time trackers for the current user across all projects. Use this when the user asks "do I have a timer running?", "what am I working on right now?", or "show me my active timers".',
542
+ {
543
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
544
+ apiKey: z.string().optional().describe('Brioright API key.')
545
+ },
546
+ async ({ workspaceId, apiKey }) => {
547
+ const ws = workspaceId || DEFAULT_WORKSPACE
548
+ if (!ws) throw new Error('workspaceId is required')
549
+ const data = await call('GET', `/workspaces/${ws}/time-entries/active`, null, apiKey)
550
+ const active = data.active || data
551
+ return { content: [{ type: 'text', text: JSON.stringify(active, null, 2) }] }
552
+ }
553
+ )
554
+
555
+ // ── stop_timer ────────────────────────────────────────────────────────────
556
+ server.tool('stop_timer',
557
+ 'Stops a running time tracker by its ID. Use this when the user says "stop my timer", "I am done with this task", or "clock me out". If you don\'t have the timer ID, call get_active_timers first.',
558
+ {
559
+ timerId: z.string().describe('The ID of the time entry to stop.'),
560
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
561
+ apiKey: z.string().optional().describe('Brioright API key.')
562
+ },
563
+ async ({ timerId, workspaceId, apiKey }) => {
564
+ const ws = workspaceId || DEFAULT_WORKSPACE
565
+ if (!ws) throw new Error('workspaceId is required')
566
+ await call('PATCH', `/workspaces/${ws}/time-entries/${timerId}/stop`, null, apiKey)
567
+ return { content: [{ type: 'text', text: `✅ Timer ${timerId} stopped successfully.` }] }
568
+ }
569
+ )
570
+
571
+ // ── get_time_summary ──────────────────────────────────────────────────────
572
+ server.tool('get_time_summary',
573
+ 'Returns a statistical summary of time logged by the user, aggregated by day or project. Use this when the user asks "how much did I work this week?", "show me my time stats", or "how many hours have I logged today?".',
574
+ {
575
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
576
+ apiKey: z.string().optional().describe('Brioright API key.')
577
+ },
578
+ async ({ workspaceId, apiKey }) => {
579
+ const ws = workspaceId || DEFAULT_WORKSPACE
580
+ if (!ws) throw new Error('workspaceId is required')
581
+ const data = await call('GET', `/workspaces/${ws}/time-entries/summary`, null, apiKey)
582
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
583
+ }
584
+ )
585
+
586
+ // ── get_time_entries ──────────────────────────────────────────────────────
587
+ server.tool('get_time_entries',
588
+ 'Lists detailed time entry logs with optional filters for project or task. Use this when the user asks "show me my time logs for the Lumi project" or "when did I work on task X?".',
589
+ {
590
+ projectId: z.string().optional().describe('Filter by Project ID.'),
591
+ taskId: z.string().optional().describe('Filter by Task ID.'),
592
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
593
+ apiKey: z.string().optional().describe('Brioright API key.')
594
+ },
595
+ async ({ projectId, taskId, workspaceId, apiKey }) => {
596
+ const ws = workspaceId || DEFAULT_WORKSPACE
597
+ if (!ws) throw new Error('workspaceId is required')
598
+ const params = new URLSearchParams()
599
+ if (projectId) params.set('projectId', projectId)
600
+ if (taskId) params.set('taskId', taskId)
601
+ const data = await call('GET', `/workspaces/${ws}/time-entries?${params}`, null, apiKey)
602
+ const entries = data.entries || data
603
+ return { content: [{ type: 'text', text: JSON.stringify(entries, null, 2) }] }
604
+ }
605
+ )
606
+
607
+ // ── get_task_dependencies ────────────────────────────────────────────────
608
+ server.tool('get_task_dependencies',
609
+ 'Returns a list of all tasks that the specified task depends on (blockers) and tasks that depend on it. Use this when the user asks "what is blocking this task?" or "are there any dependencies for task X?". Useful for project scheduling and identifying bottlenecks.',
610
+ {
611
+ taskId: z.string().describe('ID of the task to fetch dependencies for.'),
612
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
613
+ apiKey: z.string().optional().describe('Brioright API key.')
614
+ },
615
+ async ({ taskId, workspaceId, apiKey }) => {
616
+ const ws = workspaceId || DEFAULT_WORKSPACE
617
+ if (!ws) throw new Error('workspaceId is required')
618
+ const data = await call('GET', `/workspaces/${ws}/tasks/${taskId}/dependencies`, null, apiKey)
619
+ return { content: [{ type: 'text', text: JSON.stringify(data.dependencies || data, null, 2) }] }
620
+ }
621
+ )
622
+
623
+ // ── add_task_dependency ───────────────────────────────────────────────────
624
+ server.tool('add_task_dependency',
625
+ 'Creates a dependency relationship between two tasks. Use this when the user says "task A depends on task B" or "task B blocks task A". In this case, task B is the blockingTaskId.',
626
+ {
627
+ taskId: z.string().describe('ID of the task that is being blocked.'),
628
+ blockingTaskId: z.string().describe('ID of the task that must be completed first.'),
629
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
630
+ apiKey: z.string().optional().describe('Brioright API key.')
631
+ },
632
+ async ({ taskId, blockingTaskId, workspaceId, apiKey }) => {
633
+ const ws = workspaceId || DEFAULT_WORKSPACE
634
+ if (!ws) throw new Error('workspaceId is required')
635
+ const data = await call('POST', `/workspaces/${ws}/tasks/${taskId}/dependencies`, { blockingTaskId }, apiKey)
636
+ return { content: [{ type: 'text', text: `✅ Dependency added: Task ${taskId} is now blocked by Task ${blockingTaskId}.` }] }
637
+ }
638
+ )
639
+
640
+ // ── remove_task_dependency ────────────────────────────────────────────────
641
+ server.tool('remove_task_dependency',
642
+ 'Removes an existing dependency between two tasks. Use this when a dependency is no longer valid or was added by mistake. Requires the dependency ID (depId), which can be found via get_task_dependencies.',
643
+ {
644
+ taskId: z.string().describe('ID of the task that was being blocked.'),
645
+ depId: z.string().describe('ID of the dependency relationship to remove.'),
646
+ workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
647
+ apiKey: z.string().optional().describe('Brioright API key.')
648
+ },
649
+ async ({ taskId, depId, workspaceId, apiKey }) => {
650
+ const ws = workspaceId || DEFAULT_WORKSPACE
651
+ if (!ws) throw new Error('workspaceId is required')
652
+ await call('DELETE', `/workspaces/${ws}/tasks/${taskId}/dependencies/${depId}`, null, apiKey)
653
+ return { content: [{ type: 'text', text: `✅ Dependency ${depId} removed from Task ${taskId}.` }] }
654
+ }
655
+ )
656
+
657
+ // ── search_users ──────────────────────────────────────────────────────────
658
+ server.tool('search_users',
659
+ 'Allows searching for users across the entire Brioright system by name or email. Use this when you need to find a user\'s ID for task assignment and they aren\'t in the current workspace member list, or when the user says "assign this to John" and you need to find which John.',
660
+ {
661
+ query: z.string().describe('Search keyword (name or email). Minimum 2 characters.'),
662
+ apiKey: z.string().optional().describe('Brioright API key.')
663
+ },
664
+ async ({ query, apiKey }) => {
665
+ if (!query || query.trim().length < 2) throw new Error('Search query must be at least 2 characters')
666
+ const data = await call('GET', `/users/search?q=${encodeURIComponent(query.trim())}`, null, apiKey)
667
+ const users = data.users || data
668
+ return { content: [{ type: 'text', text: JSON.stringify(users, null, 2) }] }
669
+ }
670
+ )
671
+
672
+ // ── get_notifications ─────────────────────────────────────────────────────
673
+ server.tool('get_notifications',
674
+ 'Fetches the latest unread notifications for the current user. Use this when the user asks "what are my notifications?", "has anyone replied to my comment?", or "are there any updates for me?".',
675
+ {
676
+ apiKey: z.string().optional().describe('Brioright API key.')
677
+ },
678
+ async ({ apiKey }) => {
679
+ const data = await call('GET', '/notifications', null, apiKey)
680
+ const notifications = data.notifications || data
681
+ return { content: [{ type: 'text', text: JSON.stringify(notifications, null, 2) }] }
682
+ }
683
+ )
684
+
685
+ // ── mark_notification_read ────────────────────────────────────────────────
686
+ server.tool('mark_notification_read',
687
+ 'Marks one or all notifications as read. Use this when the user says "clear my notifications" or "mark notification X as read".',
688
+ {
689
+ notificationId: z.string().optional().describe('ID of the specific notification to mark as read. If omitted, and "all" is true, marks all read.'),
690
+ all: z.boolean().optional().describe('If true, marks all notifications in the workspace as read.'),
691
+ apiKey: z.string().optional().describe('Brioright API key.')
692
+ },
693
+ async ({ notificationId, all, apiKey }) => {
694
+ if (all) {
695
+ await call('PATCH', '/notifications/read-all', null, apiKey)
696
+ return { content: [{ type: 'text', text: '✅ All notifications marked as read.' }] }
697
+ }
698
+ if (!notificationId) throw new Error('Either notificationId or all=true must be provided')
699
+ await call('PATCH', `/notifications/${notificationId}/read`, null, apiKey)
700
+ return { content: [{ type: 'text', text: `✅ Notification ${notificationId} marked as read.` }] }
701
+ }
702
+ )
703
+
376
704
  return server
705
+
377
706
  }
378
707
 
379
708
  // ── HTTP/SSE transport (for cloud AI clients) ─────────────────────────────────
@@ -0,0 +1,21 @@
1
+ import axios from 'axios'
2
+ import dotenv from 'dotenv'
3
+
4
+ dotenv.config({ path: './.env' })
5
+
6
+ async function listWorkspaces() {
7
+ const API_URL = process.env.BRIORIGHT_API_URL || 'http://localhost:3001/api'
8
+ const API_KEY = process.env.BRIORIGHT_API_KEY
9
+
10
+ try {
11
+ const res = await axios.get(`${API_URL}/workspaces`, {
12
+ headers: { 'X-API-Key': API_KEY }
13
+ })
14
+ console.log('Workspaces found on local server:')
15
+ console.log(JSON.stringify(res.data.workspaces || res.data, null, 2))
16
+ } catch (err) {
17
+ console.error('Error fetching workspaces:', err.message)
18
+ }
19
+ }
20
+
21
+ listWorkspaces()
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.6.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/test-logic.js ADDED
@@ -0,0 +1,65 @@
1
+ import axios from 'axios'
2
+ import dotenv from 'dotenv'
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+ dotenv.config({ path: path.join(__dirname, '.env') })
8
+
9
+ const API_URL = process.env.BRIORIGHT_API_URL || 'http://localhost:5000/api'
10
+ const API_KEY = process.env.BRIORIGHT_API_KEY
11
+ const WS_SLUG = process.env.BRIORIGHT_WORKSPACE_ID
12
+
13
+ if (!API_KEY || !WS_SLUG) {
14
+ console.error('❌ BRIORIGHT_API_KEY and BRIORIGHT_WORKSPACE_ID must be set in .env')
15
+ process.exit(1)
16
+ }
17
+
18
+ const client = axios.create({
19
+ baseURL: API_URL,
20
+ headers: { 'X-API-Key': API_KEY }
21
+ })
22
+
23
+ async function testTools() {
24
+ console.log('🚀 Starting MCP Tool Verification...\n')
25
+
26
+ try {
27
+ // 1. Test Search Users
28
+ console.log('🔍 Testing: search_users ("Sanket")...')
29
+ const users = await client.get('/users/search?q=Sanket')
30
+ console.log(`✅ Found ${users.data.users?.length || 0} users.\n`)
31
+
32
+ // 2. Test Project Analytics
33
+ console.log('📊 Testing: get_project_analytics...')
34
+ const projects = await client.get(`/workspaces/${WS_SLUG}/projects`)
35
+ const projectId = projects.data.projects?.[0]?.id
36
+ if (projectId) {
37
+ const analytics = await client.get(`/workspaces/${WS_SLUG}/projects/${projectId}/analytics`)
38
+ console.log(`✅ Analytics fetched for project: ${projectId}\n`)
39
+ } else {
40
+ console.log('⚠️ No projects found to test analytics.\n')
41
+ }
42
+
43
+ // 3. Test Notifications
44
+ console.log('🔔 Testing: get_notifications...')
45
+ const notifications = await client.get('/notifications')
46
+ console.log(`✅ Fetched ${notifications.data.notifications?.length || 0} notifications.\n`)
47
+
48
+ // 4. Test Active Timers
49
+ console.log('⏱️ Testing: get_active_timers...')
50
+ const timers = await client.get(`/workspaces/${WS_SLUG}/time-entries/active`)
51
+ console.log(`✅ Fetched active timers.\n`)
52
+
53
+ console.log('🎉 All core endpoints for new tools verified!')
54
+ } catch (error) {
55
+ console.error('❌ Test failed:')
56
+ if (error.response) {
57
+ console.error(` Status: ${error.response.status}`)
58
+ console.error(` Data: ${JSON.stringify(error.response.data)}`)
59
+ } else {
60
+ console.error(` Message: ${error.message}`)
61
+ }
62
+ }
63
+ }
64
+
65
+ testTools()
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