gobby 0.2.6__py3-none-any.whl → 0.2.8__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 (198) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +96 -35
  4. gobby/adapters/codex_impl/__init__.py +28 -0
  5. gobby/adapters/codex_impl/adapter.py +722 -0
  6. gobby/adapters/codex_impl/client.py +679 -0
  7. gobby/adapters/codex_impl/protocol.py +20 -0
  8. gobby/adapters/codex_impl/types.py +68 -0
  9. gobby/adapters/gemini.py +140 -38
  10. gobby/agents/definitions.py +11 -1
  11. gobby/agents/isolation.py +525 -0
  12. gobby/agents/registry.py +11 -0
  13. gobby/agents/sandbox.py +261 -0
  14. gobby/agents/session.py +1 -0
  15. gobby/agents/spawn.py +42 -287
  16. gobby/agents/spawn_executor.py +415 -0
  17. gobby/agents/spawners/__init__.py +24 -0
  18. gobby/agents/spawners/command_builder.py +189 -0
  19. gobby/agents/spawners/embedded.py +21 -2
  20. gobby/agents/spawners/headless.py +21 -2
  21. gobby/agents/spawners/macos.py +26 -1
  22. gobby/agents/spawners/prompt_manager.py +125 -0
  23. gobby/cli/__init__.py +0 -2
  24. gobby/cli/install.py +4 -4
  25. gobby/cli/installers/claude.py +6 -0
  26. gobby/cli/installers/gemini.py +6 -0
  27. gobby/cli/installers/shared.py +103 -4
  28. gobby/cli/memory.py +185 -0
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/utils.py +9 -2
  31. gobby/clones/git.py +177 -0
  32. gobby/config/__init__.py +12 -97
  33. gobby/config/app.py +10 -94
  34. gobby/config/extensions.py +2 -2
  35. gobby/config/features.py +7 -130
  36. gobby/config/skills.py +31 -0
  37. gobby/config/tasks.py +4 -28
  38. gobby/hooks/__init__.py +0 -13
  39. gobby/hooks/event_handlers.py +150 -8
  40. gobby/hooks/hook_manager.py +21 -3
  41. gobby/hooks/plugins.py +1 -1
  42. gobby/hooks/webhooks.py +1 -1
  43. gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
  44. gobby/llm/resolver.py +3 -2
  45. gobby/mcp_proxy/importer.py +62 -4
  46. gobby/mcp_proxy/instructions.py +4 -2
  47. gobby/mcp_proxy/registries.py +22 -8
  48. gobby/mcp_proxy/services/recommendation.py +43 -11
  49. gobby/mcp_proxy/tools/agent_messaging.py +93 -44
  50. gobby/mcp_proxy/tools/agents.py +76 -740
  51. gobby/mcp_proxy/tools/artifacts.py +43 -9
  52. gobby/mcp_proxy/tools/clones.py +0 -385
  53. gobby/mcp_proxy/tools/memory.py +2 -2
  54. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  55. gobby/mcp_proxy/tools/sessions/_commits.py +239 -0
  56. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  57. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  58. gobby/mcp_proxy/tools/sessions/_handoff.py +503 -0
  59. gobby/mcp_proxy/tools/sessions/_messages.py +166 -0
  60. gobby/mcp_proxy/tools/skills/__init__.py +14 -29
  61. gobby/mcp_proxy/tools/spawn_agent.py +455 -0
  62. gobby/mcp_proxy/tools/tasks/_context.py +18 -0
  63. gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
  64. gobby/mcp_proxy/tools/tasks/_lifecycle.py +79 -30
  65. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
  66. gobby/mcp_proxy/tools/tasks/_session.py +22 -7
  67. gobby/mcp_proxy/tools/workflows.py +84 -34
  68. gobby/mcp_proxy/tools/worktrees.py +32 -350
  69. gobby/memory/extractor.py +15 -1
  70. gobby/memory/ingestion/__init__.py +5 -0
  71. gobby/memory/ingestion/multimodal.py +221 -0
  72. gobby/memory/manager.py +62 -283
  73. gobby/memory/search/__init__.py +10 -0
  74. gobby/memory/search/coordinator.py +248 -0
  75. gobby/memory/services/__init__.py +5 -0
  76. gobby/memory/services/crossref.py +142 -0
  77. gobby/prompts/loader.py +5 -2
  78. gobby/runner.py +13 -0
  79. gobby/servers/http.py +1 -4
  80. gobby/servers/routes/admin.py +14 -0
  81. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  82. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  83. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  84. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  85. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  86. gobby/servers/routes/mcp/hooks.py +51 -4
  87. gobby/servers/routes/mcp/tools.py +48 -1506
  88. gobby/servers/websocket.py +57 -1
  89. gobby/sessions/analyzer.py +2 -2
  90. gobby/sessions/lifecycle.py +1 -1
  91. gobby/sessions/manager.py +9 -0
  92. gobby/sessions/processor.py +10 -0
  93. gobby/sessions/transcripts/base.py +1 -0
  94. gobby/sessions/transcripts/claude.py +15 -5
  95. gobby/sessions/transcripts/gemini.py +100 -34
  96. gobby/skills/parser.py +30 -2
  97. gobby/storage/database.py +9 -2
  98. gobby/storage/memories.py +32 -21
  99. gobby/storage/migrations.py +174 -368
  100. gobby/storage/sessions.py +45 -7
  101. gobby/storage/skills.py +80 -7
  102. gobby/storage/tasks/_lifecycle.py +18 -3
  103. gobby/sync/memories.py +1 -1
  104. gobby/tasks/external_validator.py +1 -1
  105. gobby/tasks/validation.py +22 -20
  106. gobby/tools/summarizer.py +91 -10
  107. gobby/utils/project_context.py +2 -3
  108. gobby/utils/status.py +13 -0
  109. gobby/workflows/actions.py +221 -1217
  110. gobby/workflows/artifact_actions.py +31 -0
  111. gobby/workflows/autonomous_actions.py +11 -0
  112. gobby/workflows/context_actions.py +50 -1
  113. gobby/workflows/detection_helpers.py +38 -24
  114. gobby/workflows/enforcement/__init__.py +47 -0
  115. gobby/workflows/enforcement/blocking.py +281 -0
  116. gobby/workflows/enforcement/commit_policy.py +283 -0
  117. gobby/workflows/enforcement/handlers.py +269 -0
  118. gobby/workflows/enforcement/task_policy.py +542 -0
  119. gobby/workflows/engine.py +93 -0
  120. gobby/workflows/evaluator.py +110 -0
  121. gobby/workflows/git_utils.py +106 -0
  122. gobby/workflows/hooks.py +41 -0
  123. gobby/workflows/llm_actions.py +30 -0
  124. gobby/workflows/mcp_actions.py +20 -1
  125. gobby/workflows/memory_actions.py +91 -0
  126. gobby/workflows/safe_evaluator.py +191 -0
  127. gobby/workflows/session_actions.py +44 -0
  128. gobby/workflows/state_actions.py +60 -1
  129. gobby/workflows/stop_signal_actions.py +55 -0
  130. gobby/workflows/summary_actions.py +217 -51
  131. gobby/workflows/task_sync_actions.py +347 -0
  132. gobby/workflows/todo_actions.py +34 -1
  133. gobby/workflows/webhook_actions.py +185 -0
  134. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/METADATA +6 -1
  135. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/RECORD +139 -163
  136. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/WHEEL +1 -1
  137. gobby/adapters/codex.py +0 -1332
  138. gobby/cli/tui.py +0 -34
  139. gobby/install/claude/commands/gobby/bug.md +0 -51
  140. gobby/install/claude/commands/gobby/chore.md +0 -51
  141. gobby/install/claude/commands/gobby/epic.md +0 -52
  142. gobby/install/claude/commands/gobby/eval.md +0 -235
  143. gobby/install/claude/commands/gobby/feat.md +0 -49
  144. gobby/install/claude/commands/gobby/nit.md +0 -52
  145. gobby/install/claude/commands/gobby/ref.md +0 -52
  146. gobby/mcp_proxy/tools/session_messages.py +0 -1055
  147. gobby/prompts/defaults/expansion/system.md +0 -119
  148. gobby/prompts/defaults/expansion/user.md +0 -48
  149. gobby/prompts/defaults/external_validation/agent.md +0 -72
  150. gobby/prompts/defaults/external_validation/external.md +0 -63
  151. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  152. gobby/prompts/defaults/external_validation/system.md +0 -6
  153. gobby/prompts/defaults/features/import_mcp.md +0 -22
  154. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  155. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  156. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  157. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  158. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  159. gobby/prompts/defaults/features/server_description.md +0 -20
  160. gobby/prompts/defaults/features/server_description_system.md +0 -6
  161. gobby/prompts/defaults/features/task_description.md +0 -31
  162. gobby/prompts/defaults/features/task_description_system.md +0 -6
  163. gobby/prompts/defaults/features/tool_summary.md +0 -17
  164. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  165. gobby/prompts/defaults/handoff/compact.md +0 -63
  166. gobby/prompts/defaults/handoff/session_end.md +0 -57
  167. gobby/prompts/defaults/memory/extract.md +0 -61
  168. gobby/prompts/defaults/research/step.md +0 -58
  169. gobby/prompts/defaults/validation/criteria.md +0 -47
  170. gobby/prompts/defaults/validation/validate.md +0 -38
  171. gobby/storage/migrations_legacy.py +0 -1359
  172. gobby/tui/__init__.py +0 -5
  173. gobby/tui/api_client.py +0 -278
  174. gobby/tui/app.py +0 -329
  175. gobby/tui/screens/__init__.py +0 -25
  176. gobby/tui/screens/agents.py +0 -333
  177. gobby/tui/screens/chat.py +0 -450
  178. gobby/tui/screens/dashboard.py +0 -377
  179. gobby/tui/screens/memory.py +0 -305
  180. gobby/tui/screens/metrics.py +0 -231
  181. gobby/tui/screens/orchestrator.py +0 -903
  182. gobby/tui/screens/sessions.py +0 -412
  183. gobby/tui/screens/tasks.py +0 -440
  184. gobby/tui/screens/workflows.py +0 -289
  185. gobby/tui/screens/worktrees.py +0 -174
  186. gobby/tui/widgets/__init__.py +0 -21
  187. gobby/tui/widgets/chat.py +0 -210
  188. gobby/tui/widgets/conductor.py +0 -104
  189. gobby/tui/widgets/menu.py +0 -132
  190. gobby/tui/widgets/message_panel.py +0 -160
  191. gobby/tui/widgets/review_gate.py +0 -224
  192. gobby/tui/widgets/task_tree.py +0 -99
  193. gobby/tui/widgets/token_budget.py +0 -166
  194. gobby/tui/ws_client.py +0 -258
  195. gobby/workflows/task_enforcement_actions.py +0 -1343
  196. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
  197. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
  198. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,405 @@
1
+ """
2
+ Discovery endpoints for MCP tool and server listing.
3
+
4
+ Extracted from tools.py as part of Phase 2 Strangler Fig decomposition.
5
+ These endpoints handle tool discovery, search, and recommendations.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import logging
11
+ import time
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from fastapi import Depends, HTTPException, Request
15
+
16
+ from gobby.servers.routes.dependencies import get_metrics_manager, get_server
17
+ from gobby.utils.metrics import get_metrics_collector
18
+
19
+ if TYPE_CHECKING:
20
+ from gobby.mcp_proxy.metrics import ToolMetricsManager
21
+ from gobby.servers.http import HTTPServer
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Module-level metrics collector (shared across all requests)
26
+ _metrics = get_metrics_collector()
27
+
28
+ # Set to keep background tasks alive (prevent garbage collection)
29
+ _background_tasks: set[asyncio.Task[Any]] = set()
30
+
31
+
32
+ async def list_all_mcp_tools(
33
+ server_filter: str | None = None,
34
+ include_metrics: bool = False,
35
+ project_id: str | None = None,
36
+ server: "HTTPServer" = Depends(get_server),
37
+ metrics_manager: "ToolMetricsManager | None" = Depends(get_metrics_manager),
38
+ ) -> dict[str, Any]:
39
+ """
40
+ List tools from MCP servers.
41
+
42
+ Args:
43
+ server_filter: Optional server name to filter by
44
+ include_metrics: When True, include call_count, success_rate, avg_latency for each tool
45
+ project_id: Project ID for metrics lookup (uses current project if not specified)
46
+ server: HTTPServer instance (injected)
47
+ metrics_manager: Tool metrics manager (injected)
48
+
49
+ Returns:
50
+ Dict of server names to tool lists
51
+ """
52
+ start_time = time.perf_counter()
53
+ _metrics.inc_counter("http_requests_total")
54
+
55
+ try:
56
+ tools_by_server: dict[str, list[dict[str, Any]]] = {}
57
+
58
+ # Resolve project_id for metrics lookup
59
+ resolved_project_id = None
60
+ if include_metrics:
61
+ try:
62
+ resolved_project_id = server._resolve_project_id(project_id, cwd=None)
63
+ except ValueError:
64
+ # Project not initialized; skip metrics enrichment
65
+ resolved_project_id = None
66
+
67
+ # If specific server requested
68
+ if server_filter:
69
+ # Check internal first
70
+ if server._internal_manager and server._internal_manager.is_internal(server_filter):
71
+ registry = server._internal_manager.get_registry(server_filter)
72
+ if registry:
73
+ tools_by_server[server_filter] = registry.list_tools()
74
+ elif server.mcp_manager and server.mcp_manager.has_server(server_filter):
75
+ # Check if server is enabled before attempting connection
76
+ server_config = server.mcp_manager._configs.get(server_filter)
77
+ if server_config and not server_config.enabled:
78
+ tools_by_server[server_filter] = []
79
+ else:
80
+ try:
81
+ # Use ensure_connected for lazy loading
82
+ session = await server.mcp_manager.ensure_connected(server_filter)
83
+ tools_result = await session.list_tools()
84
+ tools_list = []
85
+ for t in tools_result.tools:
86
+ desc = getattr(t, "description", "") or ""
87
+ tools_list.append(
88
+ {
89
+ "name": t.name,
90
+ "brief": desc[:100],
91
+ }
92
+ )
93
+ tools_by_server[server_filter] = tools_list
94
+ except Exception as e:
95
+ logger.warning(f"Failed to list tools from {server_filter}: {e}")
96
+ tools_by_server[server_filter] = []
97
+ else:
98
+ # Get tools from all servers
99
+ # Internal servers
100
+ if server._internal_manager:
101
+ for registry in server._internal_manager.get_all_registries():
102
+ tools_by_server[registry.name] = registry.list_tools()
103
+
104
+ # External MCP servers - use ensure_connected for lazy loading
105
+ if server.mcp_manager:
106
+ for config in server.mcp_manager.server_configs:
107
+ if config.enabled:
108
+ try:
109
+ session = await server.mcp_manager.ensure_connected(config.name)
110
+ tools_result = await session.list_tools()
111
+ tools_list = []
112
+ for t in tools_result.tools:
113
+ desc = getattr(t, "description", "") or ""
114
+ tools_list.append(
115
+ {
116
+ "name": t.name,
117
+ "brief": desc[:100],
118
+ }
119
+ )
120
+ tools_by_server[config.name] = tools_list
121
+ except Exception as e:
122
+ logger.warning(f"Failed to list tools from {config.name}: {e}")
123
+ tools_by_server[config.name] = []
124
+
125
+ # Enrich with metrics if requested
126
+ if include_metrics and metrics_manager and resolved_project_id:
127
+ # Get all metrics for this project
128
+ metrics_data = metrics_manager.get_metrics(project_id=resolved_project_id)
129
+ metrics_by_key = {
130
+ (m["server_name"], m["tool_name"]): m for m in metrics_data.get("tools", [])
131
+ }
132
+
133
+ for server_name, tools_list in tools_by_server.items():
134
+ for tool in tools_list:
135
+ # Guard against non-dict or missing-name entries
136
+ if not isinstance(tool, dict) or "name" not in tool:
137
+ continue
138
+ tool_name = tool.get("name")
139
+ key = (server_name, tool_name)
140
+ if key in metrics_by_key:
141
+ m = metrics_by_key[key]
142
+ tool["call_count"] = m.get("call_count", 0)
143
+ tool["success_rate"] = m.get("success_rate")
144
+ tool["avg_latency_ms"] = m.get("avg_latency_ms")
145
+ else:
146
+ tool["call_count"] = 0
147
+ tool["success_rate"] = None
148
+ tool["avg_latency_ms"] = None
149
+
150
+ response_time_ms = (time.perf_counter() - start_time) * 1000
151
+
152
+ return {
153
+ "tools": tools_by_server,
154
+ "response_time_ms": response_time_ms,
155
+ }
156
+
157
+ except Exception as e:
158
+ _metrics.inc_counter("http_requests_errors_total")
159
+ logger.error(f"List MCP tools error: {e}", exc_info=True)
160
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
161
+
162
+
163
+ async def recommend_mcp_tools(
164
+ request: Request,
165
+ server: "HTTPServer" = Depends(get_server),
166
+ ) -> dict[str, Any]:
167
+ """
168
+ Get AI-powered tool recommendations for a task.
169
+
170
+ Request body:
171
+ {
172
+ "task_description": "I need to query a database",
173
+ "agent_id": "optional-agent-id",
174
+ "search_mode": "llm" | "semantic" | "hybrid",
175
+ "top_k": 10,
176
+ "min_similarity": 0.3,
177
+ "cwd": "/path/to/project"
178
+ }
179
+
180
+ Returns:
181
+ List of tool recommendations
182
+ """
183
+ start_time = time.perf_counter()
184
+ _metrics.inc_counter("http_requests_total")
185
+
186
+ try:
187
+ try:
188
+ body = await request.json()
189
+ except json.JSONDecodeError as err:
190
+ raise HTTPException(
191
+ status_code=400,
192
+ detail={"success": False, "error": "Malformed JSON", "message": str(err)},
193
+ ) from err
194
+
195
+ task_description = body.get("task_description")
196
+ agent_id = body.get("agent_id")
197
+ search_mode = body.get("search_mode", "llm")
198
+ top_k = body.get("top_k", 10)
199
+ min_similarity = body.get("min_similarity", 0.3)
200
+ cwd = body.get("cwd")
201
+
202
+ if not task_description:
203
+ raise HTTPException(
204
+ status_code=400,
205
+ detail={"success": False, "error": "Required field: task_description"},
206
+ )
207
+
208
+ # For semantic/hybrid modes, resolve project_id from cwd
209
+ project_id = None
210
+ if search_mode in ("semantic", "hybrid"):
211
+ try:
212
+ project_id = server._resolve_project_id(None, cwd)
213
+ except ValueError as e:
214
+ response_time_ms = (time.perf_counter() - start_time) * 1000
215
+ raise HTTPException(
216
+ status_code=400,
217
+ detail={
218
+ "success": False,
219
+ "error": str(e),
220
+ "task": task_description,
221
+ "response_time_ms": response_time_ms,
222
+ },
223
+ ) from e
224
+
225
+ # Use tools handler if available
226
+ if server._tools_handler:
227
+ result = await server._tools_handler.recommend_tools(
228
+ task_description=task_description,
229
+ agent_id=agent_id,
230
+ search_mode=search_mode,
231
+ top_k=top_k,
232
+ min_similarity=min_similarity,
233
+ project_id=project_id,
234
+ )
235
+ response_time_ms = (time.perf_counter() - start_time) * 1000
236
+ result["response_time_ms"] = response_time_ms
237
+ return result
238
+
239
+ # Fallback: no tools handler
240
+ return {
241
+ "success": False,
242
+ "error": "Tools handler not initialized",
243
+ "recommendations": [],
244
+ "response_time_ms": (time.perf_counter() - start_time) * 1000,
245
+ }
246
+
247
+ except HTTPException:
248
+ raise
249
+ except Exception as e:
250
+ _metrics.inc_counter("http_requests_errors_total")
251
+ logger.error(f"Recommend tools error: {e}", exc_info=True)
252
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
253
+
254
+
255
+ async def search_mcp_tools(
256
+ request: Request,
257
+ server: "HTTPServer" = Depends(get_server),
258
+ ) -> dict[str, Any]:
259
+ """
260
+ Search for tools using semantic similarity.
261
+
262
+ Request body:
263
+ {
264
+ "query": "create a file",
265
+ "top_k": 10,
266
+ "min_similarity": 0.0,
267
+ "server": "optional-server-filter",
268
+ "cwd": "/path/to/project"
269
+ }
270
+
271
+ Returns:
272
+ List of matching tools with similarity scores
273
+ """
274
+ start_time = time.perf_counter()
275
+ _metrics.inc_counter("http_requests_total")
276
+
277
+ try:
278
+ try:
279
+ body = await request.json()
280
+ except json.JSONDecodeError as err:
281
+ raise HTTPException(
282
+ status_code=400,
283
+ detail={"success": False, "error": "Malformed JSON", "message": str(err)},
284
+ ) from err
285
+
286
+ query = body.get("query")
287
+ top_k = body.get("top_k", 10)
288
+ min_similarity = body.get("min_similarity", 0.0)
289
+ server_filter = body.get("server")
290
+ cwd = body.get("cwd")
291
+
292
+ if not query:
293
+ raise HTTPException(
294
+ status_code=400,
295
+ detail={"success": False, "error": "Required field: query"},
296
+ )
297
+
298
+ # Resolve project_id from cwd
299
+ try:
300
+ project_id = server._resolve_project_id(None, cwd)
301
+ except ValueError as e:
302
+ response_time_ms = (time.perf_counter() - start_time) * 1000
303
+ raise HTTPException(
304
+ status_code=400,
305
+ detail={
306
+ "success": False,
307
+ "error": str(e),
308
+ "query": query,
309
+ "response_time_ms": response_time_ms,
310
+ },
311
+ ) from e
312
+
313
+ # Use semantic search directly if available
314
+ if server._tools_handler and server._tools_handler._semantic_search:
315
+ try:
316
+ import asyncio
317
+
318
+ semantic_search = server._tools_handler._semantic_search
319
+
320
+ # Check if embeddings exist - if not, trigger background generation
321
+ existing = semantic_search.get_embeddings_for_project(project_id)
322
+ if not existing and server._mcp_db_manager:
323
+ logger.info(
324
+ f"No embeddings for project {project_id}, triggering background generation..."
325
+ )
326
+
327
+ # Wrapper to log exceptions from background embedding generation
328
+ async def _embed_with_error_handling(proj_id: str) -> None:
329
+ try:
330
+ await semantic_search.embed_all_tools(
331
+ project_id=proj_id,
332
+ mcp_manager=server._mcp_db_manager,
333
+ force=False,
334
+ )
335
+ except Exception as e:
336
+ logger.error(
337
+ f"Background embedding generation failed for project {proj_id}: {e}",
338
+ exc_info=True,
339
+ )
340
+
341
+ # Trigger embedding generation as background task (non-blocking)
342
+ task = asyncio.create_task(_embed_with_error_handling(project_id))
343
+ _background_tasks.add(task)
344
+ task.add_done_callback(_background_tasks.discard)
345
+ # Return early indicating embeddings are being generated
346
+ response_time_ms = (time.perf_counter() - start_time) * 1000
347
+ return {
348
+ "success": True,
349
+ "embeddings_generating": True,
350
+ "query": query,
351
+ "results": [],
352
+ "total_results": 0,
353
+ "message": "Embeddings are being generated. Please retry in a few seconds.",
354
+ "response_time_ms": response_time_ms,
355
+ }
356
+
357
+ results = await semantic_search.search_tools(
358
+ query=query,
359
+ project_id=project_id,
360
+ top_k=top_k,
361
+ min_similarity=min_similarity,
362
+ server_filter=server_filter,
363
+ )
364
+ response_time_ms = (time.perf_counter() - start_time) * 1000
365
+ return {
366
+ "success": True,
367
+ "query": query,
368
+ "results": [r.to_dict() for r in results],
369
+ "total_results": len(results),
370
+ "response_time_ms": response_time_ms,
371
+ }
372
+ except Exception as e:
373
+ logger.error(f"Semantic search failed: {e}")
374
+ response_time_ms = (time.perf_counter() - start_time) * 1000
375
+ raise HTTPException(
376
+ status_code=500,
377
+ detail={
378
+ "success": False,
379
+ "error": str(e),
380
+ "query": query,
381
+ "response_time_ms": response_time_ms,
382
+ },
383
+ ) from e
384
+
385
+ # Fallback: no semantic search
386
+ return {
387
+ "success": False,
388
+ "error": "Semantic search not configured",
389
+ "results": [],
390
+ "response_time_ms": (time.perf_counter() - start_time) * 1000,
391
+ }
392
+
393
+ except HTTPException:
394
+ raise
395
+ except Exception as e:
396
+ _metrics.inc_counter("http_requests_errors_total")
397
+ logger.error(f"Search tools error: {e}", exc_info=True)
398
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
399
+
400
+
401
+ __all__ = [
402
+ "list_all_mcp_tools",
403
+ "recommend_mcp_tools",
404
+ "search_mcp_tools",
405
+ ]