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,49 +2,92 @@
2
2
  * MCP tool definitions and handlers
3
3
  */
4
4
 
5
- import { ApiClient, Message } from "../client.js";
5
+ import { ApiClient, Message, TicketTask, TicketTaskContext, CheckpointResponse } from "../client.js";
6
6
  import type { Tool } from "@modelcontextprotocol/sdk/types.js";
7
7
  import * as state from "../state.js";
8
+ import open from "open";
9
+
10
+ export interface PushedItems {
11
+ tasks: PendingTask[];
12
+ messages: Message[];
13
+ }
8
14
 
9
15
  export interface ToolContext {
10
16
  getCurrentAgentId: () => string;
11
17
  setCurrentAgentId: (id: string) => void;
12
18
  stopHeartbeat: () => void;
13
19
  getWorkingDir: () => string;
20
+ getPushedItems: () => PushedItems;
21
+ }
22
+
23
+ interface PendingTask {
24
+ id: string;
25
+ assigned_by: string;
26
+ assigned_by_name?: string;
27
+ task: string;
28
+ priority: string;
29
+ created_at: string;
14
30
  }
15
31
 
16
32
  interface UrgentAction {
17
33
  required: true;
18
34
  instruction: string;
19
- message: Message;
35
+ message?: Message;
36
+ task?: PendingTask;
20
37
  }
21
38
 
22
39
  interface WrappedResponse {
23
40
  result: unknown;
24
41
  pending_messages: Message[];
25
- pending_count: number;
26
- has_more: boolean;
42
+ pending_messages_count: number;
43
+ pending_tasks: PendingTask[];
44
+ pending_tasks_count: number;
27
45
  urgent_action: UrgentAction | null;
28
46
  }
29
47
 
30
48
  /**
31
- * Wraps a tool response with pending messages
32
- * This enables automatic message delivery without extra API calls
49
+ * Wraps a tool response with pending messages and tasks
50
+ * This enables automatic delivery without extra API calls
51
+ * Includes items pushed via WebSocket
33
52
  */
34
- async function wrapWithMessages(
53
+ async function wrapWithPendingItems(
35
54
  client: ApiClient,
36
55
  agentId: string | null,
37
- result: unknown
56
+ result: unknown,
57
+ context?: ToolContext
38
58
  ): Promise<WrappedResponse | unknown> {
39
59
  // If not registered, return raw result
40
60
  if (!agentId) return result;
41
61
 
42
62
  try {
43
- // Fetch unread messages
44
- const inbox = await client.getInbox(agentId, true, 10);
45
- const messages = inbox.messages || [];
63
+ // Get items pushed via WebSocket (if connected)
64
+ const pushedItems = context?.getPushedItems() || { tasks: [], messages: [] };
65
+
66
+ // Fetch unread messages and pending tasks in parallel
67
+ const [inbox, tasksResponse] = await Promise.all([
68
+ client.getInbox(agentId, true, 10),
69
+ client.getPendingTasks(agentId),
70
+ ]);
71
+
72
+ // Merge pushed items with fetched items (deduplicating by ID)
73
+ const fetchedMessages = inbox.messages || [];
74
+ const fetchedTasks = (tasksResponse.tasks || []) as PendingTask[];
75
+
76
+ // Deduplicate messages
77
+ const messageIds = new Set(fetchedMessages.map((m) => m.id));
78
+ const messages = [
79
+ ...fetchedMessages,
80
+ ...pushedItems.messages.filter((m) => !messageIds.has(m.id)),
81
+ ];
46
82
 
47
- // Sort: urgent first, then by created_at
83
+ // Deduplicate tasks
84
+ const taskIds = new Set(fetchedTasks.map((t) => t.id));
85
+ const tasks = [
86
+ ...fetchedTasks,
87
+ ...pushedItems.tasks.filter((t) => !taskIds.has(t.id)),
88
+ ];
89
+
90
+ // Sort messages: urgent first, then by created_at
48
91
  messages.sort((a, b) => {
49
92
  const aUrgent = isUrgentMessage(a);
50
93
  const bUrgent = isUrgentMessage(b);
@@ -53,28 +96,40 @@ async function wrapWithMessages(
53
96
  return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
54
97
  });
55
98
 
56
- // Find urgent message for action
57
- const urgentMsg = messages.find(isUrgentMessage);
58
-
99
+ // Determine urgent action - tasks take priority over messages
59
100
  let urgentAction: UrgentAction | null = null;
60
- if (urgentMsg) {
101
+
102
+ // Check for pending tasks first (they're actionable)
103
+ if (tasks.length > 0) {
104
+ const highPriorityTask = tasks.find((t) => t.priority === "high") || tasks[0];
61
105
  urgentAction = {
62
106
  required: true,
63
- instruction: buildUrgentInstruction(urgentMsg),
64
- message: urgentMsg,
107
+ instruction: buildTaskInstruction(highPriorityTask),
108
+ task: highPriorityTask,
65
109
  };
110
+ } else {
111
+ // Check for urgent messages
112
+ const urgentMsg = messages.find(isUrgentMessage);
113
+ if (urgentMsg) {
114
+ urgentAction = {
115
+ required: true,
116
+ instruction: buildUrgentInstruction(urgentMsg),
117
+ message: urgentMsg,
118
+ };
119
+ }
66
120
  }
67
121
 
68
122
  return {
69
123
  result,
70
124
  pending_messages: messages,
71
- pending_count: inbox.total,
72
- has_more: inbox.total > messages.length,
125
+ pending_messages_count: messages.length,
126
+ pending_tasks: tasks,
127
+ pending_tasks_count: tasks.length,
73
128
  urgent_action: urgentAction,
74
129
  };
75
130
  } catch (error) {
76
- // Don't break tool call if message fetch fails
77
- console.warn("Failed to fetch messages for wrapping:", error);
131
+ // Don't break tool call if fetch fails
132
+ console.warn("Failed to fetch pending items for wrapping:", error);
78
133
  return result;
79
134
  }
80
135
  }
@@ -102,6 +157,18 @@ Details: ${msg.body}
102
157
  Spawn a subagent with Task tool to research and respond to this query. Use send_message to reply when done.`;
103
158
  }
104
159
 
160
+ /**
161
+ * Build instruction text for pending tasks
162
+ */
163
+ function buildTaskInstruction(task: PendingTask): string {
164
+ const assignerName = task.assigned_by_name || task.assigned_by;
165
+ return `ACTION REQUIRED: You have a task assigned by ${assignerName}.
166
+
167
+ Task: ${task.task}
168
+
169
+ Use accept_task with task_id="${task.id}" to accept and start working, or decline_task to decline with a reason.`;
170
+ }
171
+
105
172
  export function registerTools(): Tool[] {
106
173
  return [
107
174
  // Registration tools
@@ -141,6 +208,10 @@ export function registerTools(): Tool[] {
141
208
  type: "string",
142
209
  description: "Project name (optional)",
143
210
  },
211
+ task_id: {
212
+ type: "string",
213
+ description: "Optional: ID of a ticket system task to link this work to",
214
+ },
144
215
  },
145
216
  required: ["task"],
146
217
  },
@@ -439,6 +510,230 @@ export function registerTools(): Tool[] {
439
510
  required: ["task_id", "reason"],
440
511
  },
441
512
  },
513
+
514
+ // Ticket system tools (for project management integration)
515
+ {
516
+ name: "get_available_tasks",
517
+ description: "Get tasks from the ticket system that are available for you to claim and work on",
518
+ inputSchema: {
519
+ type: "object",
520
+ properties: {
521
+ status: {
522
+ type: "string",
523
+ description: "Filter by status (default: todo)",
524
+ enum: ["backlog", "todo", "in_progress"],
525
+ },
526
+ project_id: {
527
+ type: "string",
528
+ description: "Filter by project ID (optional)",
529
+ },
530
+ priority: {
531
+ type: "string",
532
+ description: "Filter by priority (optional)",
533
+ enum: ["lowest", "low", "medium", "high", "highest"],
534
+ },
535
+ required_type: {
536
+ type: "string",
537
+ description: "Filter by required skill type - matches your agent type",
538
+ enum: ["frontend", "backend", "android", "ios", "ai", "design-specs"],
539
+ },
540
+ match_my_type: {
541
+ type: "boolean",
542
+ description: "If true, only show tasks matching your registered agent type",
543
+ },
544
+ },
545
+ },
546
+ },
547
+ {
548
+ name: "get_task_context",
549
+ description: "Get full context for a ticket task including its parent story and epic",
550
+ inputSchema: {
551
+ type: "object",
552
+ properties: {
553
+ task_id: {
554
+ type: "string",
555
+ description: "ID of the task to get context for",
556
+ },
557
+ },
558
+ required: ["task_id"],
559
+ },
560
+ },
561
+ {
562
+ name: "claim_ticket_task",
563
+ description: "Claim a ticket task from the project board to work on",
564
+ inputSchema: {
565
+ type: "object",
566
+ properties: {
567
+ task_id: {
568
+ type: "string",
569
+ description: "ID of the task to claim",
570
+ },
571
+ },
572
+ required: ["task_id"],
573
+ },
574
+ },
575
+ {
576
+ name: "reach_checkpoint",
577
+ description: "Signal that you've reached a checkpoint and request human review before continuing",
578
+ inputSchema: {
579
+ type: "object",
580
+ properties: {
581
+ task_id: {
582
+ type: "string",
583
+ description: "ID of the task you're working on",
584
+ },
585
+ description: {
586
+ type: "string",
587
+ description: "Description of what you've accomplished and what you're waiting for review on",
588
+ },
589
+ },
590
+ required: ["task_id", "description"],
591
+ },
592
+ },
593
+
594
+ // Ticket creation tools
595
+ {
596
+ name: "create_epic",
597
+ description: "Create an epic (large body of work) in a project",
598
+ inputSchema: {
599
+ type: "object",
600
+ properties: {
601
+ project_id: {
602
+ type: "string",
603
+ description: "Project ID to create epic in",
604
+ },
605
+ title: {
606
+ type: "string",
607
+ description: "Epic title",
608
+ },
609
+ description: {
610
+ type: "string",
611
+ description: "Epic description (supports markdown)",
612
+ },
613
+ source_file: {
614
+ type: "string",
615
+ description: "Optional: path to OpenSpec file this came from",
616
+ },
617
+ },
618
+ required: ["project_id", "title"],
619
+ },
620
+ },
621
+ {
622
+ name: "create_story",
623
+ description: "Create a story under an epic",
624
+ inputSchema: {
625
+ type: "object",
626
+ properties: {
627
+ project_id: {
628
+ type: "string",
629
+ description: "Project ID",
630
+ },
631
+ epic_id: {
632
+ type: "string",
633
+ description: "Epic ID to add story to",
634
+ },
635
+ title: {
636
+ type: "string",
637
+ description: "Story title",
638
+ },
639
+ description: {
640
+ type: "string",
641
+ description: "Story description",
642
+ },
643
+ required_type: {
644
+ type: "string",
645
+ enum: ["frontend", "backend", "android", "ios", "ai", "design-specs"],
646
+ description: "Required skill type for this story",
647
+ },
648
+ labels: {
649
+ type: "array",
650
+ items: { type: "string" },
651
+ description: "Additional labels",
652
+ },
653
+ },
654
+ required: ["project_id", "title"],
655
+ },
656
+ },
657
+ {
658
+ name: "create_task",
659
+ description: "Create a task under a story",
660
+ inputSchema: {
661
+ type: "object",
662
+ properties: {
663
+ project_id: {
664
+ type: "string",
665
+ description: "Project ID",
666
+ },
667
+ story_id: {
668
+ type: "string",
669
+ description: "Story ID to add task to",
670
+ },
671
+ title: {
672
+ type: "string",
673
+ description: "Task title",
674
+ },
675
+ description: {
676
+ type: "string",
677
+ description: "Task description",
678
+ },
679
+ required_type: {
680
+ type: "string",
681
+ enum: ["frontend", "backend", "android", "ios", "ai", "design-specs"],
682
+ description: "Required skill type",
683
+ },
684
+ },
685
+ required: ["project_id", "title"],
686
+ },
687
+ },
688
+ {
689
+ name: "upload_attachment",
690
+ description: "Upload a file (screenshot, log, etc.) as proof of work to a ticket",
691
+ inputSchema: {
692
+ type: "object",
693
+ properties: {
694
+ ticket_id: {
695
+ type: "string",
696
+ description: "Ticket ID to attach file to",
697
+ },
698
+ ticket_type: {
699
+ type: "string",
700
+ enum: ["epic", "story", "task"],
701
+ description: "Type of ticket",
702
+ },
703
+ ticket_key: {
704
+ type: "string",
705
+ description: "Ticket key (e.g., PROJ-T42)",
706
+ },
707
+ file_path: {
708
+ type: "string",
709
+ description: "Local path to file to upload",
710
+ },
711
+ description: {
712
+ type: "string",
713
+ description: "Optional description of the file",
714
+ },
715
+ },
716
+ required: ["ticket_id", "ticket_type", "ticket_key", "file_path"],
717
+ },
718
+ },
719
+ {
720
+ name: "add_comment",
721
+ description: "Add a comment to any ticket (epic, story, task)",
722
+ inputSchema: {
723
+ type: "object",
724
+ properties: {
725
+ ticket_id: {
726
+ type: "string",
727
+ description: "Ticket ID to comment on",
728
+ },
729
+ body: {
730
+ type: "string",
731
+ description: "Comment body (supports markdown)",
732
+ },
733
+ },
734
+ required: ["ticket_id", "body"],
735
+ },
736
+ },
442
737
  ];
443
738
  }
444
739
 
@@ -503,35 +798,69 @@ export async function handleToolCall(
503
798
  }
504
799
  }
505
800
 
506
- // Fresh registration
507
- // Auto-detect model from environment or use provided value
508
- const model = args.model as string ||
509
- process.env.CLAUDE_MODEL ||
510
- "claude-opus-4-5-20251101";
511
-
512
- const result = await client.registerAgent(
801
+ // Fresh registration - use browser-based flow
802
+ // Step 1: Initialize registration session
803
+ const initResult = await client.initRegistration(
513
804
  requestedId,
514
- args.name as string | undefined,
515
- owner,
516
- workingDir,
517
- model
805
+ args.name as string | undefined
518
806
  );
519
807
 
520
- context.setCurrentAgentId(requestedId);
808
+ // Step 2: Open browser to dashboard
809
+ const dashboardUrl = client.getBaseUrl().replace('/api', '') + initResult.dashboard_url;
810
+ console.error(`Opening browser for agent registration: ${dashboardUrl}`);
811
+ await open(dashboardUrl);
812
+
813
+ // Step 3: Poll for completion (max 10 minutes)
814
+ const pollInterval = 3000; // 3 seconds
815
+ const maxPolls = 200; // 10 minutes
816
+ let pollCount = 0;
817
+ let registrationResult: Awaited<ReturnType<typeof client.pollRegistrationCallback>> | null = null;
818
+
819
+ console.error('Waiting for browser registration to complete...');
820
+
821
+ while (pollCount < maxPolls) {
822
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
823
+ pollCount++;
824
+
825
+ try {
826
+ const pollResult = await client.pollRegistrationCallback(initResult.session_token);
827
+
828
+ if (pollResult.status === 'completed') {
829
+ registrationResult = pollResult;
830
+ break;
831
+ } else if (pollResult.status === 'expired') {
832
+ throw new Error('Registration session expired. Please try again.');
833
+ }
834
+ // status === 'pending' - continue polling
835
+ } catch (error) {
836
+ // Non-fatal errors during polling - continue
837
+ console.warn('Poll error:', error);
838
+ }
839
+ }
840
+
841
+ if (!registrationResult || registrationResult.status !== 'completed') {
842
+ throw new Error('Registration timed out. Please try again.');
843
+ }
844
+
845
+ const agentId = registrationResult.agent_id!;
846
+ context.setCurrentAgentId(agentId);
521
847
 
522
- // Save state for future reconnection (including name)
848
+ // Save state for future reconnection
523
849
  state.saveState(workingDir, {
524
- agent_id: requestedId,
525
- name: result.name,
850
+ agent_id: agentId,
851
+ name: registrationResult.agent_name || agentId,
526
852
  owner,
527
- token: result.token,
528
- registered_at: result.registered_at,
853
+ token: registrationResult.connect_token!,
854
+ agent_type: registrationResult.agent_type,
855
+ registered_at: new Date().toISOString(),
529
856
  });
530
857
 
531
858
  return {
532
- ...result,
533
- mode: "registered",
534
- message: `Registered as ${result.name || requestedId}. You have ${result.pending_tasks_count} pending tasks and ${result.unread_messages_count} unread messages.`,
859
+ agent_id: agentId,
860
+ name: registrationResult.agent_name,
861
+ agent_type: registrationResult.agent_type,
862
+ mode: 'registered',
863
+ message: `Successfully registered as ${registrationResult.agent_name || agentId}. You can now use other AgentHub tools.`,
535
864
  };
536
865
  }
537
866
 
@@ -540,15 +869,16 @@ export async function handleToolCall(
540
869
  const result = await client.startWork(
541
870
  agentId,
542
871
  args.task as string,
543
- args.project as string | undefined
872
+ args.project as string | undefined,
873
+ args.task_id as string | undefined
544
874
  );
545
- return wrapWithMessages(client, agentId, result);
875
+ return wrapWithPendingItems(client, agentId, result, context);
546
876
  }
547
877
 
548
878
  case "agent_set_status": {
549
879
  if (!agentId) throw new Error("Not registered. Call agent_register first.");
550
880
  const result = await client.heartbeat(agentId, args.status as "online" | "busy");
551
- return wrapWithMessages(client, agentId, result);
881
+ return wrapWithPendingItems(client, agentId, result, context);
552
882
  }
553
883
 
554
884
  case "agent_disconnect": {
@@ -569,7 +899,7 @@ export async function handleToolCall(
569
899
  body: args.body as string,
570
900
  priority: args.priority as string | undefined,
571
901
  });
572
- return wrapWithMessages(client, agentId, result);
902
+ return wrapWithPendingItems(client, agentId, result, context);
573
903
  }
574
904
 
575
905
  case "send_to_channel": {
@@ -581,7 +911,7 @@ export async function handleToolCall(
581
911
  subject: args.subject as string,
582
912
  body: args.body as string,
583
913
  });
584
- return wrapWithMessages(client, agentId, result);
914
+ return wrapWithPendingItems(client, agentId, result, context);
585
915
  }
586
916
 
587
917
  case "broadcast": {
@@ -593,7 +923,7 @@ export async function handleToolCall(
593
923
  subject: args.subject as string,
594
924
  body: args.body as string,
595
925
  });
596
- return wrapWithMessages(client, agentId, result);
926
+ return wrapWithPendingItems(client, agentId, result, context);
597
927
  }
598
928
 
599
929
  case "check_inbox": {
@@ -604,41 +934,41 @@ export async function handleToolCall(
604
934
 
605
935
  case "mark_read": {
606
936
  const result = await client.markRead(args.message_id as string);
607
- return wrapWithMessages(client, agentId, result);
937
+ return wrapWithPendingItems(client, agentId, result, context);
608
938
  }
609
939
 
610
940
  case "reply": {
611
941
  if (!agentId) throw new Error("Not registered. Call agent_register first.");
612
942
  const result = await client.reply(args.message_id as string, agentId, args.body as string);
613
- return wrapWithMessages(client, agentId, result);
943
+ return wrapWithPendingItems(client, agentId, result, context);
614
944
  }
615
945
 
616
946
  // Discovery
617
947
  case "list_agents": {
618
948
  const result = await client.listAgents(args.status as string | undefined);
619
- return wrapWithMessages(client, agentId, result);
949
+ return wrapWithPendingItems(client, agentId, result, context);
620
950
  }
621
951
 
622
952
  case "get_agent": {
623
953
  const result = await client.getAgent(args.id as string);
624
- return wrapWithMessages(client, agentId, result);
954
+ return wrapWithPendingItems(client, agentId, result, context);
625
955
  }
626
956
 
627
957
  case "list_channels": {
628
958
  const result = await client.listChannels();
629
- return wrapWithMessages(client, agentId, result);
959
+ return wrapWithPendingItems(client, agentId, result, context);
630
960
  }
631
961
 
632
962
  case "join_channel": {
633
963
  if (!agentId) throw new Error("Not registered. Call agent_register first.");
634
964
  const result = await client.joinChannel(args.channel as string, agentId);
635
- return wrapWithMessages(client, agentId, result);
965
+ return wrapWithPendingItems(client, agentId, result, context);
636
966
  }
637
967
 
638
968
  case "leave_channel": {
639
969
  if (!agentId) throw new Error("Not registered. Call agent_register first.");
640
970
  const result = await client.leaveChannel(args.channel as string, agentId);
641
- return wrapWithMessages(client, agentId, result);
971
+ return wrapWithPendingItems(client, agentId, result, context);
642
972
  }
643
973
 
644
974
  // Task completion
@@ -651,25 +981,141 @@ export async function handleToolCall(
651
981
  time_spent: args.time_spent as string | undefined,
652
982
  next_steps: args.next_steps as string | undefined,
653
983
  });
654
- return wrapWithMessages(client, agentId, result);
984
+ return wrapWithPendingItems(client, agentId, result, context);
655
985
  }
656
986
 
657
987
  case "get_pending_tasks": {
658
988
  if (!agentId) throw new Error("Not registered. Call agent_register first.");
659
989
  const result = await client.getPendingTasks(agentId);
660
- return wrapWithMessages(client, agentId, result);
990
+ return wrapWithPendingItems(client, agentId, result, context);
661
991
  }
662
992
 
663
993
  case "accept_task": {
664
994
  if (!agentId) throw new Error("Not registered. Call agent_register first.");
665
995
  const result = await client.acceptTask(agentId, args.task_id as string);
666
- return wrapWithMessages(client, agentId, result);
996
+ return wrapWithPendingItems(client, agentId, result, context);
667
997
  }
668
998
 
669
999
  case "decline_task": {
670
1000
  if (!agentId) throw new Error("Not registered. Call agent_register first.");
671
1001
  const result = await client.declineTask(agentId, args.task_id as string, args.reason as string);
672
- return wrapWithMessages(client, agentId, result);
1002
+ return wrapWithPendingItems(client, agentId, result, context);
1003
+ }
1004
+
1005
+ // Ticket system tools
1006
+ case "get_available_tasks": {
1007
+ if (!agentId) throw new Error("Not registered. Call agent_register first.");
1008
+
1009
+ let requiredType = args.required_type as string | undefined;
1010
+
1011
+ // If match_my_type is true, get agent's type from state
1012
+ if (args.match_my_type === true) {
1013
+ const workingDir = context.getWorkingDir();
1014
+ const agentState = state.loadState(workingDir);
1015
+ // Use stored agent_type if available, otherwise keep the explicit required_type
1016
+ requiredType = requiredType || agentState?.agent_type;
1017
+ }
1018
+
1019
+ const result = await client.getAvailableTasks({
1020
+ status: args.status as string | undefined,
1021
+ project_id: args.project_id as string | undefined,
1022
+ priority: args.priority as string | undefined,
1023
+ required_type: requiredType,
1024
+ });
1025
+ return wrapWithPendingItems(client, agentId, result, context);
1026
+ }
1027
+
1028
+ case "get_task_context": {
1029
+ if (!agentId) throw new Error("Not registered. Call agent_register first.");
1030
+ const result = await client.getTicketTask(args.task_id as string);
1031
+ return wrapWithPendingItems(client, agentId, result, context);
1032
+ }
1033
+
1034
+ case "claim_ticket_task": {
1035
+ if (!agentId) throw new Error("Not registered. Call agent_register first.");
1036
+ const result = await client.claimTicketTask(args.task_id as string, agentId);
1037
+ return wrapWithPendingItems(client, agentId, result, context);
1038
+ }
1039
+
1040
+ case "reach_checkpoint": {
1041
+ if (!agentId) throw new Error("Not registered. Call agent_register first.");
1042
+ const result = await client.createCheckpoint(
1043
+ args.task_id as string,
1044
+ agentId,
1045
+ args.description as string
1046
+ );
1047
+ return wrapWithPendingItems(client, agentId, result, context);
1048
+ }
1049
+
1050
+ // Ticket creation tools
1051
+ case "create_epic": {
1052
+ const result = await client.createEpic({
1053
+ project_id: args.project_id as string,
1054
+ title: args.title as string,
1055
+ description: args.description as string | undefined,
1056
+ source_file: args.source_file as string | undefined,
1057
+ });
1058
+ return {
1059
+ ...result,
1060
+ message: `Created epic ${result.key}: ${args.title}`,
1061
+ };
1062
+ }
1063
+
1064
+ case "create_story": {
1065
+ const result = await client.createStory({
1066
+ project_id: args.project_id as string,
1067
+ epic_id: args.epic_id as string | undefined,
1068
+ title: args.title as string,
1069
+ description: args.description as string | undefined,
1070
+ required_type: args.required_type as string | undefined,
1071
+ labels: args.labels as string[] | undefined,
1072
+ });
1073
+ return {
1074
+ ...result,
1075
+ message: `Created story ${result.key}: ${args.title}`,
1076
+ };
1077
+ }
1078
+
1079
+ case "create_task": {
1080
+ const result = await client.createTask({
1081
+ project_id: args.project_id as string,
1082
+ story_id: args.story_id as string | undefined,
1083
+ title: args.title as string,
1084
+ description: args.description as string | undefined,
1085
+ required_type: args.required_type as string | undefined,
1086
+ });
1087
+ return {
1088
+ ...result,
1089
+ message: `Created task ${result.key}: ${args.title}`,
1090
+ };
1091
+ }
1092
+
1093
+ case "upload_attachment": {
1094
+ if (!agentId) throw new Error("Not registered. Call agent_register first.");
1095
+ const result = await client.uploadAttachment({
1096
+ ticket_id: args.ticket_id as string,
1097
+ ticket_type: args.ticket_type as string,
1098
+ ticket_key: args.ticket_key as string,
1099
+ file_path: args.file_path as string,
1100
+ description: args.description as string | undefined,
1101
+ agent_id: agentId,
1102
+ });
1103
+ return {
1104
+ ...result,
1105
+ message: `Uploaded ${result.filename} to ${args.ticket_key}`,
1106
+ };
1107
+ }
1108
+
1109
+ case "add_comment": {
1110
+ if (!agentId) throw new Error("Not registered. Call agent_register first.");
1111
+ const result = await client.addComment({
1112
+ ticket_id: args.ticket_id as string,
1113
+ body: args.body as string,
1114
+ });
1115
+ return {
1116
+ ...result,
1117
+ message: `Added comment to ticket`,
1118
+ };
673
1119
  }
674
1120
 
675
1121
  default: