brioright-mcp 1.5.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/index.js CHANGED
@@ -503,6 +503,204 @@ function buildServer() {
503
503
  }
504
504
  )
505
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
+
506
704
  return server
507
705
 
508
706
  }
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "brioright-mcp",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
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
 
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()