agenthub-multiagent-mcp 1.4.1 → 1.5.1

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.
@@ -17,6 +17,7 @@ export interface ToolContext {
17
17
  setCurrentAgentId: (id: string) => void;
18
18
  stopHeartbeat: () => void;
19
19
  getWorkingDir: () => string;
20
+ setWorkingDir: (dir: string) => void;
20
21
  getPushedItems: () => PushedItems;
21
22
  }
22
23
 
@@ -178,6 +179,14 @@ export function registerTools(): Tool[] {
178
179
  inputSchema: {
179
180
  type: "object",
180
181
  properties: {
182
+ working_dir: {
183
+ type: "string",
184
+ description: "Project directory path where .agenthub state will be saved (required)",
185
+ },
186
+ project: {
187
+ type: "string",
188
+ description: "Project key to bind this agent to (e.g., 'KRISHI'). Required.",
189
+ },
181
190
  id: {
182
191
  type: "string",
183
192
  description: "Unique agent identifier (e.g., 'backend-1', 'tester')",
@@ -191,7 +200,7 @@ export function registerTools(): Tool[] {
191
200
  description: "Model name (e.g., 'claude-opus-4.5', 'gpt-4-turbo', 'o3', 'gemini-2.0-pro'). Provider is auto-detected.",
192
201
  },
193
202
  },
194
- required: ["id", "model"],
203
+ required: ["working_dir", "project", "model"],
195
204
  },
196
205
  },
197
206
  {
@@ -594,14 +603,10 @@ export function registerTools(): Tool[] {
594
603
  // Ticket creation tools
595
604
  {
596
605
  name: "create_epic",
597
- description: "Create an epic (large body of work) in a project",
606
+ description: "Create an epic (large body of work) in the bound project. Automatically uploads the OpenSpec file as an attachment. (requires agent_register first)",
598
607
  inputSchema: {
599
608
  type: "object",
600
609
  properties: {
601
- project_id: {
602
- type: "string",
603
- description: "Project ID to create epic in",
604
- },
605
610
  title: {
606
611
  type: "string",
607
612
  description: "Epic title",
@@ -612,25 +617,21 @@ export function registerTools(): Tool[] {
612
617
  },
613
618
  source_file: {
614
619
  type: "string",
615
- description: "Optional: path to OpenSpec file this came from",
620
+ description: "Path to OpenSpec file (e.g., 'openspec/changes/feature-name/proposal.md'). Will be uploaded as attachment.",
616
621
  },
617
622
  },
618
- required: ["project_id", "title"],
623
+ required: ["title", "source_file"],
619
624
  },
620
625
  },
621
626
  {
622
627
  name: "create_story",
623
- description: "Create a story under an epic",
628
+ description: "Create a story in the bound project (requires agent_register first)",
624
629
  inputSchema: {
625
630
  type: "object",
626
631
  properties: {
627
- project_id: {
628
- type: "string",
629
- description: "Project ID",
630
- },
631
632
  epic_id: {
632
633
  type: "string",
633
- description: "Epic ID to add story to",
634
+ description: "Epic ID to add story to (optional)",
634
635
  },
635
636
  title: {
636
637
  type: "string",
@@ -651,22 +652,18 @@ export function registerTools(): Tool[] {
651
652
  description: "Additional labels",
652
653
  },
653
654
  },
654
- required: ["project_id", "title"],
655
+ required: ["title"],
655
656
  },
656
657
  },
657
658
  {
658
659
  name: "create_task",
659
- description: "Create a task under a story",
660
+ description: "Create a task in the bound project (requires agent_register first)",
660
661
  inputSchema: {
661
662
  type: "object",
662
663
  properties: {
663
- project_id: {
664
- type: "string",
665
- description: "Project ID",
666
- },
667
664
  story_id: {
668
665
  type: "string",
669
- description: "Story ID to add task to",
666
+ description: "Story ID to add task to (optional)",
670
667
  },
671
668
  title: {
672
669
  type: "string",
@@ -682,7 +679,7 @@ export function registerTools(): Tool[] {
682
679
  description: "Required skill type",
683
680
  },
684
681
  },
685
- required: ["project_id", "title"],
682
+ required: ["title"],
686
683
  },
687
684
  },
688
685
  {
@@ -748,7 +745,17 @@ export async function handleToolCall(
748
745
  switch (name) {
749
746
  // Registration
750
747
  case "agent_register": {
751
- const workingDir = context.getWorkingDir();
748
+ // Get required params
749
+ const workingDir = args.working_dir as string;
750
+ if (!workingDir) {
751
+ throw new Error("working_dir is required");
752
+ }
753
+
754
+ const projectKey = args.project as string;
755
+ if (!projectKey) {
756
+ throw new Error("project is required");
757
+ }
758
+
752
759
  const owner = state.getCurrentOwner();
753
760
  const requestedId = args.id as string;
754
761
 
@@ -779,6 +786,9 @@ export async function handleToolCall(
779
786
  // Set connect token on client for org operations
780
787
  client.setConnectToken(existingState.token);
781
788
 
789
+ // Store working_dir in context for other tools to use
790
+ context.setWorkingDir(workingDir);
791
+
782
792
  // Update state with returned name from server
783
793
  state.saveState(workingDir, {
784
794
  ...existingState,
@@ -805,7 +815,8 @@ export async function handleToolCall(
805
815
  // Step 1: Initialize registration session
806
816
  const initResult = await client.initRegistration(
807
817
  requestedId,
808
- args.name as string | undefined
818
+ args.name as string | undefined,
819
+ projectKey
809
820
  );
810
821
 
811
822
  // Step 2: Open browser to dashboard
@@ -851,6 +862,9 @@ export async function handleToolCall(
851
862
  // Set connect token on client for org operations
852
863
  client.setConnectToken(registrationResult.connect_token!);
853
864
 
865
+ // Store working_dir in context for other tools to use
866
+ context.setWorkingDir(workingDir);
867
+
854
868
  // Save state for future reconnection
855
869
  state.saveState(workingDir, {
856
870
  agent_id: agentId,
@@ -858,6 +872,9 @@ export async function handleToolCall(
858
872
  owner,
859
873
  token: registrationResult.connect_token!,
860
874
  agent_type: registrationResult.agent_type,
875
+ project_id: registrationResult.project_id!,
876
+ project_key: registrationResult.project_key!,
877
+ org_id: registrationResult.org_id!,
861
878
  registered_at: new Date().toISOString(),
862
879
  });
863
880
 
@@ -1055,21 +1072,65 @@ export async function handleToolCall(
1055
1072
 
1056
1073
  // Ticket creation tools
1057
1074
  case "create_epic": {
1075
+ if (!agentId) throw new Error("Not registered. Call agent_register first.");
1076
+
1077
+ // Get project from state
1078
+ const workingDir = context.getWorkingDir();
1079
+ const agentState = state.loadState(workingDir);
1080
+
1081
+ if (!agentState?.project_id) {
1082
+ throw new Error("Not registered with a project. Call agent_register first.");
1083
+ }
1084
+
1085
+ const sourceFile = args.source_file as string;
1086
+ if (!sourceFile) {
1087
+ throw new Error("source_file is required. Provide the path to the OpenSpec file.");
1088
+ }
1089
+
1090
+ // Create the epic
1058
1091
  const result = await client.createEpic({
1059
- project_id: args.project_id as string,
1092
+ project_id: agentState.project_id,
1060
1093
  title: args.title as string,
1061
1094
  description: args.description as string | undefined,
1062
- source_file: args.source_file as string | undefined,
1095
+ source_file: sourceFile,
1063
1096
  });
1097
+
1098
+ // Auto-upload the OpenSpec file as attachment
1099
+ let attachmentResult = null;
1100
+ try {
1101
+ attachmentResult = await client.uploadAttachment({
1102
+ ticket_id: result.id,
1103
+ ticket_type: "epic",
1104
+ ticket_key: result.key,
1105
+ file_path: sourceFile,
1106
+ description: "OpenSpec source document",
1107
+ agent_id: agentId,
1108
+ });
1109
+ } catch (uploadError) {
1110
+ // Log but don't fail epic creation if upload fails
1111
+ console.error("Failed to upload OpenSpec attachment:", uploadError);
1112
+ }
1113
+
1064
1114
  return {
1065
1115
  ...result,
1066
- message: `Created epic ${result.key}: ${args.title}`,
1116
+ attachment: attachmentResult,
1117
+ message: attachmentResult
1118
+ ? `Created epic ${result.key}: ${args.title} (OpenSpec uploaded)`
1119
+ : `Created epic ${result.key}: ${args.title} (OpenSpec upload failed)`,
1067
1120
  };
1068
1121
  }
1069
1122
 
1070
1123
  case "create_story": {
1124
+ // Get project from state
1125
+ const workingDir = context.getWorkingDir();
1126
+ const agentState = state.loadState(workingDir);
1127
+
1128
+ if (!agentState?.project_id) {
1129
+ throw new Error("Not registered with a project. Call agent_register first.");
1130
+ }
1131
+
1071
1132
  const result = await client.createStory({
1072
- project_id: args.project_id as string,
1133
+ project_id: agentState.project_id, // From state, not args
1073
1134
  epic_id: args.epic_id as string | undefined,
1074
1135
  title: args.title as string,
1075
1136
  description: args.description as string | undefined,
@@ -1083,8 +1144,16 @@ export async function handleToolCall(
1083
1144
  }
1084
1145
 
1085
1146
  case "create_task": {
1147
+ // Get project from state
1148
+ const workingDir = context.getWorkingDir();
1149
+ const agentState = state.loadState(workingDir);
1150
+
1151
+ if (!agentState?.project_id) {
1152
+ throw new Error("Not registered with a project. Call agent_register first.");
1153
+ }
1154
+
1086
1155
  const result = await client.createTask({
1087
- project_id: args.project_id as string,
1156
+ project_id: agentState.project_id, // From state, not args
1088
1157
  story_id: args.story_id as string | undefined,
1089
1158
  title: args.title as string,
1090
1159
  description: args.description as string | undefined,
@@ -40,21 +40,25 @@ const mockClient = {
40
40
  // Mock context
41
41
  function createMockContext(): ToolContext {
42
42
  let currentAgentId = "";
43
+ let currentWorkingDir = "/tmp/test-workdir";
43
44
  return {
44
45
  getCurrentAgentId: () => currentAgentId,
45
46
  setCurrentAgentId: (id: string) => {
46
47
  currentAgentId = id;
47
48
  },
48
49
  stopHeartbeat: vi.fn(),
49
- getWorkingDir: () => "/tmp/test-workdir",
50
+ getWorkingDir: () => currentWorkingDir,
51
+ setWorkingDir: (dir: string) => {
52
+ currentWorkingDir = dir;
53
+ },
50
54
  getPushedItems: () => ({ tasks: [], messages: [] }),
51
55
  };
52
56
  }
53
57
 
54
58
  describe("registerTools", () => {
55
- it("should return all 19 tools", () => {
59
+ it("should return all 28 tools", () => {
56
60
  const tools = registerTools();
57
- expect(tools).toHaveLength(19);
61
+ expect(tools).toHaveLength(28);
58
62
  });
59
63
 
60
64
  it("should have required tool names", () => {
@@ -100,40 +104,33 @@ describe("handleToolCall", () => {
100
104
  });
101
105
 
102
106
  describe("agent_register", () => {
103
- it("should register agent and set current agent ID", async () => {
104
- const mockResult = {
105
- agent_id: "test-agent",
106
- name: "Test Agent",
107
- token: "test-token-uuid",
108
- model: "claude-opus-4.5",
109
- model_provider: "anthropic",
110
- registered_at: "2024-01-01T00:00:00Z",
111
- slack_notified: false,
112
- pending_tasks_count: 0,
113
- unread_messages_count: 0,
114
- };
115
- vi.mocked(mockClient.registerAgent).mockResolvedValue(mockResult);
107
+ // Note: The agent_register flow now uses browser-based registration
108
+ // which requires mocking initRegistration, pollRegistrationCallback, and open()
109
+ // These are integration tests that are skipped in unit tests
110
+ it.skip("should register agent and set current agent ID", async () => {
111
+ // Browser-based flow - tested via e2e tests
112
+ });
116
113
 
117
- const result = await handleToolCall(
118
- "agent_register",
119
- { id: "test-agent", name: "Test Agent", model: "claude-opus-4.5" },
120
- mockClient,
121
- context
122
- );
114
+ it("should require working_dir parameter", async () => {
115
+ await expect(
116
+ handleToolCall(
117
+ "agent_register",
118
+ { project: "TEST", model: "claude-opus-4.5" },
119
+ mockClient,
120
+ context
121
+ )
122
+ ).rejects.toThrow("working_dir is required");
123
+ });
123
124
 
124
- expect(mockClient.registerAgent).toHaveBeenCalledWith(
125
- "test-agent",
126
- "Test Agent",
127
- "test-user",
128
- "/tmp/test-workdir",
129
- "claude-opus-4.5"
130
- );
131
- expect(context.getCurrentAgentId()).toBe("test-agent");
132
- // Result is extended with mode and message
133
- expect(result).toMatchObject({
134
- ...mockResult,
135
- mode: "registered",
136
- });
125
+ it("should require project parameter", async () => {
126
+ await expect(
127
+ handleToolCall(
128
+ "agent_register",
129
+ { working_dir: "/tmp/test", model: "claude-opus-4.5" },
130
+ mockClient,
131
+ context
132
+ )
133
+ ).rejects.toThrow("project is required");
137
134
  });
138
135
  });
139
136
 
@@ -164,7 +161,8 @@ describe("handleToolCall", () => {
164
161
  expect(mockClient.startWork).toHaveBeenCalledWith(
165
162
  "test-agent",
166
163
  "Building feature X",
167
- "TestProject"
164
+ "TestProject",
165
+ undefined
168
166
  );
169
167
  // Result is wrapped with messages
170
168
  expect(result).toMatchObject({ result: mockResult });