gobby 0.2.5__py3-none-any.whl → 0.2.7__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 (244) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +13 -4
  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/agents/definitions.py +11 -1
  10. gobby/agents/isolation.py +395 -0
  11. gobby/agents/runner.py +8 -0
  12. gobby/agents/sandbox.py +261 -0
  13. gobby/agents/spawn.py +42 -287
  14. gobby/agents/spawn_executor.py +385 -0
  15. gobby/agents/spawners/__init__.py +24 -0
  16. gobby/agents/spawners/command_builder.py +189 -0
  17. gobby/agents/spawners/embedded.py +21 -2
  18. gobby/agents/spawners/headless.py +21 -2
  19. gobby/agents/spawners/prompt_manager.py +125 -0
  20. gobby/cli/__init__.py +6 -0
  21. gobby/cli/clones.py +419 -0
  22. gobby/cli/conductor.py +266 -0
  23. gobby/cli/install.py +4 -4
  24. gobby/cli/installers/antigravity.py +3 -9
  25. gobby/cli/installers/claude.py +15 -9
  26. gobby/cli/installers/codex.py +2 -8
  27. gobby/cli/installers/gemini.py +8 -8
  28. gobby/cli/installers/shared.py +175 -13
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/skills.py +858 -0
  31. gobby/cli/tasks/ai.py +0 -440
  32. gobby/cli/tasks/crud.py +44 -6
  33. gobby/cli/tasks/main.py +0 -4
  34. gobby/cli/tui.py +2 -2
  35. gobby/cli/utils.py +12 -5
  36. gobby/clones/__init__.py +13 -0
  37. gobby/clones/git.py +547 -0
  38. gobby/conductor/__init__.py +16 -0
  39. gobby/conductor/alerts.py +135 -0
  40. gobby/conductor/loop.py +164 -0
  41. gobby/conductor/monitors/__init__.py +11 -0
  42. gobby/conductor/monitors/agents.py +116 -0
  43. gobby/conductor/monitors/tasks.py +155 -0
  44. gobby/conductor/pricing.py +234 -0
  45. gobby/conductor/token_tracker.py +160 -0
  46. gobby/config/__init__.py +12 -97
  47. gobby/config/app.py +69 -91
  48. gobby/config/extensions.py +2 -2
  49. gobby/config/features.py +7 -130
  50. gobby/config/search.py +110 -0
  51. gobby/config/servers.py +1 -1
  52. gobby/config/skills.py +43 -0
  53. gobby/config/tasks.py +9 -41
  54. gobby/hooks/__init__.py +0 -13
  55. gobby/hooks/event_handlers.py +188 -2
  56. gobby/hooks/hook_manager.py +50 -4
  57. gobby/hooks/plugins.py +1 -1
  58. gobby/hooks/skill_manager.py +130 -0
  59. gobby/hooks/webhooks.py +1 -1
  60. gobby/install/claude/hooks/hook_dispatcher.py +4 -4
  61. gobby/install/codex/hooks/hook_dispatcher.py +1 -1
  62. gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
  63. gobby/llm/claude.py +22 -34
  64. gobby/llm/claude_executor.py +46 -256
  65. gobby/llm/codex_executor.py +59 -291
  66. gobby/llm/executor.py +21 -0
  67. gobby/llm/gemini.py +134 -110
  68. gobby/llm/litellm_executor.py +143 -6
  69. gobby/llm/resolver.py +98 -35
  70. gobby/mcp_proxy/importer.py +62 -4
  71. gobby/mcp_proxy/instructions.py +56 -0
  72. gobby/mcp_proxy/models.py +15 -0
  73. gobby/mcp_proxy/registries.py +68 -8
  74. gobby/mcp_proxy/server.py +33 -3
  75. gobby/mcp_proxy/services/recommendation.py +43 -11
  76. gobby/mcp_proxy/services/tool_proxy.py +81 -1
  77. gobby/mcp_proxy/stdio.py +2 -1
  78. gobby/mcp_proxy/tools/__init__.py +0 -2
  79. gobby/mcp_proxy/tools/agent_messaging.py +317 -0
  80. gobby/mcp_proxy/tools/agents.py +31 -731
  81. gobby/mcp_proxy/tools/clones.py +518 -0
  82. gobby/mcp_proxy/tools/memory.py +3 -26
  83. gobby/mcp_proxy/tools/metrics.py +65 -1
  84. gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
  85. gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
  86. gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
  87. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  88. gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
  89. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  90. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  91. gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
  92. gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
  93. gobby/mcp_proxy/tools/skills/__init__.py +616 -0
  94. gobby/mcp_proxy/tools/spawn_agent.py +417 -0
  95. gobby/mcp_proxy/tools/task_orchestration.py +7 -0
  96. gobby/mcp_proxy/tools/task_readiness.py +14 -0
  97. gobby/mcp_proxy/tools/task_sync.py +1 -1
  98. gobby/mcp_proxy/tools/tasks/_context.py +0 -20
  99. gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
  100. gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
  101. gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
  102. gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
  103. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
  104. gobby/mcp_proxy/tools/workflows.py +1 -1
  105. gobby/mcp_proxy/tools/worktrees.py +0 -338
  106. gobby/memory/backends/__init__.py +6 -1
  107. gobby/memory/backends/mem0.py +6 -1
  108. gobby/memory/extractor.py +477 -0
  109. gobby/memory/ingestion/__init__.py +5 -0
  110. gobby/memory/ingestion/multimodal.py +221 -0
  111. gobby/memory/manager.py +73 -285
  112. gobby/memory/search/__init__.py +10 -0
  113. gobby/memory/search/coordinator.py +248 -0
  114. gobby/memory/services/__init__.py +5 -0
  115. gobby/memory/services/crossref.py +142 -0
  116. gobby/prompts/loader.py +5 -2
  117. gobby/runner.py +37 -16
  118. gobby/search/__init__.py +48 -6
  119. gobby/search/backends/__init__.py +159 -0
  120. gobby/search/backends/embedding.py +225 -0
  121. gobby/search/embeddings.py +238 -0
  122. gobby/search/models.py +148 -0
  123. gobby/search/unified.py +496 -0
  124. gobby/servers/http.py +24 -12
  125. gobby/servers/routes/admin.py +294 -0
  126. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  127. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  128. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  129. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  130. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  131. gobby/servers/routes/mcp/hooks.py +1 -1
  132. gobby/servers/routes/mcp/tools.py +48 -1317
  133. gobby/servers/websocket.py +2 -2
  134. gobby/sessions/analyzer.py +2 -0
  135. gobby/sessions/lifecycle.py +1 -1
  136. gobby/sessions/processor.py +10 -0
  137. gobby/sessions/transcripts/base.py +2 -0
  138. gobby/sessions/transcripts/claude.py +79 -10
  139. gobby/skills/__init__.py +91 -0
  140. gobby/skills/loader.py +685 -0
  141. gobby/skills/manager.py +384 -0
  142. gobby/skills/parser.py +286 -0
  143. gobby/skills/search.py +463 -0
  144. gobby/skills/sync.py +119 -0
  145. gobby/skills/updater.py +385 -0
  146. gobby/skills/validator.py +368 -0
  147. gobby/storage/clones.py +378 -0
  148. gobby/storage/database.py +1 -1
  149. gobby/storage/memories.py +43 -13
  150. gobby/storage/migrations.py +162 -201
  151. gobby/storage/sessions.py +116 -7
  152. gobby/storage/skills.py +782 -0
  153. gobby/storage/tasks/_crud.py +4 -4
  154. gobby/storage/tasks/_lifecycle.py +57 -7
  155. gobby/storage/tasks/_manager.py +14 -5
  156. gobby/storage/tasks/_models.py +8 -3
  157. gobby/sync/memories.py +40 -5
  158. gobby/sync/tasks.py +83 -6
  159. gobby/tasks/__init__.py +1 -2
  160. gobby/tasks/external_validator.py +1 -1
  161. gobby/tasks/validation.py +46 -35
  162. gobby/tools/summarizer.py +91 -10
  163. gobby/tui/api_client.py +4 -7
  164. gobby/tui/app.py +5 -3
  165. gobby/tui/screens/orchestrator.py +1 -2
  166. gobby/tui/screens/tasks.py +2 -4
  167. gobby/tui/ws_client.py +1 -1
  168. gobby/utils/daemon_client.py +2 -2
  169. gobby/utils/project_context.py +2 -3
  170. gobby/utils/status.py +13 -0
  171. gobby/workflows/actions.py +221 -1135
  172. gobby/workflows/artifact_actions.py +31 -0
  173. gobby/workflows/autonomous_actions.py +11 -0
  174. gobby/workflows/context_actions.py +93 -1
  175. gobby/workflows/detection_helpers.py +115 -31
  176. gobby/workflows/enforcement/__init__.py +47 -0
  177. gobby/workflows/enforcement/blocking.py +269 -0
  178. gobby/workflows/enforcement/commit_policy.py +283 -0
  179. gobby/workflows/enforcement/handlers.py +269 -0
  180. gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
  181. gobby/workflows/engine.py +13 -2
  182. gobby/workflows/git_utils.py +106 -0
  183. gobby/workflows/lifecycle_evaluator.py +29 -1
  184. gobby/workflows/llm_actions.py +30 -0
  185. gobby/workflows/loader.py +19 -6
  186. gobby/workflows/mcp_actions.py +20 -1
  187. gobby/workflows/memory_actions.py +154 -0
  188. gobby/workflows/safe_evaluator.py +183 -0
  189. gobby/workflows/session_actions.py +44 -0
  190. gobby/workflows/state_actions.py +60 -1
  191. gobby/workflows/stop_signal_actions.py +55 -0
  192. gobby/workflows/summary_actions.py +111 -1
  193. gobby/workflows/task_sync_actions.py +347 -0
  194. gobby/workflows/todo_actions.py +34 -1
  195. gobby/workflows/webhook_actions.py +185 -0
  196. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
  197. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
  198. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
  199. gobby/adapters/codex.py +0 -1292
  200. gobby/install/claude/commands/gobby/bug.md +0 -51
  201. gobby/install/claude/commands/gobby/chore.md +0 -51
  202. gobby/install/claude/commands/gobby/epic.md +0 -52
  203. gobby/install/claude/commands/gobby/eval.md +0 -235
  204. gobby/install/claude/commands/gobby/feat.md +0 -49
  205. gobby/install/claude/commands/gobby/nit.md +0 -52
  206. gobby/install/claude/commands/gobby/ref.md +0 -52
  207. gobby/install/codex/prompts/forget.md +0 -7
  208. gobby/install/codex/prompts/memories.md +0 -7
  209. gobby/install/codex/prompts/recall.md +0 -7
  210. gobby/install/codex/prompts/remember.md +0 -13
  211. gobby/llm/gemini_executor.py +0 -339
  212. gobby/mcp_proxy/tools/session_messages.py +0 -1056
  213. gobby/mcp_proxy/tools/task_expansion.py +0 -591
  214. gobby/prompts/defaults/expansion/system.md +0 -119
  215. gobby/prompts/defaults/expansion/user.md +0 -48
  216. gobby/prompts/defaults/external_validation/agent.md +0 -72
  217. gobby/prompts/defaults/external_validation/external.md +0 -63
  218. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  219. gobby/prompts/defaults/external_validation/system.md +0 -6
  220. gobby/prompts/defaults/features/import_mcp.md +0 -22
  221. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  222. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  223. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  224. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  225. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  226. gobby/prompts/defaults/features/server_description.md +0 -20
  227. gobby/prompts/defaults/features/server_description_system.md +0 -6
  228. gobby/prompts/defaults/features/task_description.md +0 -31
  229. gobby/prompts/defaults/features/task_description_system.md +0 -6
  230. gobby/prompts/defaults/features/tool_summary.md +0 -17
  231. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  232. gobby/prompts/defaults/research/step.md +0 -58
  233. gobby/prompts/defaults/validation/criteria.md +0 -47
  234. gobby/prompts/defaults/validation/validate.md +0 -38
  235. gobby/storage/migrations_legacy.py +0 -1359
  236. gobby/tasks/context.py +0 -747
  237. gobby/tasks/criteria.py +0 -342
  238. gobby/tasks/expansion.py +0 -626
  239. gobby/tasks/prompts/expand.py +0 -327
  240. gobby/tasks/research.py +0 -421
  241. gobby/tasks/tdd.py +0 -352
  242. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
  243. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
  244. {gobby-0.2.5.dist-info → gobby-0.2.7.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
+ ]
@@ -73,7 +73,7 @@ def create_hooks_router(server: "HTTPServer") -> APIRouter:
73
73
  # Select adapter based on source
74
74
  from gobby.adapters.base import BaseAdapter
75
75
  from gobby.adapters.claude_code import ClaudeCodeAdapter
76
- from gobby.adapters.codex import CodexNotifyAdapter
76
+ from gobby.adapters.codex_impl.adapter import CodexNotifyAdapter
77
77
  from gobby.adapters.gemini import GeminiAdapter
78
78
 
79
79
  if source == "claude":