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,378 @@
1
+ """
2
+ Registry endpoints for MCP tool embedding, status, and refresh.
3
+
4
+ Extracted from tools.py as part of Phase 2 Strangler Fig decomposition.
5
+ These endpoints handle tool registry operations like embedding, status, and refresh.
6
+ """
7
+
8
+ import logging
9
+ import time
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from fastapi import Depends, HTTPException, Request
13
+
14
+ from gobby.servers.routes.dependencies import get_server
15
+ from gobby.utils.metrics import get_metrics_collector
16
+
17
+ if TYPE_CHECKING:
18
+ from gobby.servers.http import HTTPServer
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Module-level metrics collector (shared across all requests)
23
+ _metrics = get_metrics_collector()
24
+
25
+
26
+ async def embed_mcp_tools(
27
+ request: Request,
28
+ server: "HTTPServer" = Depends(get_server),
29
+ ) -> dict[str, Any]:
30
+ """
31
+ Generate embeddings for all tools in a project.
32
+
33
+ Request body:
34
+ {
35
+ "cwd": "/path/to/project",
36
+ "force": false
37
+ }
38
+
39
+ Returns:
40
+ Embedding generation stats
41
+ """
42
+ start_time = time.perf_counter()
43
+ _metrics.inc_counter("http_requests_total")
44
+
45
+ try:
46
+ body = await request.json()
47
+ cwd = body.get("cwd")
48
+ force = body.get("force", False)
49
+
50
+ # Resolve project_id from cwd
51
+ try:
52
+ project_id = server._resolve_project_id(None, cwd)
53
+ except ValueError as e:
54
+ return {
55
+ "success": False,
56
+ "error": str(e),
57
+ "response_time_ms": (time.perf_counter() - start_time) * 1000,
58
+ }
59
+
60
+ # Use semantic search to embed all tools
61
+ if server._tools_handler and server._tools_handler._semantic_search:
62
+ try:
63
+ stats = await server._tools_handler._semantic_search.embed_all_tools(
64
+ project_id=project_id,
65
+ mcp_manager=server._mcp_db_manager,
66
+ force=force,
67
+ )
68
+ response_time_ms = (time.perf_counter() - start_time) * 1000
69
+ return {
70
+ "success": True,
71
+ "stats": stats,
72
+ "response_time_ms": response_time_ms,
73
+ }
74
+ except Exception as e:
75
+ logger.error(f"Embedding generation failed: {e}")
76
+ return {
77
+ "success": False,
78
+ "error": str(e),
79
+ "response_time_ms": (time.perf_counter() - start_time) * 1000,
80
+ }
81
+
82
+ return {
83
+ "success": False,
84
+ "error": "Semantic search not configured",
85
+ "response_time_ms": (time.perf_counter() - start_time) * 1000,
86
+ }
87
+
88
+ except HTTPException:
89
+ raise
90
+ except Exception as e:
91
+ _metrics.inc_counter("http_requests_errors_total")
92
+ logger.error(f"Embed tools error: {e}", exc_info=True)
93
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
94
+
95
+
96
+ async def get_mcp_status(
97
+ server: "HTTPServer" = Depends(get_server),
98
+ ) -> dict[str, Any]:
99
+ """
100
+ Get MCP proxy status and health.
101
+
102
+ Returns:
103
+ Status summary with server counts and health info
104
+ """
105
+ start_time = time.perf_counter()
106
+ _metrics.inc_counter("http_requests_total")
107
+
108
+ try:
109
+ total_servers = 0
110
+ connected_servers = 0
111
+ cached_tools = 0
112
+ server_health: dict[str, dict[str, Any]] = {}
113
+
114
+ # Count internal servers
115
+ if server._internal_manager:
116
+ for registry in server._internal_manager.get_all_registries():
117
+ total_servers += 1
118
+ connected_servers += 1
119
+ cached_tools += len(registry.list_tools())
120
+ server_health[registry.name] = {
121
+ "state": "connected",
122
+ "health": "healthy",
123
+ "failures": 0,
124
+ }
125
+
126
+ # Count external servers
127
+ if server.mcp_manager:
128
+ for config in server.mcp_manager.server_configs:
129
+ total_servers += 1
130
+ health = server.mcp_manager.health.get(config.name)
131
+ is_connected = config.name in server.mcp_manager.connections
132
+ if is_connected:
133
+ connected_servers += 1
134
+
135
+ server_health[config.name] = {
136
+ "state": health.state.value if health else "unknown",
137
+ "health": health.health.value if health else "unknown",
138
+ "failures": health.consecutive_failures if health else 0,
139
+ }
140
+
141
+ response_time_ms = (time.perf_counter() - start_time) * 1000
142
+
143
+ return {
144
+ "total_servers": total_servers,
145
+ "connected_servers": connected_servers,
146
+ "cached_tools": cached_tools,
147
+ "server_health": server_health,
148
+ "response_time_ms": response_time_ms,
149
+ }
150
+
151
+ except Exception as e:
152
+ _metrics.inc_counter("http_requests_errors_total")
153
+ logger.error(f"Get MCP status error: {e}", exc_info=True)
154
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
155
+
156
+
157
+ async def refresh_mcp_tools(
158
+ request: Request,
159
+ server: "HTTPServer" = Depends(get_server),
160
+ ) -> dict[str, Any]:
161
+ """
162
+ Refresh MCP tools - detect schema changes and re-index as needed.
163
+
164
+ Request body:
165
+ {
166
+ "cwd": "/path/to/project",
167
+ "force": false,
168
+ "server": "optional-server-filter"
169
+ }
170
+
171
+ Returns:
172
+ Refresh stats with new/changed/unchanged tool counts
173
+ """
174
+ start_time = time.perf_counter()
175
+ _metrics.inc_counter("http_requests_total")
176
+
177
+ try:
178
+ body = await request.json()
179
+ cwd = body.get("cwd")
180
+ force = body.get("force", False)
181
+ server_filter = body.get("server")
182
+
183
+ # Resolve project_id from cwd
184
+ try:
185
+ project_id = server._resolve_project_id(None, cwd)
186
+ except ValueError as e:
187
+ return {
188
+ "success": False,
189
+ "error": str(e),
190
+ "response_time_ms": (time.perf_counter() - start_time) * 1000,
191
+ }
192
+
193
+ # Need schema hash manager and semantic search
194
+ if not server._mcp_db_manager:
195
+ return {
196
+ "success": False,
197
+ "error": "MCP database manager not configured",
198
+ "response_time_ms": (time.perf_counter() - start_time) * 1000,
199
+ }
200
+
201
+ from gobby.mcp_proxy.schema_hash import SchemaHashManager, compute_schema_hash
202
+
203
+ schema_hash_manager = SchemaHashManager(db=server._mcp_db_manager.db)
204
+ semantic_search = (
205
+ getattr(server._tools_handler, "_semantic_search", None)
206
+ if server._tools_handler
207
+ else None
208
+ )
209
+
210
+ stats: dict[str, Any] = {
211
+ "servers_processed": 0,
212
+ "tools_new": 0,
213
+ "tools_changed": 0,
214
+ "tools_unchanged": 0,
215
+ "tools_removed": 0,
216
+ "embeddings_generated": 0,
217
+ "by_server": {},
218
+ }
219
+
220
+ # Collect servers to process
221
+ servers_to_process: list[str] = []
222
+
223
+ # Internal servers
224
+ if server._internal_manager:
225
+ for registry in server._internal_manager.get_all_registries():
226
+ if server_filter is None or registry.name == server_filter:
227
+ servers_to_process.append(registry.name)
228
+
229
+ # External MCP servers
230
+ if server.mcp_manager:
231
+ for config in server.mcp_manager.server_configs:
232
+ if config.enabled:
233
+ if server_filter is None or config.name == server_filter:
234
+ servers_to_process.append(config.name)
235
+
236
+ # Process each server
237
+ for server_name in servers_to_process:
238
+ try:
239
+ tools: list[dict[str, Any]] = []
240
+
241
+ # Get tools from internal or external server
242
+ if server._internal_manager and server._internal_manager.is_internal(server_name):
243
+ internal_registry = server._internal_manager.get_registry(server_name)
244
+ if internal_registry:
245
+ for t in internal_registry.list_tools():
246
+ tool_name = t.get("name", "")
247
+ tools.append(
248
+ {
249
+ "name": tool_name,
250
+ "description": t.get("description"),
251
+ "inputSchema": internal_registry.get_schema(tool_name),
252
+ }
253
+ )
254
+ elif server.mcp_manager:
255
+ try:
256
+ session = await server.mcp_manager.ensure_connected(server_name)
257
+ tools_result = await session.list_tools()
258
+ for t in tools_result.tools:
259
+ schema = None
260
+ if hasattr(t, "inputSchema"):
261
+ if hasattr(t.inputSchema, "model_dump"):
262
+ schema = t.inputSchema.model_dump()
263
+ elif isinstance(t.inputSchema, dict):
264
+ schema = t.inputSchema
265
+ tools.append(
266
+ {
267
+ "name": getattr(t, "name", ""),
268
+ "description": getattr(t, "description", ""),
269
+ "inputSchema": schema,
270
+ }
271
+ )
272
+ except Exception as e:
273
+ logger.warning(f"Failed to connect to {server_name}: {e}")
274
+ stats["by_server"][server_name] = {"error": str(e)}
275
+ continue
276
+
277
+ # Check for schema changes
278
+ if force:
279
+ # Force mode: treat all as new
280
+ changes = {
281
+ "new": [t["name"] for t in tools],
282
+ "changed": [],
283
+ "unchanged": [],
284
+ }
285
+ else:
286
+ changes = schema_hash_manager.check_tools_for_changes(
287
+ server_name=server_name,
288
+ project_id=project_id,
289
+ tools=tools,
290
+ )
291
+
292
+ server_stats = {
293
+ "new": len(changes["new"]),
294
+ "changed": len(changes["changed"]),
295
+ "unchanged": len(changes["unchanged"]),
296
+ "removed": 0,
297
+ "embeddings": 0,
298
+ }
299
+
300
+ # Update schema hashes for new/changed tools
301
+ tools_to_embed = []
302
+ for tool in tools:
303
+ tool_name = tool["name"]
304
+ if tool_name in changes["new"] or tool_name in changes["changed"]:
305
+ schema = tool.get("inputSchema")
306
+ schema_hash = compute_schema_hash(schema)
307
+ schema_hash_manager.store_hash(
308
+ server_name=server_name,
309
+ tool_name=tool_name,
310
+ project_id=project_id,
311
+ schema_hash=schema_hash,
312
+ )
313
+ tools_to_embed.append(tool)
314
+ else:
315
+ # Just update verification time for unchanged
316
+ schema_hash_manager.update_verification_time(
317
+ server_name=server_name,
318
+ tool_name=tool_name,
319
+ project_id=project_id,
320
+ )
321
+
322
+ # Clean up stale hashes
323
+ valid_tool_names = [t["name"] for t in tools]
324
+ removed = schema_hash_manager.cleanup_stale_hashes(
325
+ server_name=server_name,
326
+ project_id=project_id,
327
+ valid_tool_names=valid_tool_names,
328
+ )
329
+ server_stats["removed"] = removed
330
+
331
+ # Generate embeddings for new/changed tools
332
+ if semantic_search and tools_to_embed:
333
+ for tool in tools_to_embed:
334
+ try:
335
+ await semantic_search.embed_tool(
336
+ server_name=server_name,
337
+ tool_name=tool["name"],
338
+ description=tool.get("description", ""),
339
+ input_schema=tool.get("inputSchema"),
340
+ project_id=project_id,
341
+ )
342
+ server_stats["embeddings"] += 1
343
+ except Exception as e:
344
+ logger.warning(f"Failed to embed {server_name}/{tool['name']}: {e}")
345
+
346
+ stats["by_server"][server_name] = server_stats
347
+ stats["servers_processed"] += 1
348
+ stats["tools_new"] += server_stats["new"]
349
+ stats["tools_changed"] += server_stats["changed"]
350
+ stats["tools_unchanged"] += server_stats["unchanged"]
351
+ stats["tools_removed"] += server_stats["removed"]
352
+ stats["embeddings_generated"] += server_stats["embeddings"]
353
+
354
+ except Exception as e:
355
+ logger.error(f"Error processing server {server_name}: {e}")
356
+ stats["by_server"][server_name] = {"error": str(e)}
357
+
358
+ response_time_ms = (time.perf_counter() - start_time) * 1000
359
+ return {
360
+ "success": True,
361
+ "force": force,
362
+ "stats": stats,
363
+ "response_time_ms": response_time_ms,
364
+ }
365
+
366
+ except HTTPException:
367
+ raise
368
+ except Exception as e:
369
+ _metrics.inc_counter("http_requests_errors_total")
370
+ logger.error(f"Refresh tools error: {e}", exc_info=True)
371
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
372
+
373
+
374
+ __all__ = [
375
+ "embed_mcp_tools",
376
+ "get_mcp_status",
377
+ "refresh_mcp_tools",
378
+ ]
@@ -0,0 +1,304 @@
1
+ """
2
+ Server management endpoints for MCP server lifecycle.
3
+
4
+ Extracted from tools.py as part of Phase 2 Strangler Fig decomposition.
5
+ These endpoints handle server listing, addition, import, and removal.
6
+ """
7
+
8
+ import logging
9
+ import time
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from fastapi import Depends, HTTPException, Request
13
+
14
+ from gobby.servers.routes.dependencies import get_internal_manager, get_mcp_manager, get_server
15
+ from gobby.utils.metrics import get_metrics_collector
16
+
17
+ if TYPE_CHECKING:
18
+ from gobby.mcp_proxy.manager import MCPClientManager
19
+ from gobby.mcp_proxy.registry_manager import InternalToolRegistryManager
20
+ from gobby.servers.http import HTTPServer
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Module-level metrics collector (shared across all requests)
25
+ _metrics = get_metrics_collector()
26
+
27
+
28
+ async def list_mcp_servers(
29
+ internal_manager: "InternalToolRegistryManager | None" = Depends(get_internal_manager),
30
+ mcp_manager: "MCPClientManager | None" = Depends(get_mcp_manager),
31
+ ) -> dict[str, Any]:
32
+ """
33
+ List all configured MCP servers.
34
+
35
+ Args:
36
+ internal_manager: Internal tool registry manager (injected)
37
+ mcp_manager: External MCP client manager (injected)
38
+
39
+ Returns:
40
+ List of servers with connection status
41
+ """
42
+ start_time = time.perf_counter()
43
+ _metrics.inc_counter("http_requests_total")
44
+
45
+ try:
46
+ server_list = []
47
+
48
+ # Add internal servers (gobby-tasks, gobby-memory, etc.)
49
+ if internal_manager:
50
+ for registry in internal_manager.get_all_registries():
51
+ server_list.append(
52
+ {
53
+ "name": registry.name,
54
+ "state": "connected",
55
+ "connected": True,
56
+ "transport": "internal",
57
+ }
58
+ )
59
+
60
+ # Add external MCP servers
61
+ if mcp_manager:
62
+ for config in mcp_manager.server_configs:
63
+ health = mcp_manager.health.get(config.name)
64
+ is_connected = config.name in mcp_manager.connections
65
+ server_list.append(
66
+ {
67
+ "name": config.name,
68
+ "state": health.state.value if health else "unknown",
69
+ "connected": is_connected,
70
+ "transport": config.transport,
71
+ "enabled": config.enabled,
72
+ }
73
+ )
74
+
75
+ response_time_ms = (time.perf_counter() - start_time) * 1000
76
+
77
+ return {
78
+ "servers": server_list,
79
+ "total_count": len(server_list),
80
+ "connected_count": len([s for s in server_list if s.get("connected")]),
81
+ "response_time_ms": response_time_ms,
82
+ }
83
+
84
+ except Exception as e:
85
+ _metrics.inc_counter("http_requests_errors_total")
86
+ logger.error(f"List MCP servers error: {e}", exc_info=True)
87
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
88
+
89
+
90
+ async def add_mcp_server(
91
+ request: Request,
92
+ server: "HTTPServer" = Depends(get_server),
93
+ ) -> dict[str, Any]:
94
+ """
95
+ Add a new MCP server configuration.
96
+
97
+ Request body:
98
+ {
99
+ "name": "my-server",
100
+ "transport": "http",
101
+ "url": "https://...",
102
+ "enabled": true
103
+ }
104
+
105
+ Returns:
106
+ Success status
107
+ """
108
+ _metrics.inc_counter("http_requests_total")
109
+
110
+ try:
111
+ body = await request.json()
112
+ name = body.get("name")
113
+ transport = body.get("transport")
114
+
115
+ if not name or not transport:
116
+ raise HTTPException(
117
+ status_code=400,
118
+ detail={"success": False, "error": "Required fields: name, transport"},
119
+ )
120
+
121
+ # Import here to avoid circular imports
122
+ from gobby.mcp_proxy.models import MCPServerConfig
123
+ from gobby.utils.project_context import get_project_context
124
+
125
+ project_ctx = get_project_context()
126
+ if not project_ctx or not project_ctx.get("id"):
127
+ raise HTTPException(
128
+ status_code=400,
129
+ detail={
130
+ "success": False,
131
+ "error": "No current project found. Run 'gobby init'.",
132
+ },
133
+ )
134
+ project_id = project_ctx["id"]
135
+
136
+ config = MCPServerConfig(
137
+ name=name,
138
+ project_id=project_id,
139
+ transport=transport,
140
+ url=body.get("url"),
141
+ command=body.get("command"),
142
+ args=body.get("args"),
143
+ env=body.get("env"),
144
+ headers=body.get("headers"),
145
+ enabled=body.get("enabled", True),
146
+ )
147
+
148
+ if server.mcp_manager is None:
149
+ raise HTTPException(
150
+ status_code=503,
151
+ detail={"success": False, "error": "MCP manager not available"},
152
+ )
153
+
154
+ await server.mcp_manager.add_server(config)
155
+
156
+ return {
157
+ "success": True,
158
+ "message": f"Added MCP server: {name}",
159
+ }
160
+
161
+ except ValueError as e:
162
+ raise HTTPException(status_code=400, detail={"success": False, "error": str(e)}) from e
163
+ except HTTPException:
164
+ raise
165
+ except Exception as e:
166
+ _metrics.inc_counter("http_requests_errors_total")
167
+ logger.error(f"Add MCP server error: {e}", exc_info=True)
168
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
169
+
170
+
171
+ async def import_mcp_server(
172
+ request: Request,
173
+ server: "HTTPServer" = Depends(get_server),
174
+ ) -> dict[str, Any]:
175
+ """
176
+ Import MCP server(s) from various sources.
177
+
178
+ Request body:
179
+ {
180
+ "from_project": "other-project", # Import from project
181
+ "github_url": "https://...", # Import from GitHub
182
+ "query": "supabase mcp", # Search and import
183
+ "servers": ["name1", "name2"] # Specific servers to import
184
+ }
185
+
186
+ Returns:
187
+ Import result with imported/skipped/failed lists
188
+ """
189
+ _metrics.inc_counter("http_requests_total")
190
+
191
+ try:
192
+ body = await request.json()
193
+ from_project = body.get("from_project")
194
+ github_url = body.get("github_url")
195
+ query = body.get("query")
196
+ servers = body.get("servers")
197
+
198
+ if not from_project and not github_url and not query:
199
+ raise HTTPException(
200
+ status_code=400,
201
+ detail={
202
+ "success": False,
203
+ "error": "Specify at least one: from_project, github_url, or query",
204
+ },
205
+ )
206
+
207
+ # Get current project ID from context
208
+ from gobby.utils.project_context import get_project_context
209
+
210
+ project_ctx = get_project_context()
211
+ if not project_ctx or not project_ctx.get("id"):
212
+ raise HTTPException(
213
+ status_code=400,
214
+ detail={
215
+ "success": False,
216
+ "error": "No current project. Run 'gobby init' first.",
217
+ },
218
+ )
219
+ current_project_id = project_ctx["id"]
220
+
221
+ if not server.config:
222
+ raise HTTPException(
223
+ status_code=500,
224
+ detail={"success": False, "error": "Daemon configuration not available"},
225
+ )
226
+
227
+ # Create importer
228
+ from gobby.mcp_proxy.importer import MCPServerImporter
229
+ from gobby.storage.database import LocalDatabase
230
+
231
+ db = LocalDatabase()
232
+ importer = MCPServerImporter(
233
+ config=server.config,
234
+ db=db,
235
+ current_project_id=current_project_id,
236
+ mcp_client_manager=server.mcp_manager,
237
+ )
238
+
239
+ # Execute import based on source
240
+ # Note: validation above ensures at least one of these is truthy
241
+ if from_project:
242
+ result = await importer.import_from_project(
243
+ source_project=from_project,
244
+ servers=servers,
245
+ )
246
+ elif github_url:
247
+ result = await importer.import_from_github(github_url)
248
+ else:
249
+ # query must be truthy due to earlier validation
250
+ result = await importer.import_from_query(query)
251
+
252
+ return result
253
+
254
+ except HTTPException:
255
+ raise
256
+ except Exception as e:
257
+ _metrics.inc_counter("http_requests_errors_total")
258
+ logger.error(f"Import MCP server error: {e}", exc_info=True)
259
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
260
+
261
+
262
+ async def remove_mcp_server(
263
+ name: str,
264
+ server: "HTTPServer" = Depends(get_server),
265
+ ) -> dict[str, Any]:
266
+ """
267
+ Remove an MCP server configuration.
268
+
269
+ Args:
270
+ name: Server name to remove
271
+
272
+ Returns:
273
+ Success status
274
+ """
275
+ _metrics.inc_counter("http_requests_total")
276
+
277
+ try:
278
+ if server.mcp_manager is None:
279
+ raise HTTPException(
280
+ status_code=503,
281
+ detail={"success": False, "error": "MCP manager not available"},
282
+ )
283
+
284
+ await server.mcp_manager.remove_server(name)
285
+
286
+ return {
287
+ "success": True,
288
+ "message": f"Removed MCP server: {name}",
289
+ }
290
+
291
+ except ValueError as e:
292
+ raise HTTPException(status_code=404, detail={"success": False, "error": str(e)}) from e
293
+ except Exception as e:
294
+ _metrics.inc_counter("http_requests_errors_total")
295
+ logger.error(f"Remove MCP server error: {e}", exc_info=True)
296
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
297
+
298
+
299
+ __all__ = [
300
+ "list_mcp_servers",
301
+ "add_mcp_server",
302
+ "import_mcp_server",
303
+ "remove_mcp_server",
304
+ ]