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.
- gobby/adapters/claude_code.py +13 -4
- gobby/adapters/codex.py +43 -3
- gobby/agents/runner.py +8 -0
- gobby/cli/__init__.py +6 -0
- gobby/cli/clones.py +419 -0
- gobby/cli/conductor.py +266 -0
- gobby/cli/installers/antigravity.py +3 -9
- gobby/cli/installers/claude.py +9 -9
- gobby/cli/installers/codex.py +2 -8
- gobby/cli/installers/gemini.py +2 -8
- gobby/cli/installers/shared.py +71 -8
- gobby/cli/skills.py +858 -0
- gobby/cli/tasks/ai.py +0 -440
- gobby/cli/tasks/crud.py +44 -6
- gobby/cli/tasks/main.py +0 -4
- gobby/cli/tui.py +2 -2
- gobby/cli/utils.py +3 -3
- gobby/clones/__init__.py +13 -0
- gobby/clones/git.py +547 -0
- gobby/conductor/__init__.py +16 -0
- gobby/conductor/alerts.py +135 -0
- gobby/conductor/loop.py +164 -0
- gobby/conductor/monitors/__init__.py +11 -0
- gobby/conductor/monitors/agents.py +116 -0
- gobby/conductor/monitors/tasks.py +155 -0
- gobby/conductor/pricing.py +234 -0
- gobby/conductor/token_tracker.py +160 -0
- gobby/config/app.py +63 -1
- gobby/config/search.py +110 -0
- gobby/config/servers.py +1 -1
- gobby/config/skills.py +43 -0
- gobby/config/tasks.py +6 -14
- gobby/hooks/event_handlers.py +145 -2
- gobby/hooks/hook_manager.py +48 -2
- gobby/hooks/skill_manager.py +130 -0
- gobby/install/claude/hooks/hook_dispatcher.py +4 -4
- gobby/install/codex/hooks/hook_dispatcher.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
- gobby/llm/claude.py +22 -34
- gobby/llm/claude_executor.py +46 -256
- gobby/llm/codex_executor.py +59 -291
- gobby/llm/executor.py +21 -0
- gobby/llm/gemini.py +134 -110
- gobby/llm/litellm_executor.py +143 -6
- gobby/llm/resolver.py +95 -33
- gobby/mcp_proxy/instructions.py +54 -0
- gobby/mcp_proxy/models.py +15 -0
- gobby/mcp_proxy/registries.py +68 -5
- gobby/mcp_proxy/server.py +33 -3
- gobby/mcp_proxy/services/tool_proxy.py +81 -1
- gobby/mcp_proxy/stdio.py +2 -1
- gobby/mcp_proxy/tools/__init__.py +0 -2
- gobby/mcp_proxy/tools/agent_messaging.py +317 -0
- gobby/mcp_proxy/tools/clones.py +903 -0
- gobby/mcp_proxy/tools/memory.py +1 -24
- gobby/mcp_proxy/tools/metrics.py +65 -1
- gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
- gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
- gobby/mcp_proxy/tools/session_messages.py +1 -2
- gobby/mcp_proxy/tools/skills/__init__.py +631 -0
- gobby/mcp_proxy/tools/task_orchestration.py +7 -0
- gobby/mcp_proxy/tools/task_readiness.py +14 -0
- gobby/mcp_proxy/tools/task_sync.py +1 -1
- gobby/mcp_proxy/tools/tasks/_context.py +0 -20
- gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
- gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +60 -29
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
- gobby/mcp_proxy/tools/workflows.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +5 -0
- gobby/memory/backends/__init__.py +6 -1
- gobby/memory/backends/mem0.py +6 -1
- gobby/memory/extractor.py +477 -0
- gobby/memory/manager.py +11 -2
- gobby/prompts/defaults/handoff/compact.md +63 -0
- gobby/prompts/defaults/handoff/session_end.md +57 -0
- gobby/prompts/defaults/memory/extract.md +61 -0
- gobby/runner.py +37 -16
- gobby/search/__init__.py +48 -6
- gobby/search/backends/__init__.py +159 -0
- gobby/search/backends/embedding.py +225 -0
- gobby/search/embeddings.py +238 -0
- gobby/search/models.py +148 -0
- gobby/search/unified.py +496 -0
- gobby/servers/http.py +23 -8
- gobby/servers/routes/admin.py +280 -0
- gobby/servers/routes/mcp/tools.py +241 -52
- gobby/servers/websocket.py +2 -2
- gobby/sessions/analyzer.py +2 -0
- gobby/sessions/transcripts/base.py +1 -0
- gobby/sessions/transcripts/claude.py +64 -5
- gobby/skills/__init__.py +91 -0
- gobby/skills/loader.py +685 -0
- gobby/skills/manager.py +384 -0
- gobby/skills/parser.py +258 -0
- gobby/skills/search.py +463 -0
- gobby/skills/sync.py +119 -0
- gobby/skills/updater.py +385 -0
- gobby/skills/validator.py +368 -0
- gobby/storage/clones.py +378 -0
- gobby/storage/database.py +1 -1
- gobby/storage/memories.py +43 -13
- gobby/storage/migrations.py +180 -6
- gobby/storage/sessions.py +73 -0
- gobby/storage/skills.py +749 -0
- gobby/storage/tasks/_crud.py +4 -4
- gobby/storage/tasks/_lifecycle.py +41 -6
- gobby/storage/tasks/_manager.py +14 -5
- gobby/storage/tasks/_models.py +8 -3
- gobby/sync/memories.py +39 -4
- gobby/sync/tasks.py +83 -6
- gobby/tasks/__init__.py +1 -2
- gobby/tasks/validation.py +24 -15
- gobby/tui/api_client.py +4 -7
- gobby/tui/app.py +5 -3
- gobby/tui/screens/orchestrator.py +1 -2
- gobby/tui/screens/tasks.py +2 -4
- gobby/tui/ws_client.py +1 -1
- gobby/utils/daemon_client.py +2 -2
- gobby/workflows/actions.py +84 -2
- gobby/workflows/context_actions.py +43 -0
- gobby/workflows/detection_helpers.py +115 -31
- gobby/workflows/engine.py +13 -2
- gobby/workflows/lifecycle_evaluator.py +29 -1
- gobby/workflows/loader.py +19 -6
- gobby/workflows/memory_actions.py +74 -0
- gobby/workflows/summary_actions.py +17 -0
- gobby/workflows/task_enforcement_actions.py +448 -6
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/METADATA +82 -21
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/RECORD +136 -107
- gobby/install/codex/prompts/forget.md +0 -7
- gobby/install/codex/prompts/memories.md +0 -7
- gobby/install/codex/prompts/recall.md +0 -7
- gobby/install/codex/prompts/remember.md +0 -13
- gobby/llm/gemini_executor.py +0 -339
- gobby/mcp_proxy/tools/task_expansion.py +0 -591
- gobby/tasks/context.py +0 -747
- gobby/tasks/criteria.py +0 -342
- gobby/tasks/expansion.py +0 -626
- gobby/tasks/prompts/expand.py +0 -327
- gobby/tasks/research.py +0 -421
- gobby/tasks/tdd.py +0 -352
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/WHEEL +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/licenses/LICENSE.md +0 -0
- {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,
|
|
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(
|
|
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(
|
|
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(
|
|
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=
|
|
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(
|
|
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,
|
|
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=
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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=
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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=
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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=
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
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(
|
|
1236
|
+
raise HTTPException(
|
|
1237
|
+
status_code=500, detail={"success": False, "error": error_msg}
|
|
1238
|
+
) from e
|
|
1063
1239
|
raise HTTPException(
|
|
1064
|
-
status_code=404,
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
gobby/servers/websocket.py
CHANGED
|
@@ -44,7 +44,7 @@ class WebSocketConfig:
|
|
|
44
44
|
"""Configuration for WebSocket server."""
|
|
45
45
|
|
|
46
46
|
host: str = "localhost"
|
|
47
|
-
port: int =
|
|
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=
|
|
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()
|
gobby/sessions/analyzer.py
CHANGED
|
@@ -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:
|