-
-
Notifications
You must be signed in to change notification settings - Fork 124
Expand file tree
/
Copy pathclaude_mcp_client.py
More file actions
executable file
·1624 lines (1399 loc) · 63.6 KB
/
Copy pathclaude_mcp_client.py
File metadata and controls
executable file
·1624 lines (1399 loc) · 63.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
Claude MCP Client for Hephaestus
This client connects to the Hephaestus server running on port 8000
"""
from fastmcp import FastMCP
import httpx
import asyncio
# Initialize MCP client
mcp = FastMCP("hephaestus-client")
# Hephaestus server URL
HEPHAESTUS_URL = "http://localhost:8000"
DEFAULT_AGENT_ID = "main-session-agent"
@mcp.tool()
def health_check() -> str:
"""Check if Hephaestus server is running"""
try:
import requests
response = requests.get(f"{HEPHAESTUS_URL}/health", timeout=5)
if response.status_code == 200:
return "✅ Hephaestus server is healthy and running on port 8000"
else:
return f"⚠️ Server responded with status {response.status_code}"
except Exception as e:
return f"❌ Cannot connect to Hephaestus server: {str(e)}"
@mcp.tool()
async def create_task(description: str, done_definition: str, agent_id: str, workflow_id: str, phase_id: int, priority: str = "medium", cwd: str = None, ticket_id: str = None) -> str:
"""Create a new task in Hephaestus.
Args:
description: What needs to be done
done_definition: Clear criteria for completion
agent_id: Your agent ID (REQUIRED - found in your initial prompt under "Your Agent ID:")
workflow_id: Your workflow ID (REQUIRED - found in your initial prompt under "Your Workflow ID:")
phase_id: Phase ID for the task (REQUIRED - MUST specify which workflow phase this task belongs to, e.g., 1, 2, 3)
priority: Task priority (low/medium/high)
cwd: Current working directory for the task (optional)
ticket_id: Associated ticket ID (OPTIONAL for SDK/root tasks, REQUIRED when ticket tracking is enabled for MCP agents)
CRITICAL: You MUST provide agent_id, workflow_id, AND phase_id for every task.
- agent_id: ALWAYS use YOUR agent ID from the prompt header (looks like "6a062184-e189-4d8d-8376-89da987b9996").
NEVER use placeholder values like 'agent-mcp' - they will cause authorization failures.
- workflow_id: ALWAYS use YOUR workflow ID from the prompt header (looks like "a1b2c3d4-e5f6-7890-abcd-ef1234567890").
- phase_id: REQUIRED - Specify the workflow phase number (e.g., 1 for Phase 1, 2 for Phase 2, etc.)
IMPORTANT FOR TICKET TRACKING:
- When ticket tracking is active, MCP agents MUST provide ticket_id
- SDK tasks (root/beginning tasks created by main-session-agent) may omit ticket_id as they ARE the ticket creators
- Use create_ticket() first to get a ticket_id, then pass it here when creating tasks
Omitting phase_id will cause workflow coordination issues.
"""
try:
async with httpx.AsyncClient() as client:
request_data = {
"task_description": description,
"done_definition": done_definition,
"ai_agent_id": agent_id,
"workflow_id": workflow_id,
"priority": priority,
"phase_id": str(phase_id)
}
# Add optional fields if provided
if cwd:
request_data["cwd"] = cwd
if ticket_id:
request_data["ticket_id"] = ticket_id
response = await client.post(
f"{HEPHAESTUS_URL}/create_task",
json=request_data,
headers={
"Content-Type": "application/json",
"X-Agent-ID": agent_id
},
timeout=10.0
)
if response.status_code == 200:
result = response.json()
cwd_info = f"\nWorking Directory: {cwd}" if cwd else ""
return f"""✅ Task created successfully!
Task ID: {result.get('task_id', 'unknown')}
Assigned to: {result.get('assigned_agent_id', 'unknown')}
Status: {result.get('status', 'unknown')}{cwd_info}
Description: {result.get('enriched_description', description)[:100]}..."""
else:
return f"❌ Failed to create task: {response.text}"
except Exception as e:
return f"❌ Error creating task: {str(e)}"
@mcp.tool()
async def get_tasks(status: str = "all") -> str:
"""List tasks in Hephaestus.
Args:
status: Filter by status (all/pending/assigned/in_progress/done/failed)
"""
try:
async with httpx.AsyncClient() as client:
params = {} if status == "all" else {"status": status}
response = await client.get(
f"{HEPHAESTUS_URL}/task_progress",
params=params,
headers={"X-Agent-ID": DEFAULT_AGENT_ID},
timeout=10.0
)
if response.status_code == 200:
tasks = response.json()
if not tasks:
return "📋 No tasks found"
if isinstance(tasks, list):
task_list = []
for task in tasks:
task_list.append(
f"• [{task['status']}] {task['id'][:8]}: {task['description'][:60]}..."
)
return f"📋 Tasks:\n" + "\n".join(task_list)
else:
# Single task
return f"📋 Task {tasks['id'][:8]}: {tasks['status']} - {tasks['description']}"
else:
return f"❌ Failed to get tasks: {response.text}"
except Exception as e:
return f"❌ Error getting tasks: {str(e)}"
@mcp.tool()
async def save_memory(content: str, agent_id: str, memory_type: str = "discovery") -> str:
"""Save a memory to Hephaestus knowledge base.
Args:
content: The memory content to save
agent_id: Your agent ID (CRITICAL: must match YOUR agent ID from your initial prompt)
memory_type: Type of memory (error_fix/discovery/decision/learning/warning/codebase_knowledge)
CRITICAL: Use your actual agent UUID from your initial prompt.
Example: agent_id="84f15f6c-35b1-4d57-97ac-92a3c0c94d29"
DO NOT use 'agent-mcp' or any placeholder - it will cause errors!
"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{HEPHAESTUS_URL}/save_memory",
json={
"ai_agent_id": agent_id,
"memory_content": content,
"memory_type": memory_type,
"tags": [],
"related_files": []
},
headers={
"Content-Type": "application/json",
"X-Agent-ID": agent_id
},
timeout=10.0
)
if response.status_code == 200:
result = response.json()
return f"✅ Memory saved! ID: {result.get('memory_id', 'unknown')}"
else:
return f"❌ Failed to save memory: {response.text}"
except Exception as e:
return f"❌ Error saving memory: {str(e)}"
@mcp.tool()
async def update_task_status(
task_id: str,
agent_id: str,
status: str,
summary: str = "",
failure_reason: str = "",
key_learnings: list = None
) -> str:
"""Update the status of a task in Hephaestus.
Args:
task_id: The ID of the task to update
agent_id: Your agent ID (CRITICAL: must match YOUR agent ID from your initial prompt)
status: New status (done/failed/in_progress)
summary: Summary of what was accomplished (for done status)
failure_reason: Reason for failure (for failed status)
key_learnings: List of key learnings from the task
CRITICAL: agent_id must match YOUR agent ID from your initial prompt.
Example (use your actual ID from prompt):
update_task_status(
agent_id="6a062184-e189-4d8d-8376-89da987b9996", # Your actual UUID
task_id="dc2c0279-ba16-4a8d-9fd5-846259967e68",
status="done",
summary="Task completed successfully"
)
DO NOT use 'agent-mcp' or any placeholder - it will cause "Agent not authorized" errors!
"""
try:
async with httpx.AsyncClient() as client:
payload = {
"task_id": task_id,
"status": status,
"agent_id": agent_id,
"key_learnings": key_learnings or []
}
if summary:
payload["summary"] = summary
if failure_reason:
payload["failure_reason"] = failure_reason
response = await client.post(
f"{HEPHAESTUS_URL}/update_task_status",
json=payload,
headers={
"Content-Type": "application/json",
"X-Agent-ID": agent_id
},
timeout=10.0
)
if response.status_code == 200:
result = response.json()
message = result.get("message", f"Task {status} successfully")
# Use appropriate emoji based on message content
if "validation" in message.lower():
status_emoji = "🔍" # Magnifying glass for validation
elif status == "done":
status_emoji = "✅"
elif status == "failed":
status_emoji = "❌"
else:
status_emoji = "🔄"
return f"{status_emoji} {message}"
else:
return f"❌ Failed to update task status: {response.text}"
except Exception as e:
return f"❌ Error updating task status: {str(e)}"
@mcp.tool()
async def give_validation_review(
task_id: str,
validator_agent_id: str,
validation_passed: bool,
feedback: str,
evidence: list = None,
recommendations: list = None
) -> str:
"""Submit validation review for a task.
Args:
task_id: The ID of the task being validated
validator_agent_id: Your validator agent ID
validation_passed: Whether validation passed (true/false)
feedback: Detailed feedback about what passed/failed
evidence: List of evidence items supporting your decision (optional)
recommendations: List of recommended follow-up tasks if validation passes (optional)
This tool should only be called by validator agents after reviewing a task.
"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{HEPHAESTUS_URL}/give_validation_review",
json={
"task_id": task_id,
"validator_agent_id": validator_agent_id,
"validation_passed": validation_passed,
"feedback": feedback,
"evidence": evidence or [],
"recommendations": recommendations or []
},
headers={
"Content-Type": "application/json",
"X-Agent-ID": validator_agent_id
},
timeout=10.0
)
if response.status_code == 200:
result = response.json()
status_emoji = "✅" if result.get("status") == "completed" else "🔄"
return f"""{status_emoji} Validation Review Submitted!
Status: {result.get('status', 'unknown')}
Message: {result.get('message', '')}
Iteration: {result.get('iteration', 'N/A')}"""
else:
return f"❌ Failed to submit validation review: {response.text}"
except Exception as e:
return f"❌ Error submitting validation review: {str(e)}"
@mcp.tool()
async def validate_my_agent_id(agent_id: str) -> str:
"""Validate that your agent ID has the correct format before using it.
Args:
agent_id: The agent ID you plan to use
Returns:
Validation result with helpful error messages if invalid
Use this tool if you're unsure about your agent ID format!
"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{HEPHAESTUS_URL}/validate_agent_id/{agent_id}",
timeout=5.0
)
if response.status_code == 200:
result = response.json()
if result["valid"]:
return f"✅ {result['message']}"
else:
mistakes = "\n".join(f" • {m}" for m in result["common_mistakes"])
return f"""❌ {result['message']}
Common mistakes:
{mistakes}
Check your initial prompt for "Your Agent ID:" - it should be a UUID like:
6a062184-e189-4d8d-8376-89da987b9996"""
else:
return f"❌ Validation failed: {response.text}"
except Exception as e:
return f"❌ Error validating agent ID: {str(e)}"
@mcp.tool()
async def get_agent_status() -> str:
"""Get status of all active agents in Hephaestus"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{HEPHAESTUS_URL}/agent_status",
headers={"X-Agent-ID": DEFAULT_AGENT_ID},
timeout=10.0
)
if response.status_code == 200:
agents = response.json()
if not agents:
return "🤖 No active agents"
agent_list = []
for agent in agents:
status_emoji = "🟢" if agent['status'] == "working" else "🔴"
agent_list.append(
f"{status_emoji} {agent['id'][:8]}: {agent['status']} - Task: {agent.get('current_task_id', 'none')[:8] if agent.get('current_task_id') else 'none'}"
)
return f"🤖 Active Agents:\n" + "\n".join(agent_list)
else:
return f"❌ Failed to get agent status: {response.text}"
except Exception as e:
return f"❌ Error getting agent status: {str(e)}"
@mcp.tool()
async def submit_result(markdown_file_path: str, agent_id: str, explanation: str, evidence: list = None, extra_files: list = None) -> str:
"""Submit a workflow result with evidence for validation.
Args:
markdown_file_path: Path to markdown file with solution and evidence
agent_id: Your agent ID
explanation: Brief explanation of what was accomplished
evidence: List of evidence supporting completion (optional)
extra_files: List of additional file paths (e.g., patches, reproduction scripts) for validators (optional)
Use when you have found the definitive solution to a workflow problem.
The markdown file should contain comprehensive evidence including:
- Clear solution statement
- Execution outputs and proof
- Step-by-step methodology
- Reproduction steps for verification
For SWEBench workflows, you should include:
- extra_files: ["./solution.patch", "./reproduction_instructions.md"]
"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{HEPHAESTUS_URL}/submit_result",
json={
"markdown_file_path": markdown_file_path,
"explanation": explanation,
"evidence": evidence or [],
"extra_files": extra_files or [],
},
headers={
"Content-Type": "application/json",
"X-Agent-ID": agent_id
},
timeout=10.0
)
if response.status_code == 200:
result = response.json()
validation_info = f"\n🔍 Validation: {'Triggered' if result.get('validation_triggered') else 'Not required'}"
return f"""✅ Result submitted successfully!
Result ID: {result.get('result_id', 'unknown')}
Workflow ID: {result.get('workflow_id', 'unknown')}
Status: {result.get('status', 'unknown')}{validation_info}
Message: {result.get('message', '')}"""
else:
return f"❌ Failed to submit result: {response.text}"
except Exception as e:
return f"❌ Error submitting result: {str(e)}"
@mcp.tool()
async def submit_result_validation(
result_id: str,
validation_passed: bool,
feedback: str,
evidence: list = None
) -> str:
"""Submit validation review for a workflow result.
Args:
result_id: ID of the result being validated (REQUIRED - this is the full result ID you were given)
validation_passed: Whether the result meets criteria (true/false)
feedback: Detailed validation feedback explaining decision
evidence: Evidence supporting the decision (list of dicts, optional)
This tool should only be called by result validator agents after reviewing
a submitted workflow result against the configured criteria.
IMPORTANT: You must use the complete result_id that was provided to you (e.g., result-a3145b59-e954-434e-a254-962ef2d1f669).
"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{HEPHAESTUS_URL}/submit_result_validation",
json={
"result_id": result_id,
"validation_passed": validation_passed,
"feedback": feedback,
"evidence": evidence or [],
},
headers={
"Content-Type": "application/json"
},
timeout=10.0
)
if response.status_code == 200:
result = response.json()
workflow_action = result.get('workflow_action_taken', 'none')
action_emoji = "🛑" if workflow_action == "workflow_terminated" else "▶️"
action_text = f"\n{action_emoji} Workflow Action: {workflow_action}" if workflow_action != 'none' else ""
return f"""✅ Result Validation Submitted!
Status: {result.get('status', 'unknown')}
Message: {result.get('message', '')}{action_text}
Result ID: {result.get('result_id', 'unknown')}"""
else:
return f"❌ Failed to submit result validation: {response.text}"
except Exception as e:
return f"❌ Error submitting result validation: {str(e)}"
@mcp.tool()
async def get_workflow_results(workflow_id: str) -> str:
"""Get all submitted results for a workflow.
Args:
workflow_id: ID of the workflow
Returns list of results with their validation status and details.
"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{HEPHAESTUS_URL}/workflows/{workflow_id}/results",
headers={"X-Agent-ID": DEFAULT_AGENT_ID},
timeout=10.0
)
if response.status_code == 200:
results = response.json()
if not results:
return f"📋 No results found for workflow {workflow_id}"
result_list = []
for result in results:
status_emoji = "✅" if result['status'] == "validated" else ("❌" if result['status'] == "rejected" else "⏳")
# Show full result_id - critical for validators to use correct ID
result_list.append(
f"{status_emoji} {result['result_id']}: {result['status']} by {result['agent_id'][:8]}"
)
return f"📋 Workflow Results:\n" + "\n".join(result_list)
else:
return f"❌ Failed to get workflow results: {response.text}"
except Exception as e:
return f"❌ Error getting workflow results: {str(e)}"
@mcp.tool()
async def broadcast_message(message: str, sender_agent_id: str) -> str:
"""Broadcast a message to all active agents in the system.
Use this when you have information that ALL other agents should know about,
or when you need help but don't know which specific agent to ask.
Args:
message: The message content to broadcast to all agents
sender_agent_id: Your agent ID (REQUIRED - use your assigned agent ID)
Examples of when to use broadcast:
- "I found a critical bug in module X that affects everyone"
- "Does anyone have information about how authentication works?"
- "I've completed the database schema - all agents can now use it"
- "Warning: The API endpoint /users is currently down"
The message will be delivered to all active agents with the prefix:
[AGENT {your_id} BROADCAST]: {your_message}
"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{HEPHAESTUS_URL}/broadcast_message",
json={"message": message},
headers={
"Content-Type": "application/json",
"X-Agent-ID": sender_agent_id
},
timeout=10.0
)
if response.status_code == 200:
result = response.json()
recipient_count = result.get('recipient_count', 0)
if recipient_count == 0:
return "📢 Broadcast sent, but no other agents are currently active"
return f"📢 Message broadcast successfully to {recipient_count} agent(s)"
else:
return f"❌ Failed to broadcast message: {response.text}"
except Exception as e:
return f"❌ Error broadcasting message: {str(e)}"
@mcp.tool()
async def send_message(message: str, sender_agent_id: str, recipient_agent_id: str) -> str:
"""Send a direct message to a specific agent.
Use this when you know which specific agent you want to communicate with,
such as asking for help from an agent working on a related task or
providing targeted information to a specific agent.
Args:
message: The message content to send
sender_agent_id: Your agent ID (REQUIRED - use your assigned agent ID)
recipient_agent_id: The ID of the agent you want to message
Examples of when to use direct messaging:
- "Agent X: I need the API specs you were working on"
- "Agent Y: Your task conflicts with mine - can we coordinate?"
- "Agent Z: I found the answer to your earlier question about caching"
- "Agent W: Can you review my implementation before I submit?"
The message will be delivered with the prefix:
[AGENT {your_id} TO AGENT {recipient_id}]: {your_message}
Tip: Use get_agent_status() to see which agents are currently active
and what tasks they're working on before sending a message.
"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{HEPHAESTUS_URL}/send_message",
json={
"recipient_agent_id": recipient_agent_id,
"message": message
},
headers={
"Content-Type": "application/json",
"X-Agent-ID": sender_agent_id
},
timeout=10.0
)
if response.status_code == 200:
result = response.json()
if result.get('success'):
return f"✉️ Message sent successfully to agent {recipient_agent_id[:8]}"
else:
return f"❌ {result.get('message', 'Failed to send message')}"
else:
return f"❌ Failed to send message: {response.text}"
except Exception as e:
return f"❌ Error sending message: {str(e)}"
# ==================== TICKET TRACKING SYSTEM TOOLS ====================
@mcp.tool()
async def create_ticket(
agent_id: str,
workflow_id: str,
title: str,
description: str,
ticket_type: str = "task",
priority: str = "medium",
tags: list = None,
blocked_by_ticket_ids: list = None,
assigned_agent_id: str = None,
parent_ticket_id: str = None
) -> str:
"""Create a new ticket in the workflow tracking system.
Use this when you discover work that needs to be tracked separately from tasks.
Returns similar tickets for duplicate detection.
Args:
agent_id: Your agent ID (CRITICAL: use YOUR UUID from initial prompt, e.g., "84f15f6c-35b1-4d57-97ac-92a3c0c94d29")
workflow_id: Your workflow ID (CRITICAL: use YOUR workflow UUID from initial prompt)
title: Short, descriptive title for the ticket (3-500 chars)
description: Detailed description of what needs to be done (min 10 chars)
ticket_type: Type of ticket (bug/feature/improvement/task/spike) - default: task
priority: Priority level (low/medium/high/critical) - default: medium
tags: Optional list of tags for categorization
blocked_by_ticket_ids: List of ticket IDs that block this ticket
assigned_agent_id: Optional agent to assign ticket to
parent_ticket_id: Optional parent ticket for sub-tickets
CRITICAL: Both agent_id and workflow_id must be your actual UUIDs from your initial prompt!
DO NOT use 'agent' or 'agent-mcp' - it will fail with "Agent not found"!
IMPORTANT: Search for existing tickets before creating to avoid duplicates!
Use search_tickets() with semantic search to find related work.
"""
import logging
import os
logger = logging.getLogger(__name__)
logger.info(f"[MCP_CLIENT_TICKET] ========== START ==========")
logger.info(f"[MCP_CLIENT_TICKET] Agent: {agent_id}")
logger.info(f"[MCP_CLIENT_TICKET] Title: {title[:60]}...")
logger.info(f"[MCP_CLIENT_TICKET] Type: {ticket_type}, Priority: {priority}")
# Use MCP_TOOL_TIMEOUT if set (for human approval workflows), otherwise default to 10 seconds
mcp_timeout_ms = os.environ.get('MCP_TOOL_TIMEOUT')
if mcp_timeout_ms:
timeout_seconds = float(mcp_timeout_ms) / 1000.0
logger.info(f"[MCP_CLIENT_TICKET] Using MCP_TOOL_TIMEOUT: {timeout_seconds}s ({mcp_timeout_ms}ms)")
else:
timeout_seconds = 10.0
logger.info(f"[MCP_CLIENT_TICKET] No MCP_TOOL_TIMEOUT set, using default: {timeout_seconds}s")
try:
async with httpx.AsyncClient() as client:
payload = {
"workflow_id": workflow_id,
"title": title,
"description": description,
"ticket_type": ticket_type,
"priority": priority,
"tags": tags or [],
"blocked_by_ticket_ids": blocked_by_ticket_ids or [],
"assigned_agent_id": assigned_agent_id,
"parent_ticket_id": parent_ticket_id,
}
logger.info(f"[MCP_CLIENT_TICKET] Payload: {payload}")
logger.info(f"[MCP_CLIENT_TICKET] Sending POST to {HEPHAESTUS_URL}/api/tickets/create")
response = await client.post(
f"{HEPHAESTUS_URL}/api/tickets/create",
json=payload,
headers={
"Content-Type": "application/json",
"X-Agent-ID": agent_id
},
timeout=timeout_seconds
)
logger.info(f"[MCP_CLIENT_TICKET] Response status: {response.status_code}")
logger.info(f"[MCP_CLIENT_TICKET] Response body: {response.text}")
if response.status_code == 200:
result = response.json()
logger.info(f"[MCP_CLIENT_TICKET] ✅ Success! Ticket ID: {result.get('ticket_id')}")
similar_msg = ""
if result.get("similar_tickets"):
similar_msg = f"\n\n⚠️ Found {len(result['similar_tickets'])} similar tickets - check for duplicates!"
success_message = f"""✅ Ticket created successfully!
Ticket ID: {result.get('ticket_id', 'unknown')}
Status: {result.get('status', 'unknown')}
Message: {result.get('message', '')}{similar_msg}"""
logger.info(f"[MCP_CLIENT_TICKET] Returning success message to agent")
logger.info(f"[MCP_CLIENT_TICKET] ========== SUCCESS ==========")
return success_message
else:
error_message = f"❌ Failed to create ticket: {response.text}"
logger.error(f"[MCP_CLIENT_TICKET] ❌ HTTP {response.status_code}: {response.text}")
logger.error(f"[MCP_CLIENT_TICKET] Returning error message to agent")
logger.error(f"[MCP_CLIENT_TICKET] ========== FAILED ==========")
return error_message
except Exception as e:
error_message = f"❌ Error creating ticket: {str(e)}"
logger.error(f"[MCP_CLIENT_TICKET] ❌ Exception: {type(e).__name__}: {e}")
logger.error(f"[MCP_CLIENT_TICKET] ========== EXCEPTION ==========")
return error_message
@mcp.tool()
async def update_ticket(
ticket_id: str,
agent_id: str,
updates: dict,
update_comment: str = None
) -> str:
"""Update ticket fields (title, description, priority, tags, assigned_agent_id, blocked_by_ticket_ids).
Cannot change status - use change_ticket_status for that.
Args:
ticket_id: ID of the ticket to update
agent_id: Your agent ID
updates: Fields to update (dict with keys: title, description, priority, assigned_agent_id, ticket_type, tags, blocked_by_ticket_ids)
update_comment: Optional comment explaining the update
"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{HEPHAESTUS_URL}/api/tickets/update",
json={
"ticket_id": ticket_id,
"updates": updates,
"update_comment": update_comment,
},
headers={
"Content-Type": "application/json",
"X-Agent-ID": agent_id
},
timeout=10.0
)
if response.status_code == 200:
result = response.json()
return f"""✅ Ticket updated successfully!
Ticket ID: {ticket_id}
Fields updated: {', '.join(result.get('fields_updated', []))}
Message: {result.get('message', '')}"""
else:
return f"❌ Failed to update ticket: {response.text}"
except Exception as e:
return f"❌ Error updating ticket: {str(e)}"
@mcp.tool()
async def change_ticket_status(
ticket_id: str,
agent_id: str,
new_status: str,
comment: str,
commit_sha: str = None
) -> str:
"""Move ticket to a different status column.
IMPORTANT: Blocked tickets (with blocked_by_ticket_ids) cannot change status until blockers are resolved.
Args:
ticket_id: ID of the ticket
agent_id: Your agent ID
new_status: New status (must match board_config columns)
comment: Required comment explaining status change (min 10 chars)
commit_sha: Optional commit SHA to link to this status change
"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{HEPHAESTUS_URL}/api/tickets/change-status",
json={
"ticket_id": ticket_id,
"new_status": new_status,
"comment": comment,
"commit_sha": commit_sha,
},
headers={
"Content-Type": "application/json",
"X-Agent-ID": agent_id
},
timeout=10.0
)
if response.status_code == 200:
result = response.json()
if result.get("blocked"):
blocking_ids = ', '.join(result.get("blocking_ticket_ids", []))
return f"""🔒 Ticket is BLOCKED!
Ticket ID: {ticket_id}
Blocked by: {blocking_ids}
Cannot change status until blocking tickets are resolved."""
else:
return f"""✅ Ticket status changed!
Ticket ID: {ticket_id}
From: {result.get('old_status', 'unknown')}
To: {result.get('new_status', 'unknown')}"""
else:
return f"❌ Failed to change ticket status: {response.text}"
except Exception as e:
return f"❌ Error changing ticket status: {str(e)}"
@mcp.tool()
async def add_ticket_comment(
ticket_id: str,
agent_id: str,
comment_text: str,
comment_type: str = "general",
mentions: list = None
) -> str:
"""Add a comment to a ticket.
Use for progress updates, blockers, or communication with other agents.
Args:
ticket_id: ID of the ticket
agent_id: Your agent ID
comment_text: Comment text (min 1 char)
comment_type: Type of comment (general/status_change/blocker/resolution) - default: general
mentions: Agent/ticket IDs mentioned in comment
"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{HEPHAESTUS_URL}/api/tickets/comment",
json={
"ticket_id": ticket_id,
"comment_text": comment_text,
"comment_type": comment_type,
"mentions": mentions or [],
},
headers={
"Content-Type": "application/json",
"X-Agent-ID": agent_id
},
timeout=10.0
)
if response.status_code == 200:
result = response.json()
return f"✅ Comment added to ticket {ticket_id}"
else:
return f"❌ Failed to add comment: {response.text}"
except Exception as e:
return f"❌ Error adding comment: {str(e)}"
@mcp.tool()
async def search_tickets(
agent_id: str,
workflow_id: str,
query: str,
search_type: str = "hybrid",
filters: dict = None,
limit: int = 10,
include_comments: bool = True
) -> str:
"""Search for tickets using HYBRID search (70% semantic + 30% keyword) by default.
Use natural language queries. Shows blocked (🔒) and resolved (✅) indicators.
Args:
agent_id: Your agent ID
workflow_id: Your workflow ID (REQUIRED - searches within this workflow only)
query: Search query (natural language, min 3 chars)
search_type: Search mode (semantic/keyword/hybrid) - DEFAULT: hybrid = 70% semantic + 30% keyword
filters: Optional filters (dict with keys: status, priority, ticket_type, assigned_agent_id, tags, is_blocked)
limit: Max number of results (1-50) - default: 10
include_comments: Whether to search in comments too - default: true
BEST PRACTICE: Use hybrid search (default) for best results!
- Hybrid combines semantic understanding with keyword precision
- Semantic search is good for conceptual queries
- Keyword search is good for exact term matching
"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{HEPHAESTUS_URL}/api/tickets/search",
json={
"workflow_id": workflow_id,
"query": query,
"search_type": search_type,
"filters": filters or {},
"limit": limit,
"include_comments": include_comments,
},
headers={
"Content-Type": "application/json",
"X-Agent-ID": agent_id
},
timeout=10.0
)
if response.status_code == 200:
result = response.json()
if not result.get("results"):
return f"🔍 No tickets found for query: '{query}'"
ticket_list = []
for ticket in result.get("results", []):
blocked_icon = "🔒" if ticket.get("is_blocked") else ""
resolved_icon = "✅" if ticket.get("is_resolved") else ""
ticket_list.append(
f"{blocked_icon}{resolved_icon} {ticket['ticket_id'][:12]}: [{ticket['status']}] {ticket['title'][:60]} (score: {ticket.get('relevance_score', 0):.2f})"
)
search_mode_msg = f"({search_type} search: " + (
"70% semantic + 30% keyword" if search_type == "hybrid" else search_type
) + ")"
return f"""🔍 Found {result.get('total_found', 0)} tickets {search_mode_msg}
Search time: {result.get('search_time_ms', 0):.0f}ms
{chr(10).join(ticket_list)}
💡 Tip: Use hybrid search (default) for best results!"""
else:
return f"❌ Failed to search tickets: {response.text}"
except Exception as e:
return f"❌ Error searching tickets: {str(e)}"
@mcp.tool()
async def get_ticket(ticket_id: str) -> str:
"""Get detailed information about a specific ticket by its exact ID.
IMPORTANT: You MUST provide the EXACT, COMPLETE ticket ID.
Args:
ticket_id: The complete ticket ID (e.g., "ticket-c368a0d1-cbd7-4231-a374-0a3a7374064e")
Do NOT use shortened IDs like "ticket-c368a"!
Returns:
Complete ticket details including:
- Full description
- All comments with timestamps
- Complete history of status changes
- All linked commits with file changes
- Blocking/blocked relationships
- Tags and metadata
If you DON'T know the exact ticket ID:
1. Use search_tickets() to find tickets by title/description
2. Use get_tickets() to list all tickets
3. Then use this function with the exact ticket_id from those results
Example workflow:
# First, search for the ticket
search_result = search_tickets(
agent_id="your-id",
query="Frontend Infrastructure",
search_type="hybrid"
)
# Note the exact ticket_id from results: ticket-c368a0d1-cbd7-4231-a374-0a3a7374064e
# Then get full details
details = get_ticket("ticket-c368a0d1-cbd7-4231-a374-0a3a7374064e")
"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{HEPHAESTUS_URL}/api/tickets/{ticket_id}",
timeout=10.0
)
if response.status_code == 200:
data = response.json()
ticket = data.get("ticket", {})
comments = data.get("comments", [])
history = data.get("history", [])
commits = data.get("commits", [])
# Format the output
result = []
# Header
blocked_icon = "🔒 " if ticket.get("is_blocked") else ""
resolved_icon = "✅ " if ticket.get("is_resolved") else ""
result.append(f"{'='*80}")
result.append(f"{blocked_icon}{resolved_icon}TICKET: {ticket['id']}")
result.append(f"{'='*80}")
# Basic info
result.append(f"\n📋 BASIC INFORMATION")