gobby 0.2.5__py3-none-any.whl → 0.2.6__py3-none-any.whl

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.
Files changed (148) hide show
  1. gobby/adapters/claude_code.py +13 -4
  2. gobby/adapters/codex.py +43 -3
  3. gobby/agents/runner.py +8 -0
  4. gobby/cli/__init__.py +6 -0
  5. gobby/cli/clones.py +419 -0
  6. gobby/cli/conductor.py +266 -0
  7. gobby/cli/installers/antigravity.py +3 -9
  8. gobby/cli/installers/claude.py +9 -9
  9. gobby/cli/installers/codex.py +2 -8
  10. gobby/cli/installers/gemini.py +2 -8
  11. gobby/cli/installers/shared.py +71 -8
  12. gobby/cli/skills.py +858 -0
  13. gobby/cli/tasks/ai.py +0 -440
  14. gobby/cli/tasks/crud.py +44 -6
  15. gobby/cli/tasks/main.py +0 -4
  16. gobby/cli/tui.py +2 -2
  17. gobby/cli/utils.py +3 -3
  18. gobby/clones/__init__.py +13 -0
  19. gobby/clones/git.py +547 -0
  20. gobby/conductor/__init__.py +16 -0
  21. gobby/conductor/alerts.py +135 -0
  22. gobby/conductor/loop.py +164 -0
  23. gobby/conductor/monitors/__init__.py +11 -0
  24. gobby/conductor/monitors/agents.py +116 -0
  25. gobby/conductor/monitors/tasks.py +155 -0
  26. gobby/conductor/pricing.py +234 -0
  27. gobby/conductor/token_tracker.py +160 -0
  28. gobby/config/app.py +63 -1
  29. gobby/config/search.py +110 -0
  30. gobby/config/servers.py +1 -1
  31. gobby/config/skills.py +43 -0
  32. gobby/config/tasks.py +6 -14
  33. gobby/hooks/event_handlers.py +145 -2
  34. gobby/hooks/hook_manager.py +48 -2
  35. gobby/hooks/skill_manager.py +130 -0
  36. gobby/install/claude/hooks/hook_dispatcher.py +4 -4
  37. gobby/install/codex/hooks/hook_dispatcher.py +1 -1
  38. gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
  39. gobby/llm/claude.py +22 -34
  40. gobby/llm/claude_executor.py +46 -256
  41. gobby/llm/codex_executor.py +59 -291
  42. gobby/llm/executor.py +21 -0
  43. gobby/llm/gemini.py +134 -110
  44. gobby/llm/litellm_executor.py +143 -6
  45. gobby/llm/resolver.py +95 -33
  46. gobby/mcp_proxy/instructions.py +54 -0
  47. gobby/mcp_proxy/models.py +15 -0
  48. gobby/mcp_proxy/registries.py +68 -5
  49. gobby/mcp_proxy/server.py +33 -3
  50. gobby/mcp_proxy/services/tool_proxy.py +81 -1
  51. gobby/mcp_proxy/stdio.py +2 -1
  52. gobby/mcp_proxy/tools/__init__.py +0 -2
  53. gobby/mcp_proxy/tools/agent_messaging.py +317 -0
  54. gobby/mcp_proxy/tools/clones.py +903 -0
  55. gobby/mcp_proxy/tools/memory.py +1 -24
  56. gobby/mcp_proxy/tools/metrics.py +65 -1
  57. gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
  58. gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
  59. gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
  60. gobby/mcp_proxy/tools/session_messages.py +1 -2
  61. gobby/mcp_proxy/tools/skills/__init__.py +631 -0
  62. gobby/mcp_proxy/tools/task_orchestration.py +7 -0
  63. gobby/mcp_proxy/tools/task_readiness.py +14 -0
  64. gobby/mcp_proxy/tools/task_sync.py +1 -1
  65. gobby/mcp_proxy/tools/tasks/_context.py +0 -20
  66. gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
  67. gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
  68. gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
  69. gobby/mcp_proxy/tools/tasks/_lifecycle.py +60 -29
  70. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
  71. gobby/mcp_proxy/tools/workflows.py +1 -1
  72. gobby/mcp_proxy/tools/worktrees.py +5 -0
  73. gobby/memory/backends/__init__.py +6 -1
  74. gobby/memory/backends/mem0.py +6 -1
  75. gobby/memory/extractor.py +477 -0
  76. gobby/memory/manager.py +11 -2
  77. gobby/prompts/defaults/handoff/compact.md +63 -0
  78. gobby/prompts/defaults/handoff/session_end.md +57 -0
  79. gobby/prompts/defaults/memory/extract.md +61 -0
  80. gobby/runner.py +37 -16
  81. gobby/search/__init__.py +48 -6
  82. gobby/search/backends/__init__.py +159 -0
  83. gobby/search/backends/embedding.py +225 -0
  84. gobby/search/embeddings.py +238 -0
  85. gobby/search/models.py +148 -0
  86. gobby/search/unified.py +496 -0
  87. gobby/servers/http.py +23 -8
  88. gobby/servers/routes/admin.py +280 -0
  89. gobby/servers/routes/mcp/tools.py +241 -52
  90. gobby/servers/websocket.py +2 -2
  91. gobby/sessions/analyzer.py +2 -0
  92. gobby/sessions/transcripts/base.py +1 -0
  93. gobby/sessions/transcripts/claude.py +64 -5
  94. gobby/skills/__init__.py +91 -0
  95. gobby/skills/loader.py +685 -0
  96. gobby/skills/manager.py +384 -0
  97. gobby/skills/parser.py +258 -0
  98. gobby/skills/search.py +463 -0
  99. gobby/skills/sync.py +119 -0
  100. gobby/skills/updater.py +385 -0
  101. gobby/skills/validator.py +368 -0
  102. gobby/storage/clones.py +378 -0
  103. gobby/storage/database.py +1 -1
  104. gobby/storage/memories.py +43 -13
  105. gobby/storage/migrations.py +180 -6
  106. gobby/storage/sessions.py +73 -0
  107. gobby/storage/skills.py +749 -0
  108. gobby/storage/tasks/_crud.py +4 -4
  109. gobby/storage/tasks/_lifecycle.py +41 -6
  110. gobby/storage/tasks/_manager.py +14 -5
  111. gobby/storage/tasks/_models.py +8 -3
  112. gobby/sync/memories.py +39 -4
  113. gobby/sync/tasks.py +83 -6
  114. gobby/tasks/__init__.py +1 -2
  115. gobby/tasks/validation.py +24 -15
  116. gobby/tui/api_client.py +4 -7
  117. gobby/tui/app.py +5 -3
  118. gobby/tui/screens/orchestrator.py +1 -2
  119. gobby/tui/screens/tasks.py +2 -4
  120. gobby/tui/ws_client.py +1 -1
  121. gobby/utils/daemon_client.py +2 -2
  122. gobby/workflows/actions.py +84 -2
  123. gobby/workflows/context_actions.py +43 -0
  124. gobby/workflows/detection_helpers.py +115 -31
  125. gobby/workflows/engine.py +13 -2
  126. gobby/workflows/lifecycle_evaluator.py +29 -1
  127. gobby/workflows/loader.py +19 -6
  128. gobby/workflows/memory_actions.py +74 -0
  129. gobby/workflows/summary_actions.py +17 -0
  130. gobby/workflows/task_enforcement_actions.py +448 -6
  131. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/METADATA +82 -21
  132. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/RECORD +136 -107
  133. gobby/install/codex/prompts/forget.md +0 -7
  134. gobby/install/codex/prompts/memories.md +0 -7
  135. gobby/install/codex/prompts/recall.md +0 -7
  136. gobby/install/codex/prompts/remember.md +0 -13
  137. gobby/llm/gemini_executor.py +0 -339
  138. gobby/mcp_proxy/tools/task_expansion.py +0 -591
  139. gobby/tasks/context.py +0 -747
  140. gobby/tasks/criteria.py +0 -342
  141. gobby/tasks/expansion.py +0 -626
  142. gobby/tasks/prompts/expand.py +0 -327
  143. gobby/tasks/research.py +0 -421
  144. gobby/tasks/tdd.py +0 -352
  145. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/WHEEL +0 -0
  146. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/entry_points.txt +0 -0
  147. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/licenses/LICENSE.md +0 -0
  148. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/top_level.txt +0 -0
@@ -7,6 +7,7 @@ Uses FastAPI dependency injection via Depends() for proper testability.
7
7
 
8
8
  import json
9
9
  import logging
10
+ import re
10
11
  import time
11
12
  from typing import TYPE_CHECKING, Any
12
13
 
@@ -29,6 +30,81 @@ if TYPE_CHECKING:
29
30
  logger = logging.getLogger(__name__)
30
31
 
31
32
 
33
+ def _process_tool_proxy_result(
34
+ result: Any,
35
+ server_name: str,
36
+ tool_name: str,
37
+ response_time_ms: float,
38
+ metrics_collector: Any,
39
+ ) -> dict[str, Any]:
40
+ """
41
+ Process tool proxy result with consistent metrics, logging, and error handling.
42
+
43
+ Args:
44
+ result: The result from tool_proxy.call_tool()
45
+ server_name: Name of the MCP server
46
+ tool_name: Name of the tool called
47
+ response_time_ms: Response time in milliseconds
48
+ metrics_collector: Metrics collector instance
49
+
50
+ Returns:
51
+ Wrapped result dict with success status and response time
52
+
53
+ Raises:
54
+ HTTPException: 404 if server not found/not configured
55
+ """
56
+ # Track metrics for tool-level failures vs successes
57
+ if isinstance(result, dict) and result.get("success") is False:
58
+ metrics_collector.inc_counter("mcp_tool_calls_failed_total")
59
+
60
+ # Check structured error code first (preferred)
61
+ error_code = result.get("error_code")
62
+ if error_code in ("SERVER_NOT_FOUND", "SERVER_NOT_CONFIGURED"):
63
+ # Normalize result to standard error shape while preserving existing fields
64
+ normalized = {"success": False, "error": result.get("error", "Unknown error")}
65
+ for key, value in result.items():
66
+ if key not in normalized:
67
+ normalized[key] = value
68
+ raise HTTPException(status_code=404, detail=normalized)
69
+
70
+ # Backward compatibility: fall back to regex matching if no error_code
71
+ if not error_code:
72
+ logger.debug(
73
+ "ToolProxyService returned error without error_code - using regex fallback"
74
+ )
75
+ error_msg = str(result.get("error", ""))
76
+ if re.search(r"server\s+(not\s+found|not\s+configured)", error_msg, re.IGNORECASE):
77
+ normalized = {"success": False, "error": result.get("error", "Unknown error")}
78
+ for key, value in result.items():
79
+ if key not in normalized:
80
+ normalized[key] = value
81
+ raise HTTPException(status_code=404, detail=normalized)
82
+
83
+ # Tool-level failure (not a transport error) - return failure envelope
84
+ return {
85
+ "success": False,
86
+ "result": result,
87
+ "response_time_ms": response_time_ms,
88
+ }
89
+ else:
90
+ metrics_collector.inc_counter("mcp_tool_calls_succeeded_total")
91
+ logger.debug(
92
+ f"MCP tool call successful: {server_name}.{tool_name}",
93
+ extra={
94
+ "server": server_name,
95
+ "tool": tool_name,
96
+ "response_time_ms": response_time_ms,
97
+ },
98
+ )
99
+
100
+ # Return 200 with wrapped result for success cases
101
+ return {
102
+ "success": True,
103
+ "result": result,
104
+ "response_time_ms": response_time_ms,
105
+ }
106
+
107
+
32
108
  def create_mcp_router() -> APIRouter:
33
109
  """
34
110
  Create MCP router with endpoints using dependency injection.
@@ -74,25 +150,39 @@ def create_mcp_router() -> APIRouter:
74
150
  "response_time_ms": response_time_ms,
75
151
  }
76
152
  raise HTTPException(
77
- status_code=404, detail=f"Internal server '{server_name}' not found"
153
+ status_code=404,
154
+ detail={
155
+ "success": False,
156
+ "error": f"Internal server '{server_name}' not found",
157
+ },
78
158
  )
79
159
 
80
160
  if mcp_manager is None:
81
- raise HTTPException(status_code=503, detail="MCP manager not available")
161
+ raise HTTPException(
162
+ status_code=503, detail={"success": False, "error": "MCP manager not available"}
163
+ )
82
164
 
83
165
  # Check if server is configured
84
166
  if not mcp_manager.has_server(server_name):
85
- raise HTTPException(status_code=404, detail=f"Unknown MCP server: '{server_name}'")
167
+ raise HTTPException(
168
+ status_code=404,
169
+ detail={"success": False, "error": f"Unknown MCP server: '{server_name}'"},
170
+ )
86
171
 
87
172
  # Use ensure_connected for lazy loading - connects on-demand if not connected
88
173
  try:
89
174
  session = await mcp_manager.ensure_connected(server_name)
90
175
  except KeyError as e:
91
- raise HTTPException(status_code=404, detail=str(e)) from e
176
+ raise HTTPException(
177
+ status_code=404, detail={"success": False, "error": str(e)}
178
+ ) from e
92
179
  except Exception as e:
93
180
  raise HTTPException(
94
181
  status_code=503,
95
- detail=f"MCP server '{server_name}' connection failed: {e}",
182
+ detail={
183
+ "success": False,
184
+ "error": f"MCP server '{server_name}' connection failed: {e}",
185
+ },
96
186
  ) from e
97
187
 
98
188
  # List tools using MCP SDK
@@ -143,14 +233,17 @@ def create_mcp_router() -> APIRouter:
143
233
  exc_info=True,
144
234
  extra={"server": server_name},
145
235
  )
146
- raise HTTPException(status_code=500, detail=f"Failed to list tools: {e}") from e
236
+ raise HTTPException(
237
+ status_code=500,
238
+ detail={"success": False, "error": f"Failed to list tools: {e}"},
239
+ ) from e
147
240
 
148
241
  except HTTPException:
149
242
  raise
150
243
  except Exception as e:
151
244
  metrics.inc_counter("http_requests_errors_total")
152
245
  logger.error(f"MCP list tools error: {server_name}", exc_info=True)
153
- raise HTTPException(status_code=500, detail=str(e)) from e
246
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
154
247
 
155
248
  @router.get("/servers")
156
249
  async def list_mcp_servers(
@@ -212,7 +305,7 @@ def create_mcp_router() -> APIRouter:
212
305
  except Exception as e:
213
306
  metrics.inc_counter("http_requests_errors_total")
214
307
  logger.error(f"List MCP servers error: {e}", exc_info=True)
215
- raise HTTPException(status_code=500, detail=str(e)) from e
308
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
216
309
 
217
310
  @router.get("/tools")
218
311
  async def list_all_mcp_tools(
@@ -341,7 +434,7 @@ def create_mcp_router() -> APIRouter:
341
434
  except Exception as e:
342
435
  metrics.inc_counter("http_requests_errors_total")
343
436
  logger.error(f"List MCP tools error: {e}", exc_info=True)
344
- raise HTTPException(status_code=500, detail=str(e)) from e
437
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
345
438
 
346
439
  @router.post("/tools/schema")
347
440
  async def get_tool_schema(
@@ -370,7 +463,8 @@ def create_mcp_router() -> APIRouter:
370
463
 
371
464
  if not server_name or not tool_name:
372
465
  raise HTTPException(
373
- status_code=400, detail="Required fields: server_name, tool_name"
466
+ status_code=400,
467
+ detail={"success": False, "error": "Required fields: server_name, tool_name"},
374
468
  )
375
469
 
376
470
  # Check internal first
@@ -388,11 +482,17 @@ def create_mcp_router() -> APIRouter:
388
482
  }
389
483
  raise HTTPException(
390
484
  status_code=404,
391
- detail=f"Tool '{tool_name}' not found on server '{server_name}'",
485
+ detail={
486
+ "success": False,
487
+ "error": f"Tool '{tool_name}' not found on server '{server_name}'",
488
+ },
392
489
  )
393
490
 
394
491
  if server.mcp_manager is None:
395
- raise HTTPException(status_code=503, detail="MCP manager not available")
492
+ raise HTTPException(
493
+ status_code=503,
494
+ detail={"success": False, "error": "MCP manager not available"},
495
+ )
396
496
 
397
497
  # Get from external MCP server
398
498
  try:
@@ -406,15 +506,27 @@ def create_mcp_router() -> APIRouter:
406
506
  "response_time_ms": response_time_ms,
407
507
  }
408
508
 
509
+ except (KeyError, ValueError) as e:
510
+ # Tool or server not found - 404
511
+ raise HTTPException(
512
+ status_code=404, detail={"success": False, "error": str(e)}
513
+ ) from e
409
514
  except Exception as e:
410
- raise HTTPException(status_code=404, detail=str(e)) from e
515
+ # Connection, timeout, or internal errors - 500
516
+ logger.error(
517
+ f"Failed to get tool schema {server_name}/{tool_name}: {e}", exc_info=True
518
+ )
519
+ raise HTTPException(
520
+ status_code=500,
521
+ detail={"success": False, "error": f"Failed to get tool schema: {e}"},
522
+ ) from e
411
523
 
412
524
  except HTTPException:
413
525
  raise
414
526
  except Exception as e:
415
527
  metrics.inc_counter("http_requests_errors_total")
416
528
  logger.error(f"Get tool schema error: {e}", exc_info=True)
417
- raise HTTPException(status_code=500, detail=str(e)) from e
529
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
418
530
 
419
531
  @router.post("/tools/call")
420
532
  async def call_mcp_tool(
@@ -446,9 +558,19 @@ def create_mcp_router() -> APIRouter:
446
558
 
447
559
  if not server_name or not tool_name:
448
560
  raise HTTPException(
449
- status_code=400, detail="Required fields: server_name, tool_name"
561
+ status_code=400,
562
+ detail={"success": False, "error": "Required fields: server_name, tool_name"},
563
+ )
564
+
565
+ # Route through ToolProxyService for consistent error enrichment
566
+ if server.tool_proxy:
567
+ result = await server.tool_proxy.call_tool(server_name, tool_name, arguments)
568
+ response_time_ms = (time.perf_counter() - start_time) * 1000
569
+ return _process_tool_proxy_result(
570
+ result, server_name, tool_name, response_time_ms, metrics
450
571
  )
451
572
 
573
+ # Fallback: no tool_proxy available, use direct registry calls
452
574
  # Check internal first
453
575
  if server._internal_manager and server._internal_manager.is_internal(server_name):
454
576
  registry = server._internal_manager.get_registry(server_name)
@@ -458,10 +580,13 @@ def create_mcp_router() -> APIRouter:
458
580
  available = [t["name"] for t in registry.list_tools()]
459
581
  raise HTTPException(
460
582
  status_code=404,
461
- detail=f"Tool '{tool_name}' not found on '{server_name}'. "
462
- f"Available: {', '.join(available)}. "
463
- f"Use list_tools(server='{server_name}') to see all tools, "
464
- f"or get_tool_schema(server_name='{server_name}', tool_name='...') for full schema.",
583
+ detail={
584
+ "success": False,
585
+ "error": f"Tool '{tool_name}' not found on '{server_name}'. "
586
+ f"Available: {', '.join(available)}. "
587
+ f"Use list_tools(server='{server_name}') to see all tools, "
588
+ f"or get_tool_schema(server_name='{server_name}', tool_name='...') for full schema.",
589
+ },
465
590
  )
466
591
  try:
467
592
  result = await registry.call(tool_name, arguments or {})
@@ -481,7 +606,10 @@ def create_mcp_router() -> APIRouter:
481
606
  ) from e
482
607
 
483
608
  if server.mcp_manager is None:
484
- raise HTTPException(status_code=503, detail="MCP manager not available")
609
+ raise HTTPException(
610
+ status_code=503,
611
+ detail={"success": False, "error": "MCP manager not available"},
612
+ )
485
613
 
486
614
  # Call external MCP tool
487
615
  try:
@@ -498,7 +626,9 @@ def create_mcp_router() -> APIRouter:
498
626
  except Exception as e:
499
627
  metrics.inc_counter("mcp_tool_calls_failed_total")
500
628
  error_msg = str(e) or f"{type(e).__name__}: (no message)"
501
- raise HTTPException(status_code=500, detail=error_msg) from e
629
+ raise HTTPException(
630
+ status_code=500, detail={"success": False, "error": error_msg}
631
+ ) from e
502
632
 
503
633
  except HTTPException:
504
634
  raise
@@ -506,7 +636,9 @@ def create_mcp_router() -> APIRouter:
506
636
  metrics.inc_counter("mcp_tool_calls_failed_total")
507
637
  error_msg = str(e) or f"{type(e).__name__}: (no message)"
508
638
  logger.error(f"Call MCP tool error: {error_msg}", exc_info=True)
509
- raise HTTPException(status_code=500, detail=error_msg) from e
639
+ raise HTTPException(
640
+ status_code=500, detail={"success": False, "error": error_msg}
641
+ ) from e
510
642
 
511
643
  @router.post("/servers")
512
644
  async def add_mcp_server(
@@ -535,7 +667,10 @@ def create_mcp_router() -> APIRouter:
535
667
  transport = body.get("transport")
536
668
 
537
669
  if not name or not transport:
538
- raise HTTPException(status_code=400, detail="Required fields: name, transport")
670
+ raise HTTPException(
671
+ status_code=400,
672
+ detail={"success": False, "error": "Required fields: name, transport"},
673
+ )
539
674
 
540
675
  # Import here to avoid circular imports
541
676
  from gobby.mcp_proxy.models import MCPServerConfig
@@ -544,7 +679,11 @@ def create_mcp_router() -> APIRouter:
544
679
  project_ctx = get_project_context()
545
680
  if not project_ctx or not project_ctx.get("id"):
546
681
  raise HTTPException(
547
- status_code=400, detail="No current project found. Run 'gobby init'."
682
+ status_code=400,
683
+ detail={
684
+ "success": False,
685
+ "error": "No current project found. Run 'gobby init'.",
686
+ },
548
687
  )
549
688
  project_id = project_ctx["id"]
550
689
 
@@ -561,7 +700,10 @@ def create_mcp_router() -> APIRouter:
561
700
  )
562
701
 
563
702
  if server.mcp_manager is None:
564
- raise HTTPException(status_code=503, detail="MCP manager not available")
703
+ raise HTTPException(
704
+ status_code=503,
705
+ detail={"success": False, "error": "MCP manager not available"},
706
+ )
565
707
 
566
708
  await server.mcp_manager.add_server(config)
567
709
 
@@ -571,13 +713,13 @@ def create_mcp_router() -> APIRouter:
571
713
  }
572
714
 
573
715
  except ValueError as e:
574
- raise HTTPException(status_code=400, detail=str(e)) from e
716
+ raise HTTPException(status_code=400, detail={"success": False, "error": str(e)}) from e
575
717
  except HTTPException:
576
718
  raise
577
719
  except Exception as e:
578
720
  metrics.inc_counter("http_requests_errors_total")
579
721
  logger.error(f"Add MCP server error: {e}", exc_info=True)
580
- raise HTTPException(status_code=500, detail=str(e)) from e
722
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
581
723
 
582
724
  @router.post("/servers/import")
583
725
  async def import_mcp_server(
@@ -610,7 +752,10 @@ def create_mcp_router() -> APIRouter:
610
752
  if not from_project and not github_url and not query:
611
753
  raise HTTPException(
612
754
  status_code=400,
613
- detail="Specify at least one: from_project, github_url, or query",
755
+ detail={
756
+ "success": False,
757
+ "error": "Specify at least one: from_project, github_url, or query",
758
+ },
614
759
  )
615
760
 
616
761
  # Get current project ID from context
@@ -619,12 +764,19 @@ def create_mcp_router() -> APIRouter:
619
764
  project_ctx = get_project_context()
620
765
  if not project_ctx or not project_ctx.get("id"):
621
766
  raise HTTPException(
622
- status_code=400, detail="No current project. Run 'gobby init' first."
767
+ status_code=400,
768
+ detail={
769
+ "success": False,
770
+ "error": "No current project. Run 'gobby init' first.",
771
+ },
623
772
  )
624
773
  current_project_id = project_ctx["id"]
625
774
 
626
775
  if not server.config:
627
- raise HTTPException(status_code=500, detail="Daemon configuration not available")
776
+ raise HTTPException(
777
+ status_code=500,
778
+ detail={"success": False, "error": "Daemon configuration not available"},
779
+ )
628
780
 
629
781
  # Create importer
630
782
  from gobby.mcp_proxy.importer import MCPServerImporter
@@ -658,7 +810,7 @@ def create_mcp_router() -> APIRouter:
658
810
  except Exception as e:
659
811
  metrics.inc_counter("http_requests_errors_total")
660
812
  logger.error(f"Import MCP server error: {e}", exc_info=True)
661
- raise HTTPException(status_code=500, detail=str(e)) from e
813
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
662
814
 
663
815
  @router.delete("/servers/{name}")
664
816
  async def remove_mcp_server(
@@ -678,7 +830,10 @@ def create_mcp_router() -> APIRouter:
678
830
 
679
831
  try:
680
832
  if server.mcp_manager is None:
681
- raise HTTPException(status_code=503, detail="MCP manager not available")
833
+ raise HTTPException(
834
+ status_code=503,
835
+ detail={"success": False, "error": "MCP manager not available"},
836
+ )
682
837
 
683
838
  await server.mcp_manager.remove_server(name)
684
839
 
@@ -688,11 +843,11 @@ def create_mcp_router() -> APIRouter:
688
843
  }
689
844
 
690
845
  except ValueError as e:
691
- raise HTTPException(status_code=404, detail=str(e)) from e
846
+ raise HTTPException(status_code=404, detail={"success": False, "error": str(e)}) from e
692
847
  except Exception as e:
693
848
  metrics.inc_counter("http_requests_errors_total")
694
849
  logger.error(f"Remove MCP server error: {e}", exc_info=True)
695
- raise HTTPException(status_code=500, detail=str(e)) from e
850
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
696
851
 
697
852
  @router.post("/tools/recommend")
698
853
  async def recommend_mcp_tools(
@@ -728,7 +883,10 @@ def create_mcp_router() -> APIRouter:
728
883
  cwd = body.get("cwd")
729
884
 
730
885
  if not task_description:
731
- raise HTTPException(status_code=400, detail="Required field: task_description")
886
+ raise HTTPException(
887
+ status_code=400,
888
+ detail={"success": False, "error": "Required field: task_description"},
889
+ )
732
890
 
733
891
  # For semantic/hybrid modes, resolve project_id from cwd
734
892
  project_id = None
@@ -770,7 +928,7 @@ def create_mcp_router() -> APIRouter:
770
928
  except Exception as e:
771
929
  metrics.inc_counter("http_requests_errors_total")
772
930
  logger.error(f"Recommend tools error: {e}", exc_info=True)
773
- raise HTTPException(status_code=500, detail=str(e)) from e
931
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
774
932
 
775
933
  @router.post("/tools/search")
776
934
  async def search_mcp_tools(
@@ -804,7 +962,10 @@ def create_mcp_router() -> APIRouter:
804
962
  cwd = body.get("cwd")
805
963
 
806
964
  if not query:
807
- raise HTTPException(status_code=400, detail="Required field: query")
965
+ raise HTTPException(
966
+ status_code=400,
967
+ detail={"success": False, "error": "Required field: query"},
968
+ )
808
969
 
809
970
  # Resolve project_id from cwd
810
971
  try:
@@ -869,7 +1030,7 @@ def create_mcp_router() -> APIRouter:
869
1030
  except Exception as e:
870
1031
  metrics.inc_counter("http_requests_errors_total")
871
1032
  logger.error(f"Search tools error: {e}", exc_info=True)
872
- raise HTTPException(status_code=500, detail=str(e)) from e
1033
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
873
1034
 
874
1035
  @router.post("/tools/embed")
875
1036
  async def embed_mcp_tools(
@@ -939,7 +1100,7 @@ def create_mcp_router() -> APIRouter:
939
1100
  except Exception as e:
940
1101
  metrics.inc_counter("http_requests_errors_total")
941
1102
  logger.error(f"Embed tools error: {e}", exc_info=True)
942
- raise HTTPException(status_code=500, detail=str(e)) from e
1103
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
943
1104
 
944
1105
  @router.get("/status")
945
1106
  async def get_mcp_status(
@@ -1000,7 +1161,7 @@ def create_mcp_router() -> APIRouter:
1000
1161
  except Exception as e:
1001
1162
  metrics.inc_counter("http_requests_errors_total")
1002
1163
  logger.error(f"Get MCP status error: {e}", exc_info=True)
1003
- raise HTTPException(status_code=500, detail=str(e)) from e
1164
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
1004
1165
 
1005
1166
  @router.post("/{server_name}/tools/{tool_name}")
1006
1167
  async def mcp_proxy(
@@ -1030,9 +1191,19 @@ def create_mcp_router() -> APIRouter:
1030
1191
  args = await request.json()
1031
1192
  except (json.JSONDecodeError, ValueError) as e:
1032
1193
  raise HTTPException(
1033
- status_code=400, detail=f"Invalid JSON in request body: {e}"
1194
+ status_code=400,
1195
+ detail={"success": False, "error": f"Invalid JSON in request body: {e}"},
1034
1196
  ) from e
1035
1197
 
1198
+ # Route through ToolProxyService for consistent error enrichment
1199
+ if server.tool_proxy:
1200
+ result = await server.tool_proxy.call_tool(server_name, tool_name, args)
1201
+ response_time_ms = (time.perf_counter() - start_time) * 1000
1202
+ return _process_tool_proxy_result(
1203
+ result, server_name, tool_name, response_time_ms, metrics
1204
+ )
1205
+
1206
+ # Fallback: no tool_proxy available, use direct registry calls
1036
1207
  # Check internal registries first (gobby-tasks, gobby-memory, etc.)
1037
1208
  if server._internal_manager and server._internal_manager.is_internal(server_name):
1038
1209
  registry = server._internal_manager.get_registry(server_name)
@@ -1042,10 +1213,13 @@ def create_mcp_router() -> APIRouter:
1042
1213
  available = [t["name"] for t in registry.list_tools()]
1043
1214
  raise HTTPException(
1044
1215
  status_code=404,
1045
- detail=f"Tool '{tool_name}' not found on '{server_name}'. "
1046
- f"Available: {', '.join(available)}. "
1047
- f"Use list_tools(server='{server_name}') to see all tools, "
1048
- f"or get_tool_schema(server_name='{server_name}', tool_name='...') for full schema.",
1216
+ detail={
1217
+ "success": False,
1218
+ "error": f"Tool '{tool_name}' not found on '{server_name}'. "
1219
+ f"Available: {', '.join(available)}. "
1220
+ f"Use list_tools(server='{server_name}') to see all tools, "
1221
+ f"or get_tool_schema(server_name='{server_name}', tool_name='...') for full schema.",
1222
+ },
1049
1223
  )
1050
1224
  try:
1051
1225
  result = await registry.call(tool_name, args or {})
@@ -1059,13 +1233,22 @@ def create_mcp_router() -> APIRouter:
1059
1233
  except Exception as e:
1060
1234
  metrics.inc_counter("mcp_tool_calls_failed_total")
1061
1235
  error_msg = str(e) or f"{type(e).__name__}: (no message)"
1062
- raise HTTPException(status_code=500, detail=error_msg) from e
1236
+ raise HTTPException(
1237
+ status_code=500, detail={"success": False, "error": error_msg}
1238
+ ) from e
1063
1239
  raise HTTPException(
1064
- status_code=404, detail=f"Internal server '{server_name}' not found"
1240
+ status_code=404,
1241
+ detail={
1242
+ "success": False,
1243
+ "error": f"Internal server '{server_name}' not found",
1244
+ },
1065
1245
  )
1066
1246
 
1067
1247
  if server.mcp_manager is None:
1068
- raise HTTPException(status_code=503, detail="MCP manager not available")
1248
+ raise HTTPException(
1249
+ status_code=503,
1250
+ detail={"success": False, "error": "MCP manager not available"},
1251
+ )
1069
1252
 
1070
1253
  # Call MCP tool
1071
1254
  try:
@@ -1096,7 +1279,9 @@ def create_mcp_router() -> APIRouter:
1096
1279
  f"MCP tool not found: {server_name}.{tool_name}",
1097
1280
  extra={"server": server_name, "tool": tool_name, "error": str(e)},
1098
1281
  )
1099
- raise HTTPException(status_code=404, detail=str(e)) from e
1282
+ raise HTTPException(
1283
+ status_code=404, detail={"success": False, "error": str(e)}
1284
+ ) from e
1100
1285
  except Exception as e:
1101
1286
  metrics.inc_counter("mcp_tool_calls_failed_total")
1102
1287
  error_msg = str(e) or f"{type(e).__name__}: (no message)"
@@ -1105,7 +1290,9 @@ def create_mcp_router() -> APIRouter:
1105
1290
  exc_info=True,
1106
1291
  extra={"server": server_name, "tool": tool_name},
1107
1292
  )
1108
- raise HTTPException(status_code=500, detail=error_msg) from e
1293
+ raise HTTPException(
1294
+ status_code=500, detail={"success": False, "error": error_msg}
1295
+ ) from e
1109
1296
 
1110
1297
  except HTTPException:
1111
1298
  raise
@@ -1113,7 +1300,9 @@ def create_mcp_router() -> APIRouter:
1113
1300
  metrics.inc_counter("mcp_tool_calls_failed_total")
1114
1301
  error_msg = str(e) or f"{type(e).__name__}: (no message)"
1115
1302
  logger.error(f"MCP proxy error: {server_name}.{tool_name}", exc_info=True)
1116
- raise HTTPException(status_code=500, detail=error_msg) from e
1303
+ raise HTTPException(
1304
+ status_code=500, detail={"success": False, "error": error_msg}
1305
+ ) from e
1117
1306
 
1118
1307
  @router.post("/refresh")
1119
1308
  async def refresh_mcp_tools(
@@ -1332,6 +1521,6 @@ def create_mcp_router() -> APIRouter:
1332
1521
  except Exception as e:
1333
1522
  metrics.inc_counter("http_requests_errors_total")
1334
1523
  logger.error(f"Refresh tools error: {e}", exc_info=True)
1335
- raise HTTPException(status_code=500, detail=str(e)) from e
1524
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
1336
1525
 
1337
1526
  return router
@@ -44,7 +44,7 @@ class WebSocketConfig:
44
44
  """Configuration for WebSocket server."""
45
45
 
46
46
  host: str = "localhost"
47
- port: int = 8765
47
+ port: int = 60888
48
48
  ping_interval: int = 30 # seconds
49
49
  ping_timeout: int = 10 # seconds
50
50
  max_message_size: int = 2 * 1024 * 1024 # 2MB
@@ -64,7 +64,7 @@ class WebSocketServer:
64
64
 
65
65
  Example:
66
66
  ```python
67
- config = WebSocketConfig(host="0.0.0.0", port=8765)
67
+ config = WebSocketConfig(host="0.0.0.0", port=60888)
68
68
 
69
69
  async with WebSocketServer(config, mcp_manager) as server:
70
70
  await server.serve_forever()
@@ -32,6 +32,8 @@ class HandoffContext:
32
32
  key_decisions: list[str] | None = None
33
33
  active_worktree: dict[str, Any] | None = None
34
34
  """Worktree context if session is operating in a worktree."""
35
+ active_skills: list[str] = field(default_factory=list)
36
+ """List of skill names that were active/injected during the session."""
35
37
 
36
38
 
37
39
  class TranscriptAnalyzer:
@@ -39,6 +39,7 @@ class ParsedMessage:
39
39
  timestamp: datetime
40
40
  raw_json: dict[str, Any]
41
41
  usage: TokenUsage | None = None
42
+ tool_use_id: str | None = None
42
43
 
43
44
 
44
45
  @runtime_checkable