brioright-mcp 1.5.0 → 1.7.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 +339 -0
- package/list-local-ws.js +21 -0
- package/package.json +1 -1
- package/test-logic.js +65 -0
package/index.js
CHANGED
|
@@ -503,6 +503,345 @@ 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
|
+
|
|
704
|
+
// ── start_work ─────────────────────────────────────────────────────────────
|
|
705
|
+
server.tool('start_work',
|
|
706
|
+
'Signals that AI has STARTED working on a task. Creates a task with status=in_progress and returns its ID. Call this at the BEGINNING of any significant work session (feature, bug fix, refactor, UI change, etc.) so the work is tracked from the start. Returns a taskId — pass it to finish_work when the work is complete. Pair: start_work → finish_work.',
|
|
707
|
+
{
|
|
708
|
+
projectId: z.string().describe('Project ID to log the work against — use list_projects if unknown.'),
|
|
709
|
+
title: z.string().describe('Clear title for the work being started, e.g. "Refactor login page UI" or "Fix avatar upload bug".'),
|
|
710
|
+
description: z.string().optional().describe('Optional: what will be done, the approach, and which files/areas will be affected.'),
|
|
711
|
+
priority: z.enum(['low', 'medium', 'high', 'urgent']).optional().default('medium').describe('Priority of the work. Use urgent for blockers.'),
|
|
712
|
+
workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
|
|
713
|
+
apiKey: z.string().optional().describe('Brioright API key.')
|
|
714
|
+
},
|
|
715
|
+
async ({ projectId, title, description, priority, workspaceId, apiKey }) => {
|
|
716
|
+
const ws = workspaceId || DEFAULT_WORKSPACE
|
|
717
|
+
if (!ws) throw new Error('workspaceId is required')
|
|
718
|
+
const startedAt = new Date().toLocaleString()
|
|
719
|
+
const data = await call('POST', `/workspaces/${ws}/projects/${projectId}/tasks`, {
|
|
720
|
+
title,
|
|
721
|
+
description: description
|
|
722
|
+
? `${description}\n\n---\n_🤖 Work started by AI agent at ${startedAt}_`
|
|
723
|
+
: `🤖 Work started by AI agent at ${startedAt}.\n\n_Call \`finish_work\` with this task ID when the work is complete._`,
|
|
724
|
+
status: 'in_progress',
|
|
725
|
+
priority: priority || 'medium',
|
|
726
|
+
}, apiKey)
|
|
727
|
+
const task = data.task || data
|
|
728
|
+
return { content: [{ type: 'text', text: `🚀 Work started and logged!\n\nTask ID: **${task.id}**\nTitle: ${task.title}\nStatus: in_progress\n\n👉 Remember to call \`finish_work\` with taskId="${task.id}" when done.` }] }
|
|
729
|
+
}
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
// ── finish_work ─────────────────────────────────────────────────────────────
|
|
733
|
+
server.tool('finish_work',
|
|
734
|
+
'Signals that AI has COMPLETED a piece of work. Marks the task as done and posts a structured markdown completion summary as a comment (what changed, files modified, key decisions). Use this to close a task that was opened with start_work. If you did not call start_work, use track_work instead for a one-shot retroactive log.',
|
|
735
|
+
{
|
|
736
|
+
taskId: z.string().describe('Task ID returned by start_work. This is the task to mark as done.'),
|
|
737
|
+
summary: z.string().describe('What was accomplished — be specific. Describe what changed, why, and any important decisions. Markdown is supported.'),
|
|
738
|
+
filesChanged: z.array(z.string()).optional().describe('List of files that were created or modified, e.g. ["src/components/Login.jsx", "src/styles/auth.css"]. Will be shown in the completion comment.'),
|
|
739
|
+
workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
|
|
740
|
+
apiKey: z.string().optional().describe('Brioright API key.')
|
|
741
|
+
},
|
|
742
|
+
async ({ taskId, summary, filesChanged, workspaceId, apiKey }) => {
|
|
743
|
+
const ws = workspaceId || DEFAULT_WORKSPACE
|
|
744
|
+
if (!ws) throw new Error('workspaceId is required')
|
|
745
|
+
|
|
746
|
+
// Mark task as done
|
|
747
|
+
await call('PATCH', `/workspaces/${ws}/tasks/${taskId}`, { status: 'done' }, apiKey)
|
|
748
|
+
|
|
749
|
+
// Build a clean structured completion comment
|
|
750
|
+
const fileSection = filesChanged?.length
|
|
751
|
+
? `\n\n### 📁 Files Changed\n${filesChanged.map(f => `- \`${f}\``).join('\n')}`
|
|
752
|
+
: ''
|
|
753
|
+
const comment = [
|
|
754
|
+
`## ✅ Work Completed`,
|
|
755
|
+
``,
|
|
756
|
+
summary,
|
|
757
|
+
fileSection,
|
|
758
|
+
``,
|
|
759
|
+
`---`,
|
|
760
|
+
`_🤖 Completed by AI agent at ${new Date().toLocaleString()}_`
|
|
761
|
+
].join('\n')
|
|
762
|
+
|
|
763
|
+
await call('POST', `/workspaces/${ws}/tasks/${taskId}/comments`, { text: comment }, apiKey)
|
|
764
|
+
|
|
765
|
+
return { content: [{ type: 'text', text: `✅ Work finished and logged!\n\nTask ${taskId} → done\nCompletion summary posted as a comment on the task.` }] }
|
|
766
|
+
}
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
// ── track_work ─────────────────────────────────────────────────────────────
|
|
770
|
+
server.tool('track_work',
|
|
771
|
+
'Smart one-shot work tracker. Call this AFTER completing any work session to automatically: (1) search for an existing matching task in the project, (2) reuse it or create a fresh one if not found, (3) mark it done, and (4) post a structured completion comment. This is the preferred tool when you did not call start_work at the beginning — it handles the full lifecycle in a single call. Ideal for retroactively logging completed work.',
|
|
772
|
+
{
|
|
773
|
+
projectId: z.string().describe('Project ID to log work against — use list_projects if unknown.'),
|
|
774
|
+
title: z.string().describe('Title of the work done. Used first to search for an existing task — be specific enough to match if one exists.'),
|
|
775
|
+
summary: z.string().describe('Detailed description of what was accomplished. Supports markdown. Include: what changed, why, key decisions, and caveats.'),
|
|
776
|
+
priority: z.enum(['low', 'medium', 'high', 'urgent']).optional().default('medium').describe('Priority for the task if a new one must be created.'),
|
|
777
|
+
filesChanged: z.array(z.string()).optional().describe('List of files created or modified. Shown in the completion comment for traceability.'),
|
|
778
|
+
workspaceId: z.string().optional().describe('Workspace slug. Defaults to BRIORIGHT_WORKSPACE_ID.'),
|
|
779
|
+
apiKey: z.string().optional().describe('Brioright API key.')
|
|
780
|
+
},
|
|
781
|
+
async ({ projectId, title, summary, priority, filesChanged, workspaceId, apiKey }) => {
|
|
782
|
+
const ws = workspaceId || DEFAULT_WORKSPACE
|
|
783
|
+
if (!ws) throw new Error('workspaceId is required')
|
|
784
|
+
|
|
785
|
+
let taskId = null
|
|
786
|
+
let action = 'created'
|
|
787
|
+
|
|
788
|
+
// Step 1: Search for an existing open task with a similar title
|
|
789
|
+
try {
|
|
790
|
+
const searchQuery = title.substring(0, 60).trim()
|
|
791
|
+
const searchData = await call('GET', `/workspaces/${ws}/search?q=${encodeURIComponent(searchQuery)}`, null, apiKey)
|
|
792
|
+
const { tasks = [] } = searchData
|
|
793
|
+
// Find a task in the same project that isn't already closed
|
|
794
|
+
const match = tasks.find(t =>
|
|
795
|
+
t.projectId === projectId &&
|
|
796
|
+
!['done', 'cancelled'].includes(t.status)
|
|
797
|
+
)
|
|
798
|
+
if (match) {
|
|
799
|
+
taskId = match.id
|
|
800
|
+
action = 'found_existing'
|
|
801
|
+
}
|
|
802
|
+
} catch {
|
|
803
|
+
// Search failed — proceed to create a new task
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Step 2: Create task if no match was found
|
|
807
|
+
if (!taskId) {
|
|
808
|
+
const data = await call('POST', `/workspaces/${ws}/projects/${projectId}/tasks`, {
|
|
809
|
+
title,
|
|
810
|
+
description: summary,
|
|
811
|
+
status: 'done',
|
|
812
|
+
priority: priority || 'medium',
|
|
813
|
+
}, apiKey)
|
|
814
|
+
const task = data.task || data
|
|
815
|
+
taskId = task.id
|
|
816
|
+
} else {
|
|
817
|
+
// Mark the found task as done
|
|
818
|
+
await call('PATCH', `/workspaces/${ws}/tasks/${taskId}`, { status: 'done' }, apiKey)
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Step 3: Post structured completion comment
|
|
822
|
+
const fileSection = filesChanged?.length
|
|
823
|
+
? `\n\n### 📁 Files Changed\n${filesChanged.map(f => `- \`${f}\``).join('\n')}`
|
|
824
|
+
: ''
|
|
825
|
+
const comment = [
|
|
826
|
+
`## ✅ Work Completed`,
|
|
827
|
+
``,
|
|
828
|
+
summary,
|
|
829
|
+
fileSection,
|
|
830
|
+
``,
|
|
831
|
+
`---`,
|
|
832
|
+
`_🤖 Auto-tracked by AI agent via \`track_work\` at ${new Date().toLocaleString()}_`
|
|
833
|
+
].join('\n')
|
|
834
|
+
|
|
835
|
+
await call('POST', `/workspaces/${ws}/tasks/${taskId}/comments`, { text: comment }, apiKey)
|
|
836
|
+
|
|
837
|
+
const actionLabel = action === 'found_existing'
|
|
838
|
+
? `♻️ Found existing open task — marked it done`
|
|
839
|
+
: `🆕 No matching task found — created a new one`
|
|
840
|
+
|
|
841
|
+
return { content: [{ type: 'text', text: `✅ Work tracked!\n\n${actionLabel}\nTask ID: ${taskId}\nCompletion summary posted as a comment.` }] }
|
|
842
|
+
}
|
|
843
|
+
)
|
|
844
|
+
|
|
506
845
|
return server
|
|
507
846
|
|
|
508
847
|
}
|
package/list-local-ws.js
ADDED
|
@@ -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
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()
|