agenthub-multiagent-mcp 1.1.5 → 1.4.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.
@@ -2,19 +2,40 @@
2
2
  * MCP tool definitions and handlers
3
3
  */
4
4
  import * as state from "../state.js";
5
+ import open from "open";
5
6
  /**
6
- * Wraps a tool response with pending messages
7
- * This enables automatic message delivery without extra API calls
7
+ * Wraps a tool response with pending messages and tasks
8
+ * This enables automatic delivery without extra API calls
9
+ * Includes items pushed via WebSocket
8
10
  */
9
- async function wrapWithMessages(client, agentId, result) {
11
+ async function wrapWithPendingItems(client, agentId, result, context) {
10
12
  // If not registered, return raw result
11
13
  if (!agentId)
12
14
  return result;
13
15
  try {
14
- // Fetch unread messages
15
- const inbox = await client.getInbox(agentId, true, 10);
16
- const messages = inbox.messages || [];
17
- // Sort: urgent first, then by created_at
16
+ // Get items pushed via WebSocket (if connected)
17
+ const pushedItems = context?.getPushedItems() || { tasks: [], messages: [] };
18
+ // Fetch unread messages and pending tasks in parallel
19
+ const [inbox, tasksResponse] = await Promise.all([
20
+ client.getInbox(agentId, true, 10),
21
+ client.getPendingTasks(agentId),
22
+ ]);
23
+ // Merge pushed items with fetched items (deduplicating by ID)
24
+ const fetchedMessages = inbox.messages || [];
25
+ const fetchedTasks = (tasksResponse.tasks || []);
26
+ // Deduplicate messages
27
+ const messageIds = new Set(fetchedMessages.map((m) => m.id));
28
+ const messages = [
29
+ ...fetchedMessages,
30
+ ...pushedItems.messages.filter((m) => !messageIds.has(m.id)),
31
+ ];
32
+ // Deduplicate tasks
33
+ const taskIds = new Set(fetchedTasks.map((t) => t.id));
34
+ const tasks = [
35
+ ...fetchedTasks,
36
+ ...pushedItems.tasks.filter((t) => !taskIds.has(t.id)),
37
+ ];
38
+ // Sort messages: urgent first, then by created_at
18
39
  messages.sort((a, b) => {
19
40
  const aUrgent = isUrgentMessage(a);
20
41
  const bUrgent = isUrgentMessage(b);
@@ -24,27 +45,40 @@ async function wrapWithMessages(client, agentId, result) {
24
45
  return 1;
25
46
  return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
26
47
  });
27
- // Find urgent message for action
28
- const urgentMsg = messages.find(isUrgentMessage);
48
+ // Determine urgent action - tasks take priority over messages
29
49
  let urgentAction = null;
30
- if (urgentMsg) {
50
+ // Check for pending tasks first (they're actionable)
51
+ if (tasks.length > 0) {
52
+ const highPriorityTask = tasks.find((t) => t.priority === "high") || tasks[0];
31
53
  urgentAction = {
32
54
  required: true,
33
- instruction: buildUrgentInstruction(urgentMsg),
34
- message: urgentMsg,
55
+ instruction: buildTaskInstruction(highPriorityTask),
56
+ task: highPriorityTask,
35
57
  };
36
58
  }
59
+ else {
60
+ // Check for urgent messages
61
+ const urgentMsg = messages.find(isUrgentMessage);
62
+ if (urgentMsg) {
63
+ urgentAction = {
64
+ required: true,
65
+ instruction: buildUrgentInstruction(urgentMsg),
66
+ message: urgentMsg,
67
+ };
68
+ }
69
+ }
37
70
  return {
38
71
  result,
39
72
  pending_messages: messages,
40
- pending_count: inbox.total,
41
- has_more: inbox.total > messages.length,
73
+ pending_messages_count: messages.length,
74
+ pending_tasks: tasks,
75
+ pending_tasks_count: tasks.length,
42
76
  urgent_action: urgentAction,
43
77
  };
44
78
  }
45
79
  catch (error) {
46
- // Don't break tool call if message fetch fails
47
- console.warn("Failed to fetch messages for wrapping:", error);
80
+ // Don't break tool call if fetch fails
81
+ console.warn("Failed to fetch pending items for wrapping:", error);
48
82
  return result;
49
83
  }
50
84
  }
@@ -67,6 +101,17 @@ Details: ${msg.body}
67
101
 
68
102
  Spawn a subagent with Task tool to research and respond to this query. Use send_message to reply when done.`;
69
103
  }
104
+ /**
105
+ * Build instruction text for pending tasks
106
+ */
107
+ function buildTaskInstruction(task) {
108
+ const assignerName = task.assigned_by_name || task.assigned_by;
109
+ return `ACTION REQUIRED: You have a task assigned by ${assignerName}.
110
+
111
+ Task: ${task.task}
112
+
113
+ Use accept_task with task_id="${task.id}" to accept and start working, or decline_task to decline with a reason.`;
114
+ }
70
115
  export function registerTools() {
71
116
  return [
72
117
  // Registration tools
@@ -106,6 +151,10 @@ export function registerTools() {
106
151
  type: "string",
107
152
  description: "Project name (optional)",
108
153
  },
154
+ task_id: {
155
+ type: "string",
156
+ description: "Optional: ID of a ticket system task to link this work to",
157
+ },
109
158
  },
110
159
  required: ["task"],
111
160
  },
@@ -399,6 +448,228 @@ export function registerTools() {
399
448
  required: ["task_id", "reason"],
400
449
  },
401
450
  },
451
+ // Ticket system tools (for project management integration)
452
+ {
453
+ name: "get_available_tasks",
454
+ description: "Get tasks from the ticket system that are available for you to claim and work on",
455
+ inputSchema: {
456
+ type: "object",
457
+ properties: {
458
+ status: {
459
+ type: "string",
460
+ description: "Filter by status (default: todo)",
461
+ enum: ["backlog", "todo", "in_progress"],
462
+ },
463
+ project_id: {
464
+ type: "string",
465
+ description: "Filter by project ID (optional)",
466
+ },
467
+ priority: {
468
+ type: "string",
469
+ description: "Filter by priority (optional)",
470
+ enum: ["lowest", "low", "medium", "high", "highest"],
471
+ },
472
+ required_type: {
473
+ type: "string",
474
+ description: "Filter by required skill type - matches your agent type",
475
+ enum: ["frontend", "backend", "android", "ios", "ai", "design-specs"],
476
+ },
477
+ match_my_type: {
478
+ type: "boolean",
479
+ description: "If true, only show tasks matching your registered agent type",
480
+ },
481
+ },
482
+ },
483
+ },
484
+ {
485
+ name: "get_task_context",
486
+ description: "Get full context for a ticket task including its parent story and epic",
487
+ inputSchema: {
488
+ type: "object",
489
+ properties: {
490
+ task_id: {
491
+ type: "string",
492
+ description: "ID of the task to get context for",
493
+ },
494
+ },
495
+ required: ["task_id"],
496
+ },
497
+ },
498
+ {
499
+ name: "claim_ticket_task",
500
+ description: "Claim a ticket task from the project board to work on",
501
+ inputSchema: {
502
+ type: "object",
503
+ properties: {
504
+ task_id: {
505
+ type: "string",
506
+ description: "ID of the task to claim",
507
+ },
508
+ },
509
+ required: ["task_id"],
510
+ },
511
+ },
512
+ {
513
+ name: "reach_checkpoint",
514
+ description: "Signal that you've reached a checkpoint and request human review before continuing",
515
+ inputSchema: {
516
+ type: "object",
517
+ properties: {
518
+ task_id: {
519
+ type: "string",
520
+ description: "ID of the task you're working on",
521
+ },
522
+ description: {
523
+ type: "string",
524
+ description: "Description of what you've accomplished and what you're waiting for review on",
525
+ },
526
+ },
527
+ required: ["task_id", "description"],
528
+ },
529
+ },
530
+ // Ticket creation tools
531
+ {
532
+ name: "create_epic",
533
+ description: "Create an epic (large body of work) in a project",
534
+ inputSchema: {
535
+ type: "object",
536
+ properties: {
537
+ project_id: {
538
+ type: "string",
539
+ description: "Project ID to create epic in",
540
+ },
541
+ title: {
542
+ type: "string",
543
+ description: "Epic title",
544
+ },
545
+ description: {
546
+ type: "string",
547
+ description: "Epic description (supports markdown)",
548
+ },
549
+ source_file: {
550
+ type: "string",
551
+ description: "Optional: path to OpenSpec file this came from",
552
+ },
553
+ },
554
+ required: ["project_id", "title"],
555
+ },
556
+ },
557
+ {
558
+ name: "create_story",
559
+ description: "Create a story under an epic",
560
+ inputSchema: {
561
+ type: "object",
562
+ properties: {
563
+ project_id: {
564
+ type: "string",
565
+ description: "Project ID",
566
+ },
567
+ epic_id: {
568
+ type: "string",
569
+ description: "Epic ID to add story to",
570
+ },
571
+ title: {
572
+ type: "string",
573
+ description: "Story title",
574
+ },
575
+ description: {
576
+ type: "string",
577
+ description: "Story description",
578
+ },
579
+ required_type: {
580
+ type: "string",
581
+ enum: ["frontend", "backend", "android", "ios", "ai", "design-specs"],
582
+ description: "Required skill type for this story",
583
+ },
584
+ labels: {
585
+ type: "array",
586
+ items: { type: "string" },
587
+ description: "Additional labels",
588
+ },
589
+ },
590
+ required: ["project_id", "title"],
591
+ },
592
+ },
593
+ {
594
+ name: "create_task",
595
+ description: "Create a task under a story",
596
+ inputSchema: {
597
+ type: "object",
598
+ properties: {
599
+ project_id: {
600
+ type: "string",
601
+ description: "Project ID",
602
+ },
603
+ story_id: {
604
+ type: "string",
605
+ description: "Story ID to add task to",
606
+ },
607
+ title: {
608
+ type: "string",
609
+ description: "Task title",
610
+ },
611
+ description: {
612
+ type: "string",
613
+ description: "Task description",
614
+ },
615
+ required_type: {
616
+ type: "string",
617
+ enum: ["frontend", "backend", "android", "ios", "ai", "design-specs"],
618
+ description: "Required skill type",
619
+ },
620
+ },
621
+ required: ["project_id", "title"],
622
+ },
623
+ },
624
+ {
625
+ name: "upload_attachment",
626
+ description: "Upload a file (screenshot, log, etc.) as proof of work to a ticket",
627
+ inputSchema: {
628
+ type: "object",
629
+ properties: {
630
+ ticket_id: {
631
+ type: "string",
632
+ description: "Ticket ID to attach file to",
633
+ },
634
+ ticket_type: {
635
+ type: "string",
636
+ enum: ["epic", "story", "task"],
637
+ description: "Type of ticket",
638
+ },
639
+ ticket_key: {
640
+ type: "string",
641
+ description: "Ticket key (e.g., PROJ-T42)",
642
+ },
643
+ file_path: {
644
+ type: "string",
645
+ description: "Local path to file to upload",
646
+ },
647
+ description: {
648
+ type: "string",
649
+ description: "Optional description of the file",
650
+ },
651
+ },
652
+ required: ["ticket_id", "ticket_type", "ticket_key", "file_path"],
653
+ },
654
+ },
655
+ {
656
+ name: "add_comment",
657
+ description: "Add a comment to any ticket (epic, story, task)",
658
+ inputSchema: {
659
+ type: "object",
660
+ properties: {
661
+ ticket_id: {
662
+ type: "string",
663
+ description: "Ticket ID to comment on",
664
+ },
665
+ body: {
666
+ type: "string",
667
+ description: "Comment body (supports markdown)",
668
+ },
669
+ },
670
+ required: ["ticket_id", "body"],
671
+ },
672
+ },
402
673
  ];
403
674
  }
404
675
  export async function handleToolCall(name, args, client, context) {
@@ -445,38 +716,71 @@ export async function handleToolCall(name, args, client, context) {
445
716
  }
446
717
  }
447
718
  }
448
- // Fresh registration
449
- // Auto-detect model from environment or use provided value
450
- const model = args.model ||
451
- process.env.CLAUDE_MODEL ||
452
- "claude-opus-4-5-20251101";
453
- const result = await client.registerAgent(requestedId, args.name, owner, workingDir, model);
454
- context.setCurrentAgentId(requestedId);
455
- // Save state for future reconnection (including name)
719
+ // Fresh registration - use browser-based flow
720
+ // Step 1: Initialize registration session
721
+ const initResult = await client.initRegistration(requestedId, args.name);
722
+ // Step 2: Open browser to dashboard
723
+ const dashboardUrl = client.getBaseUrl().replace('/api', '') + initResult.dashboard_url;
724
+ console.error(`Opening browser for agent registration: ${dashboardUrl}`);
725
+ await open(dashboardUrl);
726
+ // Step 3: Poll for completion (max 10 minutes)
727
+ const pollInterval = 3000; // 3 seconds
728
+ const maxPolls = 200; // 10 minutes
729
+ let pollCount = 0;
730
+ let registrationResult = null;
731
+ console.error('Waiting for browser registration to complete...');
732
+ while (pollCount < maxPolls) {
733
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
734
+ pollCount++;
735
+ try {
736
+ const pollResult = await client.pollRegistrationCallback(initResult.session_token);
737
+ if (pollResult.status === 'completed') {
738
+ registrationResult = pollResult;
739
+ break;
740
+ }
741
+ else if (pollResult.status === 'expired') {
742
+ throw new Error('Registration session expired. Please try again.');
743
+ }
744
+ // status === 'pending' - continue polling
745
+ }
746
+ catch (error) {
747
+ // Non-fatal errors during polling - continue
748
+ console.warn('Poll error:', error);
749
+ }
750
+ }
751
+ if (!registrationResult || registrationResult.status !== 'completed') {
752
+ throw new Error('Registration timed out. Please try again.');
753
+ }
754
+ const agentId = registrationResult.agent_id;
755
+ context.setCurrentAgentId(agentId);
756
+ // Save state for future reconnection
456
757
  state.saveState(workingDir, {
457
- agent_id: requestedId,
458
- name: result.name,
758
+ agent_id: agentId,
759
+ name: registrationResult.agent_name || agentId,
459
760
  owner,
460
- token: result.token,
461
- registered_at: result.registered_at,
761
+ token: registrationResult.connect_token,
762
+ agent_type: registrationResult.agent_type,
763
+ registered_at: new Date().toISOString(),
462
764
  });
463
765
  return {
464
- ...result,
465
- mode: "registered",
466
- message: `Registered as ${result.name || requestedId}. You have ${result.pending_tasks_count} pending tasks and ${result.unread_messages_count} unread messages.`,
766
+ agent_id: agentId,
767
+ name: registrationResult.agent_name,
768
+ agent_type: registrationResult.agent_type,
769
+ mode: 'registered',
770
+ message: `Successfully registered as ${registrationResult.agent_name || agentId}. You can now use other AgentHub tools.`,
467
771
  };
468
772
  }
469
773
  case "agent_start_work": {
470
774
  if (!agentId)
471
775
  throw new Error("Not registered. Call agent_register first.");
472
- const result = await client.startWork(agentId, args.task, args.project);
473
- return wrapWithMessages(client, agentId, result);
776
+ const result = await client.startWork(agentId, args.task, args.project, args.task_id);
777
+ return wrapWithPendingItems(client, agentId, result, context);
474
778
  }
475
779
  case "agent_set_status": {
476
780
  if (!agentId)
477
781
  throw new Error("Not registered. Call agent_register first.");
478
782
  const result = await client.heartbeat(agentId, args.status);
479
- return wrapWithMessages(client, agentId, result);
783
+ return wrapWithPendingItems(client, agentId, result, context);
480
784
  }
481
785
  case "agent_disconnect": {
482
786
  // No wrapping - agent is going offline
@@ -497,7 +801,7 @@ export async function handleToolCall(name, args, client, context) {
497
801
  body: args.body,
498
802
  priority: args.priority,
499
803
  });
500
- return wrapWithMessages(client, agentId, result);
804
+ return wrapWithPendingItems(client, agentId, result, context);
501
805
  }
502
806
  case "send_to_channel": {
503
807
  if (!agentId)
@@ -509,7 +813,7 @@ export async function handleToolCall(name, args, client, context) {
509
813
  subject: args.subject,
510
814
  body: args.body,
511
815
  });
512
- return wrapWithMessages(client, agentId, result);
816
+ return wrapWithPendingItems(client, agentId, result, context);
513
817
  }
514
818
  case "broadcast": {
515
819
  if (!agentId)
@@ -521,7 +825,7 @@ export async function handleToolCall(name, args, client, context) {
521
825
  subject: args.subject,
522
826
  body: args.body,
523
827
  });
524
- return wrapWithMessages(client, agentId, result);
828
+ return wrapWithPendingItems(client, agentId, result, context);
525
829
  }
526
830
  case "check_inbox": {
527
831
  // No wrapping - already fetching messages
@@ -531,38 +835,38 @@ export async function handleToolCall(name, args, client, context) {
531
835
  }
532
836
  case "mark_read": {
533
837
  const result = await client.markRead(args.message_id);
534
- return wrapWithMessages(client, agentId, result);
838
+ return wrapWithPendingItems(client, agentId, result, context);
535
839
  }
536
840
  case "reply": {
537
841
  if (!agentId)
538
842
  throw new Error("Not registered. Call agent_register first.");
539
843
  const result = await client.reply(args.message_id, agentId, args.body);
540
- return wrapWithMessages(client, agentId, result);
844
+ return wrapWithPendingItems(client, agentId, result, context);
541
845
  }
542
846
  // Discovery
543
847
  case "list_agents": {
544
848
  const result = await client.listAgents(args.status);
545
- return wrapWithMessages(client, agentId, result);
849
+ return wrapWithPendingItems(client, agentId, result, context);
546
850
  }
547
851
  case "get_agent": {
548
852
  const result = await client.getAgent(args.id);
549
- return wrapWithMessages(client, agentId, result);
853
+ return wrapWithPendingItems(client, agentId, result, context);
550
854
  }
551
855
  case "list_channels": {
552
856
  const result = await client.listChannels();
553
- return wrapWithMessages(client, agentId, result);
857
+ return wrapWithPendingItems(client, agentId, result, context);
554
858
  }
555
859
  case "join_channel": {
556
860
  if (!agentId)
557
861
  throw new Error("Not registered. Call agent_register first.");
558
862
  const result = await client.joinChannel(args.channel, agentId);
559
- return wrapWithMessages(client, agentId, result);
863
+ return wrapWithPendingItems(client, agentId, result, context);
560
864
  }
561
865
  case "leave_channel": {
562
866
  if (!agentId)
563
867
  throw new Error("Not registered. Call agent_register first.");
564
868
  const result = await client.leaveChannel(args.channel, agentId);
565
- return wrapWithMessages(client, agentId, result);
869
+ return wrapWithPendingItems(client, agentId, result, context);
566
870
  }
567
871
  // Task completion
568
872
  case "agent_complete_task": {
@@ -575,25 +879,131 @@ export async function handleToolCall(name, args, client, context) {
575
879
  time_spent: args.time_spent,
576
880
  next_steps: args.next_steps,
577
881
  });
578
- return wrapWithMessages(client, agentId, result);
882
+ return wrapWithPendingItems(client, agentId, result, context);
579
883
  }
580
884
  case "get_pending_tasks": {
581
885
  if (!agentId)
582
886
  throw new Error("Not registered. Call agent_register first.");
583
887
  const result = await client.getPendingTasks(agentId);
584
- return wrapWithMessages(client, agentId, result);
888
+ return wrapWithPendingItems(client, agentId, result, context);
585
889
  }
586
890
  case "accept_task": {
587
891
  if (!agentId)
588
892
  throw new Error("Not registered. Call agent_register first.");
589
893
  const result = await client.acceptTask(agentId, args.task_id);
590
- return wrapWithMessages(client, agentId, result);
894
+ return wrapWithPendingItems(client, agentId, result, context);
591
895
  }
592
896
  case "decline_task": {
593
897
  if (!agentId)
594
898
  throw new Error("Not registered. Call agent_register first.");
595
899
  const result = await client.declineTask(agentId, args.task_id, args.reason);
596
- return wrapWithMessages(client, agentId, result);
900
+ return wrapWithPendingItems(client, agentId, result, context);
901
+ }
902
+ // Ticket system tools
903
+ case "get_available_tasks": {
904
+ if (!agentId)
905
+ throw new Error("Not registered. Call agent_register first.");
906
+ let requiredType = args.required_type;
907
+ // If match_my_type is true, get agent's type from state
908
+ if (args.match_my_type === true) {
909
+ const workingDir = context.getWorkingDir();
910
+ const agentState = state.loadState(workingDir);
911
+ // Use stored agent_type if available, otherwise keep the explicit required_type
912
+ requiredType = requiredType || agentState?.agent_type;
913
+ }
914
+ const result = await client.getAvailableTasks({
915
+ status: args.status,
916
+ project_id: args.project_id,
917
+ priority: args.priority,
918
+ required_type: requiredType,
919
+ });
920
+ return wrapWithPendingItems(client, agentId, result, context);
921
+ }
922
+ case "get_task_context": {
923
+ if (!agentId)
924
+ throw new Error("Not registered. Call agent_register first.");
925
+ const result = await client.getTicketTask(args.task_id);
926
+ return wrapWithPendingItems(client, agentId, result, context);
927
+ }
928
+ case "claim_ticket_task": {
929
+ if (!agentId)
930
+ throw new Error("Not registered. Call agent_register first.");
931
+ const result = await client.claimTicketTask(args.task_id, agentId);
932
+ return wrapWithPendingItems(client, agentId, result, context);
933
+ }
934
+ case "reach_checkpoint": {
935
+ if (!agentId)
936
+ throw new Error("Not registered. Call agent_register first.");
937
+ const result = await client.createCheckpoint(args.task_id, agentId, args.description);
938
+ return wrapWithPendingItems(client, agentId, result, context);
939
+ }
940
+ // Ticket creation tools
941
+ case "create_epic": {
942
+ const result = await client.createEpic({
943
+ project_id: args.project_id,
944
+ title: args.title,
945
+ description: args.description,
946
+ source_file: args.source_file,
947
+ });
948
+ return {
949
+ ...result,
950
+ message: `Created epic ${result.key}: ${args.title}`,
951
+ };
952
+ }
953
+ case "create_story": {
954
+ const result = await client.createStory({
955
+ project_id: args.project_id,
956
+ epic_id: args.epic_id,
957
+ title: args.title,
958
+ description: args.description,
959
+ required_type: args.required_type,
960
+ labels: args.labels,
961
+ });
962
+ return {
963
+ ...result,
964
+ message: `Created story ${result.key}: ${args.title}`,
965
+ };
966
+ }
967
+ case "create_task": {
968
+ const result = await client.createTask({
969
+ project_id: args.project_id,
970
+ story_id: args.story_id,
971
+ title: args.title,
972
+ description: args.description,
973
+ required_type: args.required_type,
974
+ });
975
+ return {
976
+ ...result,
977
+ message: `Created task ${result.key}: ${args.title}`,
978
+ };
979
+ }
980
+ case "upload_attachment": {
981
+ if (!agentId)
982
+ throw new Error("Not registered. Call agent_register first.");
983
+ const result = await client.uploadAttachment({
984
+ ticket_id: args.ticket_id,
985
+ ticket_type: args.ticket_type,
986
+ ticket_key: args.ticket_key,
987
+ file_path: args.file_path,
988
+ description: args.description,
989
+ agent_id: agentId,
990
+ });
991
+ return {
992
+ ...result,
993
+ message: `Uploaded ${result.filename} to ${args.ticket_key}`,
994
+ };
995
+ }
996
+ case "add_comment": {
997
+ if (!agentId)
998
+ throw new Error("Not registered. Call agent_register first.");
999
+ const result = await client.addComment({
1000
+ ticket_id: args.ticket_id,
1001
+ body: args.body,
1002
+ });
1003
+ return {
1004
+ ...result,
1005
+ message: `Added comment to ticket`,
1006
+ };
597
1007
  }
598
1008
  default:
599
1009
  throw new Error(`Unknown tool: ${name}`);